Laravel 11 Inertia CRUD with React TypeScript Example

Learn how to build a CRUD application with Laravel, Inertia.js, and React.js. This tutorial will walk you through creating a modern, interactive SPA with Laravel as the backend, Inertia.js for seamless page transitions, and React.js for dynamic front-end features. Let’s dive in and create a powerful web app from scratch!

Step 1: Install Laravel and Configure Database

Installing a fresh new laravel application.

composer create-project laravel/laravel inertia-react

To connect your Laravel application to a database, you’ll need to update the .env configuration file with your database credentials. Open the .env file and enter your database details as follows:

.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=database_name
DB_USERNAME=database_user_name
DB_PASSWORD=database_password

Step 2: Install Breeze and Set Up Inertia.js with React

Install laravel breeze via composer.

composer require laravel/breeze --dev

Next, run below command.

php artisan breeze:install
  Which Breeze stack would you like to install?
  Blade with Alpine ................................ blade
  Livewire (Volt Class API) with Alpine ............ livewire
  Livewire (Volt Functional API) with Alpine ........ livewire-functional
  React with Inertia ........................ react
  Vue with Inertia ............................... vue
  API only .................... api
 react

Next, You need to select typescript.

Step 3: Create Post Model and Resource Controller

To create a Post model, migration, and controller, run the following command in your terminal:

php artisan make:model Post -mcr
  • -m generates a migration file.
  • -c generates a controller.
  • -r adds resource methods to the controller.

This command will create:

  1. A Post model in app/Models/Post.php.
  2. A migration file in database/migrations/ for the posts table.
  3. A resource controller in app/Http/Controllers/PostController.php with default CRUD methods.
create_posts_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};
PHP
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = [
        'title',
        'description'
    ];
    
    use HasFactory;
}

Step 4: Implement Form Request Validation, Update Controller, and Define Routes

To handle validation for creating and updating posts, you need to create a form request. Follow these steps:

  1. Generate the Form Request:
    • Run the following command to create a StorePostRequest class:
php artisan make:request StorePostRequest
  1. Define Validation Rules:
    • Open the newly created PostRequest file and add your validation rules in the rules method. For example:
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true; // Adjust this based on your authorization logic
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'title' => 'required|string|max:255',
            'description' => 'required|string'
        ];
    }
}
  1. Use the Form Request in Your Controller: also add Redirect, Inertia.
PostController.php
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use App\Http\Requests\StorePostRequest;
use Illuminate\Support\Facades\Redirect;
use Inertia\Inertia;

class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        $posts = Post::latest()->get();

        return Inertia::render('Post/Index', ['posts' => $posts]);
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return Inertia::render('Post/Create');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StorePostRequest  $request)
    {
        Post::create(
            $request->validated()
        );
        return Redirect::route('posts.index');

    }

    /**
     * Display the specified resource.
     */
    public function show(Post $post)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(Post $post)
    {
        return Inertia::render('Post/Edit', [
            'post' => [
                'id' => $post->id,
                'title' => $post->title,
                'description' => $post->description
            ]
        ]);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(StorePostRequest $request, Post $post)
    {
        $post->update($request->validated());

        return Redirect::route('posts.index');
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Post $post)
    {
        $post->delete();

        return Redirect::route('posts.index');
    }
}
  1. Define Routes
web.php
<?php

use App\Http\Controllers\ProfileController;
use App\Http\Controllers\PostController;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

Route::get('/', function () {
    return Inertia::render('Welcome', [
        'canLogin' => Route::has('login'),
        'canRegister' => Route::has('register'),
        'laravelVersion' => Application::VERSION,
        'phpVersion' => PHP_VERSION,
    ]);
});

Route::get('/dashboard', function () {
    return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');

Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
   
});
Route::resource('posts', PostController::class);
require __DIR__.'/auth.php';

Step 5: Create React.js Views for CRUD Operations

  • Create the Create View

How to Use Inertia Links in Laravel 11 with React

Generate the Create view for adding new posts:

resources/js/Pages/Post/Create.tsx
import React from "react";
import { Link, useForm } from "@inertiajs/react";
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { PageProps } from '@/types';

// Define the types for form data and errors
interface FormData {
    title: string;
    description: string;
}

interface FormErrors {
    title?: string;
    description?: string;
}

