In the first practical lesson, we will convert React + API project to Inertia.
React + API
First, the code "before": how does it look with React + API?
resources/views/js/components/Posts/Index.jsx:
import React, { useState, useEffect } from 'react';import usePosts from '../../hooks/usePosts.js'; export default function PostsIndex() { const { posts, getPosts } = usePosts(); useEffect(() => { getPosts(); }, []); return ( <div> <table className="min-w-full divide-y divide-gray-200 border"> <thead> <tr> <th className="px-6 py-3 bg-gray-50 text-left"> <span className="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">ID</span> </th> <th className="px-6 py-3 bg-gray-50 text-left"> <span className="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Title</span> </th> <th className="px-6 py-3 bg-gray-50 text-left"> <span className="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Content</span> </th> <th className="px-6 py-3 bg-gray-50 text-left"> <span className="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Created At</span> </th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200 divide-solid"> {posts && posts.data && posts.data.map((post) => ( <tr key={post.id}> <td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900"> {post.id} </td> <td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900"> {post.title} </td> <td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900"> {post.content} </td> <td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900"> {post.created_at} </td> </tr> ))} </tbody> </table> </div> );};
resources/views/js/hooks/usePosts.js:
import { useState } from 'react';import axios from 'axios'; const usePosts = () => { const [posts, setPosts] = useState({}); const getPosts = async () => { try { const response = await axios.get('/api/posts'); setPosts(response.data); } catch (error) { console.error('Error fetching posts:', error); } }; return { posts, getPosts };}; export default usePosts;
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" /> @viteReactRefresh @vite('resources/js/app.jsx') @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.jsx']) @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: React with React DOM must be installed already.
npm install @inertiajs/react
resources/js/app.js:
import './bootstrap'; import React from 'react'; import { createRoot } from 'react-dom/client';import PostsIndex from './components/Posts/Index.jsx'; const reactComponents = { 'posts-index': PostsIndex,}; document.querySelectorAll('[data-react-component]').forEach(domElement => { const componentName = domElement.dataset.reactComponent; const Component = reactComponents[componentName]; if (Component) { const root = createRoot(domElement); root.render(<Component />); }}); import { createInertiaApp } from '@inertiajs/react' import { createRoot } from 'react-dom/client' createInertiaApp({ resolve: name => { const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true }) return pages[`./Pages/${name}.jsx`] }, setup({ el, App, props }) { createRoot(el).render(<App {...props} />) },})
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 hooks now, the posts are passed using props in the React component.
resources/js/Pages/Posts/Index.jsx:
import React, { useState, useEffect } from 'react'; import usePosts from '../../hooks/usePosts.js'; export default function PostsIndex({ posts }) { export default function PostsIndex() { const { posts, getPosts } = usePosts(); useEffect(() => { getPosts(); }, []); return ( // ... {posts && posts.data && posts.data.map((post) => ( {posts && posts && posts.map((post) => ( // ... );};
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 dashboard, 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 React, 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 hooks API in React, or something more complex.
This was the basic example. Let's move on and see what Inertia offers more.
Would have been nice to have started from scratch instead of converting an API version over.
Also maybe typo, not sure if meant to put view instead of Vue here
Still nice tutorial.