Courses

Vue Inertia + Laravel 11: From Scratch

From Vue + API to Vue Inertia: Table of Data

In the first practical lesson, we will convert Vue.js + API project to Inertia.


Vue + API

First, the code "before": how does it look with Vue + API using Composition API and composables?

resources/views/js/components/Posts/Index.vue:

<script setup>
import usePosts from '../../composables/posts.js';
import { onMounted } from 'vue';
 
const { posts, getPosts } = usePosts()
 
onMounted(() => {
getPosts()
})
</script>
 
<template>
<table class="min-w-full divide-y divide-gray-200 border">
<thead>
<tr>
<th class="px-6 py-3 bg-gray-50 text-left">
<div class="flex flex-row items-center justify-between cursor-pointer">
<div class="leading-4 font-medium text-gray-500 uppercase tracking-wider">
ID
</div>
</div>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<div class="flex flex-row items-center justify-between cursor-pointer">
<div class="leading-4 font-medium text-gray-500 uppercase tracking-wider">
Title
</div>
</div>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Content</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Created At</span>
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200 divide-solid">
<tr v-for="post in posts">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ post.id }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ post.title }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ post.content }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ post.created_at }}
</td>
</tr>
</tbody>
</table>
</template>

resources/views/js/composables/posts.js:

import { ref } from 'vue';
 
export default function usePosts() {
const posts = ref({})
 
const getPosts = async () => {
axios.get('/api/posts')
.then(response => posts.value = response.data.data)
}
 
return { posts, getPosts }
}

app/Http/Controllers/PostController.php:

use App\Models\Post;
use App\Http\Resources\PostResource;
use Illuminate\Http\Resources\Json\JsonResource;
 
class PostController extends Controller
{
public function index(): JsonResource
{
return PostResource::collection(Post::all());
}
}

app/Http/Resources/PostResource.php:

class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => substr($this->content, 0, 50) . '...',
'created_at' => $this->created_at->toDateString(),
];
}
}

Before using Inertia, we must install it. Installation is for server-side and client-side. First, the server side.


Inertia: Server Side Installation

composer require inertiajs/inertia-laravel

Next, we must set up the root template. Because I used Laravel Breeze as the initial starter, I will replace the resources/views/layouts/app.blade.php with the content from the Inertia documentation.

resources/views/layouts/app.blade.php:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
@vite('resources/js/app.js')
@inertiaHead
</head>
<body>
@inertia
</body>
</html>

Now, styles won't be loaded. We can also add app.css to the @vite directive to load styles.

resources/views/layouts/app.blade.php:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
@vite('resources/js/app.js')
@vite(['resources/css/app.css', 'resources/js/app.js'])
@inertiaHead
</head>
<body>
@inertia
</body>
</html>

Lastly, we need to set up the Inertia middleware.

php artisan inertia:middleware

In Laravel 11, Middlewares are managed in the bootstrap/app.php file.

bootstrap/app.php:

use App\Http\Middleware\HandleInertiaRequests;
 
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
HandleInertiaRequests::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();

Now, let's set up the client side.


Inertia: Client Side Installation

Notice: Vue.js must be installed already.

npm install @inertiajs/vue3

resources/js/app.js:

import './bootstrap';
 
import { createApp } from 'vue'
import PostsIndex from './components/Posts/Index.vue'
 
createApp({})
.component('PostsIndex', PostsIndex)
.mount('#app')
 
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
 
createInertiaApp({
resolve: name => {
const pages = import.meta.glob('./Pages/**/*.vue', { eager: true })
return pages[`./Pages/${name}.vue`]
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el)
},
})

Lastly, Inertia expects the root view to be in the resources/views folder. Because I used Breeze with the Blade, the root view is inside the resources/views/layouts folder. This can be easily changed.

app/Http/Middleware/HandleInertiaRequests.php:

class HandleInertiaRequests extends Middleware
{
protected $rootView = 'app';
protected $rootView = 'layouts.app';
 
// ...
}

Showing Table with Inertia

Now that we have Inertia installed, let's use it to show posts in the table. First, we must render inertia and pass the posts in the Controller.

app/Http/Controllers/PostController.php:

use Inertia\Inertia;
use Inertia\Response;
 
class PostController extends Controller
{
public function index(): JsonResource
public function index(): Response
{
$posts = Post::all();
 
return PostResource::collection(Post::all());
return Inertia::render('Posts/Index', compact('posts'));
}
}

Then, instead of resources/js/components, Inertia looks for pages inside resources/js/Pages. Instead of using a composable now, the posts are passed using props in the Vue component.

resources/js/Pages/Posts/Index.vue:

<script setup>
import usePosts from '../../composables/posts.js';
import { onMounted } from 'vue';
 
const { posts, getPosts } = usePosts()
 
onMounted(() => {
getPosts()
})
 
const props = defineProps({
posts: {
type: Object,
required: true
}
})
</script>
 
<template>
// ...
</template>

And the route to show posts.

routes/web.php:

use App\Http\Controllers\PostController;
 
Route::view('/', 'dashboard')->name('dashboard');
 
Route::get('posts', [PostController::class, 'index']);

If we visit the homepage, we will get an error because we haven't changed anything to work with Inertia.

But, if we visit the posts page, we should see a table of the posts.

Of course, we don't have any design, and we lost some things implemented on the Vue, like shortening the content, but we do have the posts passed from the Controller, and it works in Inertia.


Conclusion: Benefits?

This is the main benefit and the primary goal of Inertia. You would stick with your logic to Laravel and get the data from Laravel and would not need to create separate API, separate composition API in VueJS, or something more complex.

This was the basic example. Let's move on and see what Inertia offers more.

No comments or questions yet...