const Create = ({ auth }: PageProps) => {
    const { data, setData, errors, post } = useForm<FormData>({
        title: "",
        description: "",
    });

    // Handle form submission
    function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
        e.preventDefault();
        post(route("posts.store"));
    }

    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Dashboard</h2>}
        >
                <div className="container mx-auto max-w-7xl">
                    <div>
                        <h1 className="mb-8 text-3xl font-bold mt-8">
                            <Link
                                href={route("posts.index")}
                                className="text-blue-600 font-bold hover:text-blue-700"
                            >
                                Posts
                            </Link>
                            <span className="font-medium text-blue-600"> / </span>
                            Create
                        </h1>
                    </div>
                    <div className="max-w-6xl p-8 bg-white rounded shadow">
                        <form name="createForm" onSubmit={handleSubmit}>
                            <div className="flex flex-col">
                                <div className="mb-4">
                                    <label htmlFor="title" className="block text-gray-700">Title</label>
                                    <input
                                        type="text"
                                        id="title"
                                        className="w-full px-4 py-2 border border-gray-300 rounded-md"
                                        name="title"
                                        value={data.title}
                                        onChange={(e) =>
                                            setData("title", e.target.value)
                                        }
                                    />
                                    {errors.title && (
                                        <span className="text-red-600 text-sm">
                                            {errors.title}
                                        </span>
                                    )}
                                </div>
                                <div className="mb-4">
                                    <label htmlFor="description" className="block text-gray-700">Description</label>
                                    <textarea
                                        id="description"
                                        className="w-full px-4 py-2 border border-gray-300 rounded-md"
                                        name="description"
                                        value={data.description}
                                        onChange={(e) =>
                                            setData("description", e.target.value)
                                        }
                                    />
                                    {errors.description && (
                                        <span className="text-red-600 text-sm">
                                            {errors.description}
                                        </span>
                                    )}
                                </div>
                            </div>
                            <div>
                                <button
                                    type="submit"
                                    className="px-3 py-1.5  text-white bg-blue-500 rounded hover:bg-blue-600"
                                >
                                    Save
                                </button>
                            </div>
                        </form>
                    </div>
                </div>
           
        </AuthenticatedLayout>
    );
};

export default Create;
inertia react create
  • Create the Index View

Tailwind CSS Simple Table Example

Generate the Index view for listing posts:

resources/js/Pages/Post/Index.tsx
import React from "react";
import { Link, usePage, router } from "@inertiajs/react";
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { PageProps } from '@/types';

// Define the type for a single post
interface Post {
    id: number;
    title: string;
    description: string;
}

// Adjust the type to reflect the correct structure of posts
interface Posts {
    data: Post[];
}

const Index = ({ auth }: PageProps) => {
    const { posts } = usePage().props;
    const data: Post[] = posts; 
    console.log(posts); // Check the structure in the console

    // Function to handle delete action
    const handleDelete = (id: number) => {
        if (confirm("Are you sure you want to delete this post?")) {
            router.delete(route("posts.destroy", id));
        }
    };

    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Dashboard</h2>}
        >
            <div className="container mx-auto max-w-7xl mt-4">
                <h1 className="mb-8 text-4xl font-bold text-center">Posts Index</h1>
                <div className="flex items-center justify-between mb-6">
                    <Link 
                        className="px-3 py-1.5 text-white bg-blue-500 rounded-md focus:outline-none"
                        href={route("posts.create")}
                    >
                        Create Post
                    </Link >
                </div>

                <div className="overflow-x-auto">
                    <table className="min-w-full bg-white">
                        <thead>
                            <tr>
                                <th className="px-4 py-2 bg-gray-200 text-gray-600 border-b border-gray-300 text-left text-sm uppercase font-semibold">
                                    #
                                </th>
                                <th className="px-4 py-2 bg-gray-200 text-gray-600 border-b border-gray-300 text-left text-sm uppercase font-semibold">
                                    Title
                                </th>
                                <th className="px-4 py-2 bg-gray-200 text-gray-600 border-b border-gray-300 text-left text-sm uppercase font-semibold">
                                    Description
                                </th>
                                <th className="px-4 py-2 bg-gray-200 text-gray-600 border-b border-gray-300 text-left text-sm uppercase font-semibold">
                                    Actions
                                </th>
                            </tr>
                        </thead>
                        <tbody>
                            {data.length > 0 ? (
                                data.map(({ id, title, description }) => (
                                    <tr key={id}>
                                        <td className="px-4 py-2 border-b border-gray-300">
                                            {id}
                                        </td>
                                        <td className="px-4 py-2 border-b border-gray-300">
                                            {title}
                                        </td>
                                        <td className="px-4 py-2 border-b border-gray-300">
                                            {description}
                                        </td>
                                        <td className="px-4 py-2 border-b border-gray-300">
                                            <Link 
                                                className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 rounded text-xs mr-1"
                                                href={route("posts.edit", id)}
                                            >
                                                Edit
                                            </Link>
                                            <button 
                                                className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded text-xs"
                                                onClick={() => handleDelete(id)} // Trigger delete function
                                            >
                                                Delete
                                            </button>
                                        </td>
                                    </tr>
                                ))
                            ) : (
                                <tr>
                                    <td
                                        className="px-4 py-2 border-b border-gray-300"
                                        colSpan="4"
                                    >
                                        No posts found.
                                    </td>
                                </tr>
                            )}
                        </tbody>
                    </table>
                </div>
            </div>
        </AuthenticatedLayout>
    );
};

