Создание 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? Вроде бы только язык с типами, а на деле экономит кучу нервов: ловит ошибки ещё до того, как код попал в браузер (и до того, как на них наткнулся кто-то из вашей команды или пользователи). Особенно заметно это на проектах с несколькими разработчиками: меньше непонятных багов, проще договариваться о том “что куда приходит” и легче проводить рефакторинги — старый код не ломается так внезапно.
Когда всё это собирается воедино — становится реально проще жить. Приложение получается быстрое, поддерживать его приятно, а если завтра подрастёт команда или проекты станут масштабнее — ваш стек не начнёт скрипеть под нагрузкой. Освоить такой набор инструментов — всё равно что вложиться в хороший фундамент для своего будущего продукта.