本文将介绍react 19的一些新特性和API,主打一个结合例子和踩坑记录的方式来分享。
一、use: 消费Context和Promise
1. Context
关于Context react19 带来了两点变化:
- 可以使用
use在条件中消费 Context - 可以不再使用
<Context.Provider>,而是直接使用<Context>
tsx
import { use } from 'react';
const ThemeContext = createContext('');
function App() {
// ✅ 直接 ThemeContext 作为组件
return (
<ThemeContext value="dark">
<Button showTheme={true}/>
</ThemeContext>
);
}
function Button({ showTheme }) {
if (showTheme) {
// ✅ 可以在条件语句中使用 use
const theme = use(ThemeContext);
return <button className={theme}>Styled Button</button>;
}
return <button>Default Button</button>;
}
2. 消费Promise
消费promise,最常见的场景就是处理 GET类请求。
下面以表格分页查询这个场景为例子,进行探索。
- 进入页面,需要加载页面数据
- 当修改分页(点击下一页),则请求新的数据
1.切换路由-请求数据
react 19不支持在渲染中直接创建promise 然后用use 进行消费,所以创建promise的工作交给了react-router,让它在初始化页面组件时传入一个promise。
Page.tsx
tsx
import { useLoaderData, type ClientLoaderFunctionArgs } from "react-router";
export async function clientLoader() {
const p = getVideos()
return {
videoListPromise: p
}
}
export function Page() {
const { videoListPromise } = useLoaderData<typeof clientLoader>()
return <Suspense fallback={<div>Loading...</div>}>
<VideoList {videoListPromise}/>
</Suspense>
}
export async VideoList() {
const { data: response } = use(videoListPromise)
const videos = response?.data?.items || [];
return <div>
{videos.map((video) => (
<VideoCard
key={video.id}
video={video}
/>
))}
<div>
}
P.S. 这里使用的是react-router v7 的 framework mode,
clientLoader表示页面异步请求,react-router等clientLoader这个promise完成后 挂载组件,此时可以通过useLoaderData获取数据。
这个例子中,clientLoader并没有真的发起异步请求,而是返回一个pending的promise,这个是关键,就是为了让use(promise).
当渲染中,遇到use(videoListPromise) , 会throw 一个 promise,<Suspense>组件能捕获到从而渲染fallback。当这个promise 成功(resolved)就继续执行下面的渲染代码。
注意点:
use()需要在<Suspense>内使用,否则就无法捕获到promise,就不能加载fallback.use()消费的promise如果失败(rejected),会触发<ErrorBoundary>(如果有的话),不会继续渲染下面的组件。在这种范式下,trycatch就没必要了,只需要定义好<ErrorBoundary>。- 不建议在渲染中创建promise给
use使用,因为这样每次重新渲染就会重新创建一个新promise,重复请求。最佳实践是这个promise要从组件外部传入。
2.交互-请求数据
前面展示了一个"纯粹的"切换到某个页面,页面加载初始数据。但有时候需要在交互后,继续触发数据请求,比如"分页"。
此时我们会发出一个疑问 ------ promise在组件外面创建的,我的参数怎么传递呢? 答:将参数"URL 化".
先看效果:

