Создание SPA с помощью Laravel, Inertia.js, React и TypeScript

Создание SPA с помощью Laravel, Inertia.js, React и TypeScript

Вот что происходит на практике: каждый раз, когда мы взвешиваем между «олдскульным» подходом с полной перезагрузкой страниц и современными SPA, чувствуешь себя чуть ли не перед выбором жить в доме с полным набором удобств — но отдельно от кухни! Первый способ прост — написал код, форма отправилась, все обновилось, работает. Но интерфейс такой вариант никогда не балует: мало интерактивности, каждый клик — маленькая пауза.

SPA-решение на другом полюсе: приложения реагируют мгновенно, маршруты внутри работают быстро и плавно, только вот фронтенд теперь живет своей жизнью (обычно на React или Vue), а бэкенд превращается просто в набор API-эндпоинтов. Всё бы хорошо, да вот мороки: приходится заново продумывать аутентификацию, синхронизировать состояние приложений и поддерживать сразу две отдельные системы.

Inertia.js тут реально спасает — он умудряется совместить лучшее из обоих миров. Ты по-прежнему пишешь контроллеры в Laravel (или другом любимом backend-фреймворке), пользуешься знакомой серверной маршрутизацией, а интерфейс при этом собираешь как современное SPA-компонентное приложение (на React/Vue/Svelte, кто во что горазд). И самое приятное — не нужно мутить отдельный REST или GraphQL API ради всего этого.

Добавьте сюда TypeScript (а мы уже не можем без строгих типов) — и получается крепкая связка типобезопасности и быстрой разработки. В итоге мы получаем почти магию: весь интерактив single-page-приложений плюс уют фреймворков вроде Laravel без бесконечных “fetch” на стороне клиента.

В этом руководстве мы пошагово разберём создание SPA на стеке Laravel + Inertia.js + React + TypeScript.

Что такое Inertia.js и как это работает

Inertia.js — это не фреймворк, а связующий слой (glue) между серверной и клиентской частью. Ключевая идея в том, что вместо возврата HTML-страниц или JSON-данных ваши контроллеры возвращают Inertia-ответы, содержащие имя компонента и данные для него (props).

При первой загрузке страницы Inertia отдаёт полноценный HTML. При последующих переходах по ссылкам он перехватывает клики, отправляет XHR-запрос, получает JSON с новым компонентом и его props, после чего динамически подменяет компонент без перезагрузки страницы. Пользователь получает опыт SPA, а разработчик пишет код, как в обычном монолитном приложении.

Шаг 1. Установка Laravel

Начнём с создания нового проекта Laravel. У вас уже должны быть установлены PHP 8.2+, Composer и Node.js.

composer create-project laravel/laravel inertia-app
cd inertia-app

Самый простой способ настроить Inertia с React и TypeScript — использовать официальный стартовый набор Laravel, но мы разберём ручную установку для полного понимания процесса.

Шаг 2. Установка серверной части Inertia

Устанавливаем адаптер Inertia для Laravel:

composer require inertiajs/inertia-laravel

Далее создадим шаблон, который будет служить точкой входа. Создадим файл resources/views/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">
    @viteReactRefresh
    @vite(['resources/js/app.tsx'])
    @inertiaHead
</head>
<body>
    @inertia
</body>
</html>

Теперь опубликуем и настроим middleware Inertia:

php artisan inertia:middleware

Зарегистрируем созданный middleware в файле bootstrap/app.php:

use App\Http\Middleware\HandleInertiaRequests;

->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        HandleInertiaRequests::class,
    ]);
})

Шаг 3. Настройка клиентской части

Устанавливаем необходимые пакеты для React, TypeScript и Inertia используя npm:

npm install @inertiajs/react react react-dom
npm install -D typescript @types/react @types/react-dom @vitejs/plugin-react

Обновим конфигурацию Vite в файле vite.config.ts:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/js/app.tsx'],
            refresh: true,
        }),
        react(),
    ],
});

Создадим файл конфигурации tsconfig.json для TypeScript в корне проекта:

{
    "compilerOptions": {
        "target": "ESNext",
        "module": "ESNext",
        "moduleResolution": "bundler",
        "jsx": "react-jsx",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "baseUrl": ".",
        "paths": {
            "@/*": ["resources/js/*"]
        }
    },
    "include": ["resources/js/**/*.ts", "resources/js/**/*.tsx"]
}

Шаг 4. Создание точки входа приложения

Создадим главный файл resources/js/app.tsx:

import { createInertiaApp } from '@inertiajs/react';
import { createRoot } from 'react-dom/client';

createInertiaApp({
    resolve: (name) => {
        const pages = import.meta.glob('./Pages/**/*.tsx', { eager: true });
        return pages[`./Pages/${name}.tsx`];
    },
    setup({ el, App, props }) {
        createRoot(el).render(<App {...props} />);
    },
});

