Laravel 12 + Inertia + React Full-Stack Tutorial

Laravel 12 + Inertia + React Full-Stack Tutorial

Laravel 12 + Inertia + React Full-Stack Tutorial

In this tutorial, we will build a simple CRUD application to manage posts using Laravel 12 for the backend and React JS for the frontend using Inertia.js. 

What You'll Build:

Post CRUD app (Create, Read, Update, Delete) using:

  • Laravel 12 (Backend)

  • Inertia.js (Bridge between Laravel and React)

  • React.js 18 (Frontend)

Requirements

Make sure you have the following installed:

  • PHP 8.2+

  • Composer

  • Node.js + npm

  • Laravel CLI

  • MySQL/PostgreSQL

  • A web browser (Chrome, Firefox, etc.)

Step 1: Install Laravel 12

composer create-project laravel/laravel laravel-react-crud

Update .env to connect with your database:

DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=react_crud DB_USERNAME=root DB_PASSWORD=

Step 2: Install Breeze with Inertia.js + React

composer require laravel/breeze --dev php artisan breeze:install react npm install npm run dev

This installs Breeze with Inertia + React stack and sets up default auth scaffolding.

Step 3: Create Post Model and Migration

php artisan make:model Post -mcr

In database/migrations/xxxx_create_posts_table.php, Define the schema:

Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('body'); $table->timestamps(); });

Run the migration:

php artisan migrate

Update the model app/Models/Post.php:

protected $fillable = ['title', 'body'];

Step 4: Create Post Controller

Add logic to app/Http/Controllers/PostController.php:

namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Http\Request; use Inertia\Inertia; class PostController extends Controller { public function index() { return Inertia::render('Posts/Index', [ 'posts' => Post::all() ]); } public function create() { return Inertia::render('Posts/Create'); } public function store(Request $request) { $request->validate([ 'title' => 'required', 'body' => 'required', ]); Post::create($request->all()); return redirect()->route('posts.index'); } public function edit(Post $post) { return Inertia::render('Posts/Edit', [ 'post' => $post ]); } public function update(Request $request, Post $post) { $request->validate([ 'title' => 'required', 'body' => 'required', ]); $post->update($request->all()); return redirect()->route('posts.index'); } public function destroy(Post $post) { $post->delete(); return redirect()->route('posts.index'); } }

Step 5: Define Routes

In routes/web.php:

use App\Http\Controllers\PostController; Route::middleware(['auth', 'verified'])->group(function () { Route::resource('posts', PostController::class); });

Step 6: Create React Components

Create a folder:

mkdir resources/js/Pages/Posts

📄 resources/js/Pages/Posts/Index.jsx

import React, { useState } from 'react'; import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { Head, Link, router } from '@inertiajs/react'; export default function Index({ auth, posts }) { const [showDeleteModal, setShowDeleteModal] = useState(false); const [postToDelete, setPostToDelete] = useState(null); const openDeleteModal = (id) => { setPostToDelete(id); setShowDeleteModal(true); }; const cancelDelete = () => { setShowDeleteModal(false); setPostToDelete(null); }; const confirmDelete = () => { if (postToDelete) { router.delete(`/posts/${postToDelete}`, { onSuccess: () => { setShowDeleteModal(false); setPostToDelete(null); }, }); } }; return ( <AuthenticatedLayout user={auth.user}> <Head title="Posts" /> <div className="py-12"> <div className="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div className="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6"> <div className="flex justify-between items-center mb-4"> <h2 className="text-xl font-bold">Post List</h2> <Link href="/posts/create" className="bg-blue-600 text-white text-sm px-2.5 py-1.5 rounded hover:bg-blue-700" > ➕ Create </Link> </div> <table className="w-full table-auto border"> <thead> <tr className="bg-gray-100 text-left"> <th className="border p-2 w-[8%]">No</th> <th className="border p-2">Title</th> <th className="border p-2">Body</th> <th className="border p-2 text-center w-[20%]">Actions</th> </tr> </thead> <tbody> {posts.length === 0 ? ( <tr> <td colSpan="4" className="border p-4 text-center text-gray-500"> No posts available. </td> </tr> ) : ( posts.map((post, index) => ( <tr key={post.id} className="hover:bg-gray-50"> <td className="border p-2 text-center">{index + 1}</td> <td className="border p-2">{post.title}</td> <td className="border p-2">{post.body}</td> <td className="border p-2 text-center space-x-2"> <Link href={`/posts/${post.id}/edit`} className="text-xs text-white bg-blue-600 hover:bg-blue-700 px-2 py-1 rounded" > ✏️ Edit </Link> <button onClick={() => openDeleteModal(post.id)} className="text-xs text-white bg-red-600 hover:bg-red-700 px-2 py-1 rounded" > 🗑️ Delete </button> </td> </tr> )) )} </tbody> </table> </div> </div> </div> {/* Delete Confirmation Modal */} {showDeleteModal && ( <div className="fixed inset-0 flex items-center justify-center z-50 bg-black bg-opacity-50"> <div className="bg-white rounded-lg shadow-md w-full max-w-sm p-6"> <h2 className="text-lg font-semibold text-gray-800 mb-4 text-center"> Confirm Deletion </h2> <p className="text-gray-600 mb-6 text-center"> Are you sure you want to delete this post? </p> <div className="flex justify-center space-x-2"> <button onClick={cancelDelete} className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-semibold py-1 px-4 rounded" > Cancel </button> <button onClick={confirmDelete} className="bg-red-500 hover:bg-red-700 text-white font-semibold py-1 px-4 rounded" > Yes, Delete </button> </div> </div> </div> )} </AuthenticatedLayout> ); }