要在 React 19 中结合 React Router 使用 use + Suspense 并利用 URL 实现分页,核心思路是:将 URL 中的查询参数(如 ?page=2)作为触发 clientLoader 重新运行的依赖项。
当 URL 改变时,React Router 会重新调用 clientLoader,产生一个新的 Promise,VideoList 组件通过 use(promise) 挂起并触发 Suspense 状态。
P.S这是
clientLoader(以及服务端的loader)设计的核心优势之一:数据加载与路由变化天然绑定 ,无需手动监听useParams()或useLocation()。 如果你不希望 参数变化时重新加载(极少见),可以通过缓存或自定义逻辑在clientLoader内部跳过请求,但通常不需要。
以下是完整的改进方案:
a. 改进 clientLoader
我们需要从 request.url 中提取分页参数,并将其传递给 API 函数。
tsx
import { useLoaderData, type ClientLoaderFunctionArgs, useSearchParams } from "react-router";
import { use, Suspense } from "react";
// 模拟 API
declare function getVideos(params: { page: number }): Promise<any>;
export async function clientLoader({ request }: ClientLoaderFunctionArgs) {
const url = new URL(request.url);
const page = Number(url.searchParams.get("page")) || 1; // 默认第一页
// 返回一个新的 Promise
const p = getVideos({ page });
return {
videoListPromise: p,
currentPage: page
};
}
b. 改进 Page 和 VideoList 组件
tsx
export function Page() {
// 1. 获取 loader 传递的 Promise 和当前页码
const { videoListPromise, currentPage } = useLoaderData<typeof clientLoader>();
const [searchParams, setSearchParams] = useSearchParams();
// 处理翻页逻辑:直接更新 URL 查询参数
const handlePageChange = (newPage: number) => {
setSearchParams((prev) => {
prev.set("page", String(newPage));
return prev;
});
};
return (
<div className="container">
<h1>Video Library - Page {currentPage}</h1>
{/* 2. Suspense 包裹异步渲染区域 */}
<Suspense fallback={<div className="loading">Loading videos...</div>}>
<VideoList videoListPromise={videoListPromise} />
</Suspense>
{/* 3. 分页控制器 */}
<div className="pagination-controls">
<button
disabled={currentPage <= 1}
onClick={() => handlePageChange(currentPage - 1)}
>
Previous
</button>
<span> Page {currentPage} </span>
<button
onClick={() => handlePageChange(currentPage + 1)}
>
Next
</button>
</div>
</div>
);
}
// 4. VideoList 使用 React 19 的 use() 钩子
interface VideoListProps {
videoListPromise: Promise<any>;
}
export function VideoList({ videoListPromise }: VideoListProps) {
// use() 会自动解构 Promise,并在 Pending 时挂起 Suspense
const response = use(videoListPromise);
const videos = response?.data?.items || [];
return (
<div>
{videos.map((video: any) => (
<VideoCard key={video.id} video={video} />
))}
</div>
);
}
c. 重新加载数据的坑
第一次进入页面,触发了<Suspense>的fallback。 切换分页,重新请求数据时,useLoaderData会返回一个新的Promise {<pending>} 但是却没有触发<Suspense>的fallback。。。 有点反直觉
解决办法就是,给<Suspense>加key(不加的话,react重新渲染可能直接跳过Suspense的协调,跳过渲染) <Suspense key={currentPage} fallback={<Loading />} >
3.小总结
有些场景不太适合 clientLoader + use + <Suspense> 这种实践方式,比如满足「参数控制刷新」+ 「局部刷新」的场景。
- 这种场景,往往是有多个请求,每个请求都需要传递参数(URL只有一个就不太好表达)。每个局部刷新对应的UI 都需要 fallback。(fallback会暂时覆盖原来的UI,不好)
- 举个例子:「列表加载更多」的场景,往上滚动时需要加载更多数据,总不能加载时用fallback来访覆盖UI吧。还是得退化到
useEffect,useEffect处理副作用终归还是必不可少的。
而 use(promise) 更适合"声明式、一次性解析",加载页面级数据。
🆚 对比总结
| 维度 | use(promise) + Suspense |
useEffect / React Query |
|---|---|---|
| 适用场景 | 页面级数据(如文章详情、商品页) | 列表、表格、仪表盘等交互密集型 UI |
| Loading 体验 | 全局 fallback(看不到数据) | 局部 loading(还可以看到历史数据)✅ |
| 状态控制 | 仅 pending/success/error | 支持 isFetching, isRefetching, error, cache 等 ✅ |
| 用户交互响应 | 弱(被动挂起) | 强(主动触发 + 精确反馈)✅ |
| 开发心智模型 | 声明式(数据即依赖) | 命令式(事件 → 请求 → 更新)✅(对管理端更直观) |
| 生态支持 | 需手动管理 | React Query/SWR 提供开箱即用分页支持 ✅ |
二、Actions:处理表单和POST类请求
1. 什么是Actions?
1.在 React 19 中,Actions 本质上是支持异步函数的 Transition。
2.在 React 19 之前,处理异步操作通常需要手动管理 pending(加载中)、error(错误)和 data(数据)状态。Actions 的出现让这些流程自动化了。
3.当你将一个异步函数传递给 useTransition 或 useActionState 时,React 会自动处理:
Pending 状态:自动开始并结束,无需手动 setIsLoading(true/false)。自动排队:React 保证多个提交按顺序处理。UI 响应性:UI 在数据请求期间保持响应,不会阻塞。
2. react 19 的表单提交
1. 原生的表单
html
<form action="/submit-data" method="POST">
通过onsubmit监听 submit事件,函数return true,走原生表单提交逻辑(即发送到action的url)。return false,则阻止提交
html
<form onsubmit="return validateForm()">
<!-- 表单字段 -->
<input type="submit" value="提交">
</form>
<script>
function validateForm() {
// 验证逻辑...
return true; // 或 false
}
</script>
更多情况下,利用 onsubmit来手动发送http请求,提交POST请求。然后自己e.preventDefault();(阻止默认的表单提交行为)
2. 使用useActionState处理表单
React 19 对表单处理引入了较大的改进,目标是简化数据变更、状态管理和提升用户体验。
这些变化的核心是引入了「表单对action的支持」和「3个新的 Hooks」,它们旨在让您能够更多地利用 Web 标准的 <form> 元素,减少手动处理状态和 event.preventDefault() 的样板代码。
- form元素支持
action传入一个异步函数(asyncFunction) - 提供
useActionState创建asyncFunction和管理异步状态 - 提供
useFormStatus方便表单子组件 获取表单的状态 - 提供
useOptimistic乐观更新
下面以登录表单为场景来介绍 react19 表单提交的变化。
先介绍下react19之前的版本,最直接的登录表单是如何做的。
jsx
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Form, useNavigate } from "react-router"
import { loginApiAuthLoginPost } from "@/APIs"
import { useAuthStore } from "@/store/useAuthStore"
export default function Login() {
const navigate = useNavigate();
const setAuth = useAuthStore((state) => state.setAuth);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
const { data, error: apiError } = await loginApiAuthLoginPost({
body: { username, password }
});
if (apiError) {
setError("Login failed. Please check your credentials.");
return;
}
if (data && data.code === 200 && data.data) {
setAuth(data.data.token, data.data.user);
navigate("/dashboard");
} else {
setError(data?.err_msg || "Unknown error occurred");
}
} catch (err) {
console.error(err);
setError("An unexpected error occurred. Please try again.");
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleLogin} className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
type="text"
placeholder="admin"
required
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && (
<div className="text-sm text-red-500 font-medium">
{error}
</div>
)}
<Button className="w-full" type="submit" disabled={loading}>
{loading ? "Signing in..." : "Sign in"}
</Button>
</form>
)
}