export default Index;
inertia react index
  • Create the Edit View

Generate the Edit view for updating existing posts:

resources/js/Pages/Post/Edit.tsx
import React from "react";
import { Link, usePage, useForm, router  } from "@inertiajs/react";
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { PageProps } from '@/types';

// Define types for form data and errors
interface Post {
    id: number;
    title: string;
    description: string;
}

interface FormErrors {
    title?: string;
    description?: string;
}

const Edit = ({ auth }: PageProps) => {
    const { post }: { post: Post } = usePage().props;
    const { data, setData, put, errors } = useForm({
        title: post.title || "",
        description: post.description || "",
    });

    function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
        e.preventDefault();
        put(route("posts.update", post.id));
    }

    function destroy() {
        if (confirm("Are you sure you want to delete this post?")) {
            router.delete(route("posts.destroy", post.id));
        }
    }

    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Dashboard</h2>}
        >
            <div className="container mx-auto max-w-7xl mt-8">
                <div>
                    <h1 className="mb-8 text-3xl font-bold">
                        <Link
                            href={route("posts.index")}
                            className="text-blue-600 hover:text-blue-700"
                        >
                            Posts
                        </Link>
                        <span className="font-medium text-blue-600"> / </span>
                        Edit
                    </h1>
                </div>
                <div className="max-w-6xl p-8 bg-white rounded shadow">
                    <form name="editForm" onSubmit={handleSubmit}>
                        <div className="flex flex-col space-y-4">
                            <div>
                                <label htmlFor="title" className="block text-gray-700">Title</label>
                                <input
                                    type="text"
                                    id="title"
                                    className="w-full px-4 py-2 border border-gray-300 rounded-md"
                                    name="title"
                                    value={data.title}
                                    onChange={(e) =>
                                        setData("title", e.target.value)
                                    }
                                />
                                {errors.title && (
                                    <span className="text-red-600 text-sm">
                                        {errors.title}
                                    </span>
                                )}
                            </div>
                            <div>
                                <label htmlFor="description" className="block text-gray-700">Description</label>
                                <textarea
                                    id="description"
                                    className="w-full px-4 py-2 border border-gray-300 rounded-md"
                                    name="description"
                                    value={data.description}
                                    onChange={(e) =>
                                        setData("description", e.target.value)
                                    }
                                />
                                {errors.description && (
                                    <span className="text-red-600 text-sm">
                                        {errors.description}
                                    </span>
                                )}
                            </div>
                        </div>
                        <div className="flex justify-between mt-6">
                            <button
                                type="submit"
                                className="px-3 py-1.5 text-white bg-blue-500 rounded hover:bg-blue-600"
                            >
                                Update
                            </button>
                            <button
                                onClick={destroy}
                                tabIndex={-1}
                                type="button"
                                className="px-3 py-1.5 text-white bg-red-500 rounded hover:bg-red-600"
                            >
                                Delete
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </AuthenticatedLayout>
    );
};

export default Edit;
inertia react index

Note: For an efficient development workflow, it’s recommended to use two terminal windows:

  1. First Terminal: Run your server (e.g., php artisan serve for Laravel).
  2. Second Terminal: Execute additional commands as needed (e.g., npm run dev or yarn dev for building assets).

Tools

Tailwind CSS Gradient Generator

Pixels to Rem (and Rem to Pixels) Converter

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *