深色模式
React 19 正式发布了。来整理一下从 18 迁移到 19 的实际注意事项,以及团队踩过的坑。
主要变化回顾
useActionState(之前叫 useFormState,API 改了)
useFormStatus
useOptimistic
use() hook
ref 作为 prop(forwardRef 弃用)
Context 直接用 <Context>(不需要 <Context.Provider>)迁移:forwardRef
tsx
// React 18:需要 forwardRef
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, ...props }, ref) => (
<input
ref={ref}
className={cn("border rounded px-3 py-2", className)}
{...props}
/>
),
);
Input.displayName = "Input";
// React 19:直接传 ref prop
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
ref?: React.Ref<HTMLInputElement>;
}
function Input({ ref, className, ...props }: InputProps) {
return (
<input
ref={ref}
className={cn("border rounded px-3 py-2", className)}
{...props}
/>
);
}但注意:如果你的项目有很多用到 forwardRef 的组件库(比如 Radix UI、shadcn/ui),这些库会逐步更新,不需要你手动改。
迁移:Context
tsx
// React 18
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
// React 19
<ThemeContext value={theme}>
{children}
</ThemeContext>旧写法依然有效,只是新写法更简洁。
迁移:useFormState → useActionState
tsx
// React 18(react-dom)
import { useFormState } from "react-dom";
const [state, action] = useFormState(myAction, initialState);
// React 19(react)
import { useActionState } from "react";
const [state, action, isPending] = useActionState(myAction, initialState);多了第三个返回值 isPending,不需要再单独管理 loading 状态。
服务端组件 + Server Actions 的生产实践
tsx
// app/products/page.tsx(服务端组件,直接访问数据库)
import { db } from "@/lib/db";
export default async function ProductsPage() {
// 不需要 API 路由,直接查数据库
const products = await db.product.findMany({
where: { published: true },
orderBy: { createdAt: "desc" },
});
return <ProductList products={products} />;
}tsx
// actions/product.ts(Server Actions)
"use server";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
import { auth } from "@/lib/auth";
export async function deleteProduct(productId: string) {
const session = await auth();
if (!session?.user?.isAdmin) {
throw new Error("Unauthorized");
}
await db.product.delete({ where: { id: productId } });
revalidatePath("/products");
}tsx
// 客户端组件使用 Server Action
"use client";
import { deleteProduct } from "@/actions/product";
import { useActionState } from "react";
function DeleteButton({ productId }: { productId: string }) {
const [state, action, isPending] = useActionState(async () => {
await deleteProduct(productId);
return { success: true };
}, null);
return (
<form action={action}>
<button disabled={isPending}>{isPending ? "删除中..." : "删除"}</button>
</form>
);
}我们团队遇到的坑
1. Suspense 边界设置
Server Components 配合 Suspense 时,边界位置很重要:
tsx
// 坏的:整个页面等待一个慢查询
export default async function Page() {
const [slowData, fastData] = await Promise.all([
getSlowData(),
getFastData(),
]);
// ...
}
// 好的:慢的数据用 Suspense 包裹
export default function Page() {
return (
<div>
<FastSection /> {/* 先渲染 */}
<Suspense fallback={<Skeleton />}>
<SlowSection /> {/* 异步渲染 */}
</Suspense>
</div>
);
}2. Server Actions 的错误处理
tsx
"use server";
export async function submitForm(formData: FormData) {
try {
// ...
return { success: true };
} catch (e) {
// 不要直接 throw Error,客户端会收到 500 页面
// 要 return error 信息
return { success: false, error: "操作失败,请重试" };
}
}小结
- React 19 迁移成本不高,大部分是向后兼容的
forwardRef弃用,但还能用,慢慢迁移就好useActionState替代useFormState,多了 isPending- Server Components + Server Actions 是生产级的,值得在新项目里全面采用
- Suspense 边界设计是关键,要仔细规划