a. form的action属性
1.<form action> 支持函数: 现在,您可以直接将一个 异步函数(Action) 传递给 <form> 元素的 action 属性。
- 当表单提交时,React 会自动调用这个函数,并且在函数内不需要手动调用
event.preventDefault()。 - Actions 默认在 Transition 中运行,可以自动处理 Pending (等待中) 状态、错误和乐观更新。
- 自动重置表单。当
<form>Action 成功执行后,React 会自动为 非受控组件 重置表单。
2.web标准的FormData接口 , 原生表单的提交事件会传递一个FormData对象给函数,该对象包含了表单的键值对。
FormData.entries()返回一个包含所有键值对的iterator对象。FormData.get()返回在 FormData对象中与给定键关联的第一个值。
3.核心逻辑变成了 (formData: FormData) => Promise<any>的函数
jsx
const handleLogin = async (formData: FormData) => {
setError(null);
setLoading(true);
const payload = Object.fromEntries(formData.entries());
try {
const { data, error: apiError } = await loginApiAuthLoginPost({
body: { ...payload }
});
//...
} catch (err) {
console.error(err);
setError("An unexpected error occurred. Please try again.");
} finally {
setLoading(false);
}
4.将handleLogin传递给form的action, 记得给form表单控件加上name="xxx"属性 。
jsx
<form action={handleLogin} className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
name="username"
type="text"
placeholder="admin"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
name="password"
type="password"
required
/>
</div>
{error && (
<div className="text-sm text-red-500 font-medium">
{error}
</div>
)}
<Button className="w-full" type="submit" disabled={loading}>
{loading ? "Signing in..." : "Sign in"}
</Button>
</form>
b. useActionState
useActionState介绍
- 它将表单提交的异步操作、状态管理和加载指示结合在一起。
- 它返回最新的
状态、一个Action 函数,以及一个isPending状态,您可以使用它来处理表单的提交结果(如成功消息或错误)。 - 优点: 减少了对多个
useState的需求,简化了状态逻辑。
参数细节
-
useActionState接受一个函数,有两个参数:prevState代表之前状态formData代表action提交的数据(如果是form表单,则传入的是FormData)
-
useActionState可以指定两个泛型,分别对应prevState和formData的类型,比如useActionState<string | null, FormData>()
-
返回值:[state, submitAction, isPending]
常见问题 有点麻烦的是:FormData对ts很不友好,你无法得知FormData上有什么属性和value。
const payload = Object.fromEntries(formData.entries()) 中的payload ts类型如下:
ts
const payload: {
[k: string]: FormDataEntryValue;
}
这就要借助牛逼的zod和zod-form-data两个库了,顺便把表单的前端校验也做了~
- zod 大名鼎鼎的TS校验库
- zod-form-data 校验FormData并提供TS类型
登录表单案例 使用 Actions( useActionState) 改进后的代码如下:
tsx
import { useActionState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useNavigate } from "react-router"
import { loginApiAuthLoginPost } from "@/APIs"
import { useAuthStore } from "@/store/useAuthStore"
import { z } from "zod";
import { zfd } from "zod-form-data";
const schema = zfd.formData({
username: zfd.text(),
password: zfd.text(z.string().min(6))
});
export default function Login() {
const navigate = useNavigate();
const setAuth = useAuthStore((state) => state.setAuth);
const [error, handleLogin, loading] = useActionState<string | null, FormData>(async (_prevState, formData) => {
// const payload = Object.fromEntries(formData.entries()) //禁止这种ts不友好的转换
const payload = schema.parse(formData)
try {
const { data, error: apiError } = await loginApiAuthLoginPost({
body: payload,
});
if (apiError) {
return "Login failed. Please check your credentials."
} else if (data && data.code >= 300) {
return data?.err_msg || "Unknown error occurred"
}
setAuth(data.data.token, data.data.user);
navigate("/dashboard");
return null;
} catch (err) {
return "An unexpected error occurred. Please try again."
}
}, null);
return (
<form action={handleLogin} className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
name="username"
type="text"
placeholder="admin"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
name="password"
type="password"
required
/>
</div>
{error && (
<div className="text-sm text-red-500 font-medium">
{error}
</div>
)}
<Button className="w-full" type="submit" disabled={loading}>
{loading ? "Signing in..." : "Sign in"}
</Button>
</form>
)
}
c. useFormStatus
用于在子组件中访问父级表单的状态(特别是 pending 状态),解决"属性透传(Prop Drilling)"问题。
tsx
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "提交中..." : "提交"}</button>;
}
// 在父组件中
<form action={action}>
<SubmitButton />
</form>
d. useOptimistic
在异步请求完成之前,先假设操作成功并立即更新 UI。如果请求失败,React 会自动回滚到旧状态。
适用的场景:
- 社交互动,比如:点赞、收藏、关注
- 简单的列表操作,比如:删除、添加、排序
- 单字段的表单更新,比如:修改昵称、切换某个开关(Switch)
这些场景的共同点是:逻辑简单、操作频繁、用户对即时反馈要求高、且失败率通常较低。
3. 小总结
总的来说,对于一些简单的表单 场景, 可以尽量不依赖第三方组件,做到优雅快速开发。同时Actions这种范式也不仅仅只用于form,任何POST请求都可以用这种方式来处理,减少了模板代码。 类似一个简易的useAsync hook。