React 19 是自 Hooks 引入以来最重要的更新。它没有带来全新的概念——而是带来了一个我们用各种不同方式解决了上千次的问题的最终解决方案:表单处理和变更管理。
目录
React 19 解决的问题
在 React 19 之前,一个带有加载反馈、错误处理和乐观更新的表单需要这样:
// 之前: 一个"基本"功能需要 35+ 行
function ProfileForm() {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setIsPending(true);
setError(null);
try {
const data = new FormData(e.currentTarget);
await updateProfile(data);
setSuccess(true);
} catch (err) {
setError("Error al guardar");
} finally {
setIsPending(false);
}
}
// ...
}before.tsx
useActionState:无需手动 useState 的表单
import { useActionState } from "react";
async function updateProfileAction(prevState: State, formData: FormData) {
try {
await updateProfile({
name: formData.get("name") as string,
bio: formData.get("bio") as string,
});
return { success: true, error: null };
} catch {
return { success: false, error: "保存个人资料时出错" };
}
}
function ProfileForm() {
const [state, action, isPending] = useActionState(
updateProfileAction,
{ success: false, error: null }
);
return (
<form action={action}>
<input name="name" placeholder="姓名" />
<textarea name="bio" placeholder="简介" />
{state.error && <p className="error">{state.error}</p>}
{state.success && <p className="success">已保存!</p>}
<button type="submit" disabled={isPending}>
{isPending ? "保存中..." : "保存"}
</button>
</form>
);
}profile-form.tsx
useOptimistic:自动回滚的即时 UI
乐观更新模式(在服务器确认之前更新 UI)曾经很繁琐。现在:
import { useOptimistic, useActionState } from "react";
function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
initialTodos,
(state, newTodo: Todo) => [...state, newTodo]
);
async function addTodoAction(_: State, formData: FormData) {
const title = formData.get("title") as string;
// UI 立即更新
addOptimisticTodo({ id: crypto.randomUUID(), title, done: false });
// 真正的变更(如果失败 hook 会回滚)
await createTodo(title);
return { error: null };
}
const [state, action, isPending] = useActionState(addTodoAction, {
error: null,
});
return (
<>
<ul>
{optimisticTodos.map(todo => (
<li
key={todo.id}
style={{ opacity: todo.id.startsWith("temp") ? 0.5 : 1 }}
>
{todo.title}
</li>
))}
</ul>
<form action={action}>
<input name="title" required />
<button disabled={isPending}>添加</button>
</form>
</>
);
}todo-list.tsx
use():有条件地消费 Promises 和上下文
import { use, Suspense } from "react";
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // — 可在条件语句中使用
return <h1>{user.name}</h1>;
}
// Suspense boundary 缓存并解析 promise
function App() {
const userPromise = fetchUser("123"); // 在组件外创建
return (
<Suspense fallback={<p>加载用户中…</p>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}user-profile.tsx
Server Actions 实践
React 19 将 Server Actions(用 "use server" 标记的、在服务器上执行的函数)正式化:
"use server";
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";
export async function deletePost(id: string) {
await db.post.delete({ where: { id } });
revalidatePath("/posts"); // 使服务器缓存失效
}actions.ts
import { deletePost } from "./actions";
export function PostCard({ post }: { post: Post }) {
return (
<article>
<h2>{post.title}</h2>
<form action={deletePost.bind(null, post.id)}>
<button type="submit">删除</button>
</form>
</article>
);
}post-card.tsx
新 API 摘要
| API | 替代 | 使用时机 |
|---|---|---|
useActionState | 用于表单的 useState + useReducer | 所有带 UI 反馈的变更 |
useOptimistic | 手动回滚逻辑 | 提升感知性能的更新 |
use(promise) | 用于数据获取的 useEffect + useState | 在渲染中读取 promises 的组件 |
use(context) | useContext | 需要有条件地读取时 |
ref 作为 prop | forwardRef | 始终——消除不必要的包装器 |