import.meta.glob — автоматически подгружает все компоненты страниц из папки Pages.

resolve — функция сопоставляет имя компонента, переданное из контроллера, с реальным файлом.

Шаг 5. Создание первой страницы

Создадим компонент resources/js/Pages/Home.tsx:

import { Head, Link } from '@inertiajs/react';

interface HomeProps {
    title: string;
    users: Array<{ id: number; name: string }>;
}

export default function Home({ title, users }: HomeProps) {
    return (
        <>
            <Head title={title} />
            <div className="container">
                <h1>{title}</h1>
                <ul>
                    {users.map((user) => (
                        <li key={user.id}>
                            <Link href={`/users/${user.id}`}>
                                {user.name}
                            </Link>
                        </li>
                    ))}
                </ul>
            </div>
        </>
    );
}

Здесь компонент Link (Inertia) перехватывает переходы и обеспечивает работу SPA без перезагрузки.

Интерфейс HomeProps описывает типы props, что даёт защиту от ошибок.

Шаг 6. Настройка маршрутов и контроллера

Добавим маршрут в routes/web.php:

use App\Http\Controllers\HomeController;

Route::get('/', [HomeController::class, 'index']);

Создадим контроллер и заполним его логикой:

php artisan make:controller HomeController
namespace App\Http\Controllers;

use Inertia\Inertia;
use Inertia\Response;

class HomeController extends Controller
{
    public function index(): Response
    {
        return Inertia::render('Home', [
            'title' => 'Список пользователей',
            'users' => [
                ['id' => 1, 'name' => 'Вася'],
                ['id' => 2, 'name' => 'Маша'],
            ],
        ]);
    }
}

Метод Inertia::render принимает имя компонента и массив props. Эти данные автоматически будут переданы в React-компонент.

Шаг 7. Работа с формами

Inertia предоставляет удобный хук useForm для работы с формами, используем его:

import { useForm } from '@inertiajs/react';
import { FormEvent } from 'react';

export default function CreateUser() {
    const { data, setData, post, processing, errors } = useForm({
        name: '',
        email: '',
    });

    function submit(e: FormEvent) {
        e.preventDefault();
        post('/users');
    }

    return (
        <form onSubmit={submit}>
            <input
                value={data.name}
                onChange={(e) => setData('name', e.target.value)}
            />
            {errors.name && <div>{errors.name}</div>}

            <input
                value={data.email}
                onChange={(e) => setData('email', e.target.value)}
            />
            {errors.email && <div>{errors.email}</div>}

            <button type="submit" disabled={processing}>
                Создать
            </button>
        </form>
    );
}

Ошибки валидации формы автоматически попадают в объект errors.

Шаг 8. Общие данные и типизация

Часто требуется передавать данные на все страницы. Это делается в методе share посредника (middleware) HandleInertiaRequests:

public function share(Request $request): array
{
    return [
        ...parent::share($request),
        'auth' => [
            'user' => $request->user(),
        ],
    ];
}

Для типизации глобальных пропсов рекомендую создавать отдельные файлы, например resources/js/types/index.d.ts:

export interface User {
    id: number;
    name: string;
    email: string;
}

export interface PageProps {
    auth: {
        user: User | null;
    };
}

Запуск приложения

Запускаем сборку фронтенда и Laravel сервер:

npm run dev
php artisan serve

Открываем сайт по адресу указанному после сборки, обычно http://localhost:8000 — вы увидите работающее SPA.

Заключение

Если коротко — работать с Laravel, Inertia.js, React и TypeScript вместе действительно кайфово. Это по сути как взять лучшие куски мира Single Page Application (например, моментальные переходы между страницами и живой интерфейс, когда ничего не моргает и страница никуда не перезагружается), но при этом вам не приходится жертвовать уютом классического бэкенда на Laravel. Все эти любимые фишки вроде маршрутизации через роуты, контроллеры с их валидацией, авторизацию “из коробки” — всё это никуда не девается.

Чем хорош TypeScript? Вроде бы только язык с типами, а на деле экономит кучу нервов: ловит ошибки ещё до того, как код попал в браузер (и до того, как на них наткнулся кто-то из вашей команды или пользователи). Особенно заметно это на проектах с несколькими разработчиками: меньше непонятных багов, проще договариваться о том “что куда приходит” и легче проводить рефакторинги — старый код не ломается так внезапно.

Когда всё это собирается воедино — становится реально проще жить. Приложение получается быстрое, поддерживать его приятно, а если завтра подрастёт команда или проекты станут масштабнее — ваш стек не начнёт скрипеть под нагрузкой. Освоить такой набор инструментов — всё равно что вложиться в хороший фундамент для своего будущего продукта.

Предыдущая статья Этапы создания сайта