resources/js/Pages/Posts/Create.jsx

import React from 'react'; import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { Head, Link, useForm } from '@inertiajs/react'; export default function Create({ auth }) { const { data, setData, post, processing, errors } = useForm({ title: '', body: '', }); const submit = (e) => { e.preventDefault(); post('/posts'); }; return ( <AuthenticatedLayout user={auth.user} header={ <h2 className="font-semibold text-xl text-gray-800 leading-tight"> Create Posts </h2> } > <Head title="Create Post" /> <div className="py-12"> <div className="max-w-4xl mx-auto px-2 sm:px-4 lg:px-5"> <div className="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6"> {/* Back Button */} <Link href="/posts" className="inline-flex items-center gap-2 px-4 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded hover:bg-gray-300" > 🔙 <span>Back</span> </Link> {/* Form */} <form onSubmit={submit} className="space-y-6 mt-6"> {/* Title Field */} <div> <label className="block text-sm font-medium text-gray-700 mb-1">Title</label> <input type="text" value={data.title} onChange={(e) => setData('title', e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200" /> {errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>} </div> {/* Body Field */} <div> <label className="block text-sm font-medium text-gray-700 mb-1">Body</label> <textarea value={data.body} onChange={(e) => setData('body', e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200" /> {errors.body && <p className="text-red-500 text-sm mt-1">{errors.body}</p>} </div> {/* Submit Button */} <button type="submit" disabled={processing} className={`bg-green-600 text-white text-sm px-4 py-2 rounded hover:bg-green-700 transition ${ processing ? 'opacity-50 cursor-not-allowed' : '' }`} > ✅ Submit </button> </form> </div> </div> </div> </AuthenticatedLayout> ); }

✏️ resources/js/Pages/Posts/Edit.jsx

import React from 'react'; import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { Head, Link, useForm } from '@inertiajs/react'; export default function Edit({ auth, post }) { const { data, setData, put, processing, errors } = useForm({ title: post.title || '', body: post.body || '', }); const submit = (e) => { e.preventDefault(); put(`/posts/${post.id}`); }; return ( <AuthenticatedLayout user={auth.user} header={ <h2 className="font-semibold text-xl text-gray-800 leading-tight"> Edit Post </h2> } > <Head title="Edit Post" /> <div className="py-12"> <div className="max-w-4xl mx-auto px-2 sm:px-4 lg:px-5"> <div className="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6"> <Link href="/posts" className="inline-flex items-center px-4 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded hover:bg-gray-300" > 🔙 Back </Link> <form onSubmit={submit} className="space-y-4 mt-6"> <div> <label className="block text-sm font-medium text-gray-700 mb-1">Title</label> <input type="text" value={data.title} onChange={(e) => setData('title', e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200" /> {errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>} </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">Body</label> <textarea value={data.body} onChange={(e) => setData('body', e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200" /> {errors.body && <p className="text-red-500 text-sm mt-1">{errors.body}</p>} </div> <button type="submit" disabled={processing} className={`bg-blue-600 text-white text-sm px-4 py-2 rounded hover:bg-blue-700 transition ${ processing ? 'opacity-50 cursor-not-allowed' : '' }`} > 💾 Update </button> </form> </div> </div> </div> </AuthenticatedLayout> ); }

Step 7: Add Posts to Navigation Menu

Open resources/js/Layouts/AuthenticatedLayout.jsx.  , and inside your navigation area:

<NavLink href={route('posts.index')} active={route().current('posts.*')}> Posts </NavLink>

Final Touch

Run the app:

php artisan serve npm run dev

Visit: http://localhost:8000/posts (after login/register)

Summary

You’ve built a full Post CRUD using:

  • Laravel 12 as the backend

  • Inertia.js to connect the back-end and front-end

  • React 18 for UI components

Souy Soeng

Souy Soeng

Hi there 👋, I’m Soeng Souy (StarCode Kh)
-------------------------------------------
🌱 I’m currently creating a sample Laravel and React Vue Livewire
👯 I’m looking to collaborate on open-source PHP & JavaScript projects
💬 Ask me about Laravel, MySQL, or Flutter
⚡ Fun fact: I love turning ☕️ into code!

Post a Comment

CAN FEEDBACK
close