> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# React 入门 —— 与 Inertia × Laravel 搭配使用的基础

> 在 Laravel + Inertia.js 中使用 React 的入门指南。从 laravel/ui 到当前启动套件的历史沿革,再到 useForm、usePage 等 Inertia React hook 的实战讲解。

## 什么是 React

React 是由 Meta(前 Facebook)开发和维护的用户界面构建 JavaScript 库。它以声明式的 UI 写法和**基于组件**的架构著称,从小组件到完整 SPA 都有广泛应用。

React 的核心是通过**虚拟 DOM** 实现高效的重渲染。状态(state)发生变化时,React 只把差异反映到 DOM 上,不需要手动操作 DOM。

<Info>
  本文讲解的是 React 19 与 Inertia v3 的组合。Laravel 13 的启动套件默认使用这一组合。
</Info>

### JSX 和 TSX

React 组件用 **JSX**(JavaScript XML)语法编写。可以在 JavaScript 中直接写类似 HTML 的语法。

```jsx theme={null}
// JSX 示例
function Greeting({ name }) {
    return <h1>你好,{name}</h1>
}
```

启动套件默认采用 **TypeScript**(`.tsx`)。有了类型定义,IDE 的补全会更强,bug 也更容易早发现。

```tsx theme={null}
// TSX 示例(TypeScript)
type Props = {
    name: string
}

function Greeting({ name }: Props) {
    return <h1>你好,{name}</h1>
}
```

<Tip>
  Laravel 的 React 启动套件默认使用 TypeScript + TSX。本文示例也都用 TSX 编写。
</Tip>

***

## 在 Laravel 中的定位

### 历史

React 与 Laravel 的关系比 Vue 稍晚一些,不过现在已经和 Vue 平起平坐,甚至更受青睐。

```mermaid theme={null}
timeline
    title Laravel 与 React 的历程
    2019 : Laravel 6 —— 在 laravel/ui 中加入 React 脚手架
    2021 : Laravel 8 —— Breeze 加入 Inertia React 栈
    2022 : Laravel 9 —— 迁移到 Vite
    2025 : Laravel 12 —— 启动套件焕新(React / Vue)
    2026 : Laravel 13 —— 支持 Inertia v3 的启动套件
```

**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 组件,实现“现代单体”架构。

```mermaid theme={null}
graph LR
    Browser["浏览器"]
    Inertia["Inertia.js<br>(适配器层)"]
    Laravel["Laravel<br>(控制器)"]
    React["React<br>(页面组件)"]

    Browser <-->|XHR / 整页加载| Inertia
    Inertia <-->|Inertia 响应| Laravel
    Inertia -->|props| React
    React -->|渲染| Browser
```

***

## 搭建

### 通过启动套件(推荐)

新项目最省事的方法就是用启动套件。

```shell theme={null}
laravel new my-app
```

在交互式提示中选择 **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 实现)

### 手动安装

在已有项目中加入时,需要分别安装服务端和客户端。

```shell theme={null}
# 服务端(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 插件。

```ts theme={null}
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 应用。

```tsx theme={null}
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} />)
    },
})
```

<Info>
  手动安装的更详细步骤(根模板设置、中间件注册等)请参考 [Inertia 官方文档](https://inertiajs.com/installation)。
</Info>

***

## 目录结构

启动套件会把 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 的值或表达式。

```tsx theme={null}
const name = '世界'
const count = 3

return (
    <div>
        <p>你好,{name}!</p>
        <p>2 倍是 {count * 2}</p>
    </div>
)
```

#### 条件分支 —— `&&` 和三元运算符

React 没有对应 `v-if` 的指令。简单条件用 `&&`,if/else 用三元运算符 `? :`。

```tsx theme={null}
{/* 条件为 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。

```tsx theme={null}
<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`。

```tsx theme={null}
{/* HTML: <div class="container"> */}
<div className="container">...</div>
```

#### 事件处理 —— 驼峰命名

JSX 的事件属性用驼峰命名,传入函数引用。

```tsx theme={null}
<button onClick={handleClick}>点击</button>

{/* 取消表单默认提交行为 */}
<form onSubmit={(e) => { e.preventDefault(); handleSubmit() }}>
    ...
</form>
```

***

## 页面组件基础

Inertia 的页面组件就是普通的 React 组件。从 Laravel 控制器传来的数据会作为 props 接收。

### 控制器

```php theme={null}
// 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 页面组件

```tsx theme={null}
// 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 完成,避免整页重载。

```tsx theme={null}
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。

***

## `Form` 组件

`@inertiajs/react` 提供的 `<Form>` 组件是启动套件认证页面使用的表单提交推荐样式。通过 props 指定 `action` 和 `method`,通过 children 函数(render prop)接收 `errors` 和 `processing`。

### 基本用法

```tsx theme={null}
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](/zh/blog/wayfinder-introduction) 把路由当作对象来管理。`store.form()` 会返回包含路由对象的 `action` 和 `method` 的对象,直接 spread 到 `<Form>` 上。

```tsx theme={null}
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` 中列出的字段会在提交成功后自动重置。密码等提交完希望清空的字段可以放在这里。

<Info>
  不使用 Wayfinder 时,直接传 `action="/login"` 也可以。
</Info>

***

## `useForm` hook

表单处理使用 `@inertiajs/react` 的 `useForm` hook,可以简洁地实现表单状态管理、提交、校验错误显示。

### 控制器侧

```php theme={null}
// 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 表单组件

```tsx theme={null}
// 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()` 方法中定义。

```php theme={null}
// 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 访问共享数据。

```tsx theme={null}
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>
            )}
        </>
    )
}
```

<Info>
  共享数据会包含在每一次请求中,建议只保留必要的最少数据。使用 `fn()` 惰性求值时,只有真正访问时才会执行求值。
</Info>

***

## React hook 基础

在 Inertia × React 开发中你必须了解的几个 React 基础 hook。

### `useState` —— 局部状态管理

```tsx theme={null}
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` —— 处理副作用

```tsx theme={null}
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` —— 性能优化

```tsx theme={null}
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` 中定义共享数据类型。

```ts theme={null}
// 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
    }
}
```

### 在页面组件中使用类型

```tsx theme={null}
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 强大生态兼备的开发体验。用启动套件创建项目,连认证页面在内都是现成的,可以马上开始开发。

<Card title="Inertia.js 官方文档" icon="book-open" href="https://inertiajs.com">
  Inertia v3 的完整功能请参考官方文档。
</Card>


## Related topics

- [Svelte 入门 —— 与 Inertia × Laravel 搭配使用的基础](/zh/blog/svelte-introduction.md)
- [Vue.js 入门 —— 与 Inertia × Laravel 搭配使用的基础](/zh/blog/vue-introduction.md)
- [前端](/zh/frontend.md)
- [开始学习 Laravel 前需要具备的知识](/zh/true-tutorial.md)
- [认证入门](/zh/authentication.md)
