什么是 React
React 是由 Meta(前 Facebook)开发和维护的用户界面构建 JavaScript 库。它以声明式的 UI 写法和基于组件的架构著称,从小组件到完整 SPA 都有广泛应用。
React 的核心是通过虚拟 DOM 实现高效的重渲染。状态(state)发生变化时,React 只把差异反映到 DOM 上,不需要手动操作 DOM。
本文讲解的是 React 19 与 Inertia v3 的组合。Laravel 13 的启动套件默认使用这一组合。
JSX 和 TSX
React 组件用 JSX(JavaScript XML)语法编写。可以在 JavaScript 中直接写类似 HTML 的语法。
// JSX 示例
function Greeting({ name }) {
return <h1>你好,{name}</h1>
}
启动套件默认采用 TypeScript(.tsx)。有了类型定义,IDE 的补全会更强,bug 也更容易早发现。
// TSX 示例(TypeScript)
type Props = {
name: string
}
function Greeting({ name }: Props) {
return <h1>你好,{name}</h1>
}
Laravel 的 React 启动套件默认使用 TypeScript + TSX。本文示例也都用 TSX 编写。
在 Laravel 中的定位
React 与 Laravel 的关系比 Vue 稍晚一些,不过现在已经和 Vue 平起平坐,甚至更受青睐。
Laravel 6(2019 年) 把认证脚手架剥离到 laravel/ui 包,同时提供了 Vue 和 React 版本。但当时 Vue 是主流,React 版存在感较弱。
Laravel Breeze(2021 年) 加入 Inertia + React 栈后开始正式流行,Laravel 12(2025 年) 启动套件焕新时,React 已经和 Vue 并列(甚至排在前面)。
当前主流:Inertia × React
当前 Laravel 中使用 React 的核心方式就是 Inertia × React。Inertia 让你不必设计 API,从 Laravel 控制器直接把数据传给 React 组件,实现“现代单体”架构。
通过启动套件(推荐)
新项目最省事的方法就是用启动套件。
在交互式提示中选择 React,以下内容会全部自动配好。
inertiajs/inertia-laravel(服务端适配器)
@inertiajs/react(客户端适配器)
react + react-dom(React 19 本体)
@vitejs/plugin-react(Vite 插件)
- TypeScript +
@types/react
- Tailwind CSS + shadcn/ui 组件库
HandleInertiaRequests 中间件
- 登录 / 注册等认证页面(已用 Inertia + React + TypeScript 实现)
手动安装
在已有项目中加入时,需要分别安装服务端和客户端。
# 服务端(PHP)
composer require inertiajs/inertia-laravel
# 客户端(JavaScript)
npm install @inertiajs/react react react-dom
npm install --save-dev @vitejs/plugin-react @types/react @types/react-dom typescript
然后在 vite.config.ts 里加上 React 插件。
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.tsx'],
refresh: true,
}),
react(),
],
})
在 resources/js/app.tsx 里启动 Inertia 应用。
import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
createInertiaApp({
resolve: (name) =>
resolvePageComponent(
`./pages/${name}.tsx`,
import.meta.glob('./pages/**/*.tsx'),
),
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />)
},
})
目录结构
启动套件会把 React 页面组件放在 resources/js/pages/ 目录下。
resources/js/
├── app.tsx # Inertia 应用入口
├── bootstrap.ts
├── components/ # 可复用 UI 组件
│ ├── ui/ # shadcn/ui 组件
│ └── ...
├── hooks/ # 自定义 React hook
├── layouts/ # 布局组件
│ ├── app-layout.tsx
│ └── auth-layout.tsx
├── lib/ # 工具函数 / 配置
├── pages/ # Inertia 页面组件(对应控制器名)
│ ├── auth/
│ │ ├── login.tsx
│ │ └── register.tsx
│ ├── dashboard.tsx
│ └── posts/
│ ├── index.tsx
│ ├── create.tsx
│ └── show.tsx
└── types/ # TypeScript 类型定义
写 Inertia::render('posts/index', [...]) 时,对应的组件就是 resources/js/pages/posts/index.tsx。
JSX 基础语法
React 使用 JSX。在 JavaScript 中直接写类 HTML 的语法。下面介绍阅读启动套件代码时最基本要掌握的几种模式。
{} —— 变量展开
在 JSX 中用 {} 嵌入 JavaScript 的值或表达式。
const name = '世界'
const count = 3
return (
<div>
<p>你好,{name}!</p>
<p>2 倍是 {count * 2}</p>
</div>
)
条件分支 —— && 和三元运算符
React 没有对应 v-if 的指令。简单条件用 &&,if/else 用三元运算符 ? :。
{/* 条件为 true 时才显示 */}
{isLoggedIn && <p>欢迎!</p>}
{/* if / else */}
{isLoggedIn ? <p>欢迎!</p> : <a href="/login">登录</a>}
{/* if / else if / else */}
{isLoggedIn
? <p>欢迎!</p>
: role === 'admin'
? <p>正在以管理员身份登录</p>
: <a href="/login">登录</a>
}
列表渲染 —— .map()
用 Array.map() 渲染列表。为了让差异更新高效,一定要指定 key prop。
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
相当于 Vue 的 v-for :key,或 Svelte 的 {#each}。
className —— CSS 类名
JSX 会被编译成 JavaScript,class 是保留字。CSS 类要用 className。
{/* HTML: <div class="container"> */}
<div className="container">...</div>
事件处理 —— 驼峰命名
JSX 的事件属性用驼峰命名,传入函数引用。
<button onClick={handleClick}>点击</button>
{/* 取消表单默认提交行为 */}
<form onSubmit={(e) => { e.preventDefault(); handleSubmit() }}>
...
</form>
页面组件基础
Inertia 的页面组件就是普通的 React 组件。从 Laravel 控制器传来的数据会作为 props 接收。
控制器
// app/Http/Controllers/PostController.php
use Inertia\Inertia;
use App\Models\Post;
class PostController extends Controller
{
public function index()
{
return Inertia::render('posts/index', [
'posts' => Post::latest()->paginate(10),
]);
}
}
React 页面组件
// resources/js/pages/posts/index.tsx
import { Link } from '@inertiajs/react'
type Post = {
id: number
title: string
created_at: string
}
type Props = {
posts: {
data: Post[]
// paginate(10) 会返回包含分页信息的对象
// 也能用 current_page, last_page, per_page, total 等
}
}
export default function PostsIndex({ posts }: Props) {
return (
<div>
<h1>文章列表</h1>
{posts.data.map((post) => (
<article key={post.id}>
<h2>
<Link href={`/posts/${post.id}`}>{post.title}</Link>
</h2>
<p>{post.created_at}</p>
</article>
))}
</div>
)
}
只要作为组件参数接收 props,就可以直接使用控制器传来的数据。无需定义 REST API。
Link 组件
用 @inertiajs/react 提供的 <Link> 组件,页面跳转会通过 XHR 完成,避免整页重载。
import { Link } from '@inertiajs/react'
export default function PostsIndex() {
return (
<div>
{/* 基础链接 */}
<Link href="/posts">文章列表</Link>
{/* 用 POST 方法的链接(比如删除) */}
<Link href="/posts/1" method="delete" as="button">
删除
</Link>
{/* 预加载(hover 时预取) */}
<Link href="/posts/1" preload>
查看文章
</Link>
</div>
)
}
写法和普通 <a> 一样,但幕后 Inertia 只替换页面组件,体验接近 SPA。
@inertiajs/react 提供的 <Form> 组件是启动套件认证页面使用的表单提交推荐样式。通过 props 指定 action 和 method,通过 children 函数(render prop)接收 errors 和 processing。
基本用法
import { Form } from '@inertiajs/react'
export default function PostCreate() {
return (
<Form action="/posts" method="post" className="flex flex-col gap-4">
{({ errors, processing }) => (
<>
<div>
<label htmlFor="title">标题</label>
<input id="title" name="title" type="text" required />
{errors.title && <p className="error">{errors.title}</p>}
</div>
<div>
<label htmlFor="content">正文</label>
<textarea id="content" name="content" />
{errors.content && <p className="error">{errors.content}</p>}
</div>
<button type="submit" disabled={processing}>
{processing ? '提交中...' : '发布'}
</button>
</>
)}
</Form>
)
}
<Form> 的 children 是 ({ errors, processing }) => JSX 的函数(render prop 模式)。Form 组件会自动算出这些值并传入。表单字段用 HTML 原生的 name 属性,而非 onChange,浏览器标准的表单数据采集也照常工作。
启动套件的模式
启动套件用 Wayfinder 把路由当作对象来管理。store.form() 会返回包含路由对象的 action 和 method 的对象,直接 spread 到 <Form> 上。
import { Form } from '@inertiajs/react'
import { store } from '@/routes/login'
export default function Login() {
return (
<Form
{...store.form()}
resetOnSuccess={['password']}
className="flex flex-col gap-6"
>
{({ errors, processing }) => (
<>
{/* 表单内容 */}
</>
)}
</Form>
)
}
resetOnSuccess 中列出的字段会在提交成功后自动重置。密码等提交完希望清空的字段可以放在这里。
不使用 Wayfinder 时,直接传 action="/login" 也可以。
表单处理使用 @inertiajs/react 的 useForm hook,可以简洁地实现表单状态管理、提交、校验错误显示。
控制器侧
// app/Http/Controllers/PostController.php
class PostController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
]);
Post::create($validated + ['user_id' => auth()->id()]);
return redirect()->route('posts.index')
->with('success', '文章已创建。');
}
}
React 表单组件
// resources/js/pages/posts/create.tsx
import { useForm } from '@inertiajs/react'
import { FormEventHandler } from 'react'
export default function PostCreate() {
const { data, setData, post, processing, errors } = useForm({
title: '',
content: '',
})
const submit: FormEventHandler = (e) => {
e.preventDefault()
post('/posts')
}
return (
<form onSubmit={submit}>
<div>
<label>标题</label>
<input
type="text"
value={data.title}
onChange={(e) => setData('title', e.target.value)}
/>
{errors.title && <p className="error">{errors.title}</p>}
</div>
<div>
<label>正文</label>
<textarea
value={data.content}
onChange={(e) => setData('content', e.target.value)}
/>
{errors.content && <p className="error">{errors.content}</p>}
</div>
<button type="submit" disabled={processing}>
{processing ? '提交中...' : '发布'}
</button>
</form>
)
}
useForm 返回对象的主要属性:
| 属性 / 方法 | 说明 |
|---|
data | 表单数据对象 |
setData(field, value) | 更新字段值 |
errors | 校验错误(按字段名访问) |
processing | 提交中为 true(用来禁用按钮) |
isDirty | 相对初始值发生变化时为 true |
post(url) | 以 POST 提交 |
put(url) | 以 PUT 提交(更新) |
delete(url) | 以 DELETE 提交 |
reset() | 表单重置为初始值 |
校验返回错误时,useForm 会保留已输入内容并展示错误。
共享数据(Shared Data)
所有页面都需要的公共数据(登录中的用户信息、闪存消息等)可以在 HandleInertiaRequests 中间件的 share() 方法中定义。
// app/Http/Middleware/HandleInertiaRequests.php
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user()
? $request->user()->only('id', 'name', 'email')
: null,
],
'flash' => [
'success' => $request->session()->get('success'),
'error' => $request->session()->get('error'),
],
]);
}
}
React 组件里通过 usePage() hook 访问共享数据。
import { usePage } from '@inertiajs/react'
type SharedProps = {
auth: {
user: { id: number; name: string; email: string } | null
}
flash: {
success: string | null
error: string | null
}
}
export default function AppHeader() {
const { auth, flash } = usePage<SharedProps>().props
return (
<>
<header>
{auth.user ? (
<span>{auth.user.name}</span>
) : (
<span>游客</span>
)}
</header>
{flash.success && (
<div className="alert-success">{flash.success}</div>
)}
</>
)
}
共享数据会包含在每一次请求中,建议只保留必要的最少数据。使用 fn() 惰性求值时,只有真正访问时才会执行求值。
React hook 基础
在 Inertia × React 开发中你必须了解的几个 React 基础 hook。
useState —— 局部状态管理
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setIsOpen(!isOpen)}>切换</button>
</div>
)
}
useEffect —— 处理副作用
import { useState, useEffect } from 'react'
export default function Timer() {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setSeconds((s) => s + 1)
}, 1000)
// 清理函数
return () => clearInterval(timer)
}, []) // 空数组 = 挂载时执行一次
return <p>已过 {seconds} 秒</p>
}
useMemo 和 useCallback —— 性能优化
import { useMemo, useCallback } from 'react'
import { router } from '@inertiajs/react'
type Post = { id: number; title: string; published: boolean }
type Props = { posts: Post[] }
export default function PostsList({ posts }: Props) {
// 缓存值(只要 posts 不变就不重新计算)
const publishedPosts = useMemo(
() => posts.filter((post) => post.published),
[posts],
)
// 缓存函数(依赖不变就不会重建)
const handleClick = useCallback((id: number) => {
router.visit(`/posts/${id}`)
}, [])
return (
<ul>
{publishedPosts.map((post) => (
<li key={post.id} onClick={() => handleClick(post.id)}>
{post.title}
</li>
))}
</ul>
)
}
TypeScript 支持
启动套件的 React 版默认用 TypeScript。结合 Inertia 的类型定义可以保证 props 的类型安全。
全局类型定义
启动套件里在 resources/js/types/index.d.ts 中定义共享数据类型。
// resources/js/types/index.d.ts
export interface User {
id: number
name: string
email: string
email_verified_at?: string
}
export type PageProps<T extends Record<string, unknown> = Record<string, unknown>> = T & {
auth: {
user: User
}
}
在页面组件中使用类型
import { PageProps } from '@/types'
type Post = {
id: number
title: string
content: string
}
export default function PostsIndex({ auth, posts }: PageProps<{ posts: Post[] }>) {
return (
<div>
<p>登录中:{auth.user.name}</p>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
)
}
React 与 Laravel 组合起来,特别是通过 Inertia 采用“现代单体”结构时会展现出强大能力。它与 TypeScript 契合度也很好,适合大规模应用开发。
| 要素 | 作用 |
|---|
| Laravel 控制器 | 路由、数据获取、校验 |
Inertia::render() | 从控制器把数据传给 React 组件 |
| React 页面组件 | 接收 props 渲染 UI |
useForm | 表单状态管理、提交、错误展示 |
Link 组件 | 无需整页刷新的页面跳转 |
usePage().props | 访问共享数据 |
| TypeScript | 保证 props 类型安全,增强 IDE 补全 |
用 Inertia × React,你能得到 Laravel 后端的简洁与 React 强大生态兼备的开发体验。用启动套件创建项目,连认证页面在内都是现成的,可以马上开始开发。
Inertia.js 官方文档
Inertia v3 的完整功能请参考官方文档。