React 19 是自 hooks 以来最重要的版本。服务器组件最初是 React 18 中的一个实验性功能,现在已经稳定并与并发渲染模型完全集成。但这些变化远远超出了稳定 RSC 的范围——React 19 引入了 Actions、一个新的 use() 钩子、表单集成、乐观更新和文档元数据管理,它们共同改变了您对 React 应用程序中数据流的看法。
属于我们的性能与可扩展性系列
阅读完整指南本指南重点关注 React 18 和 19 之间实际发生的变化,解释每个变化背后的架构推理,并展示替代旧方法的生产模式。
要点
- 服务器组件仅在服务器上运行 - 它们没有生命周期,没有状态,也没有浏览器 API
use()钩子替换了客户端组件中的await以使用承诺和上下文- React 19 Actions 替换了表单提交的手动
useState+fetch模式useOptimistic在服务器确认之前启用即时 UI 更新useFormStatus为您提供挂起状态,无需通过表单组件进行道具钻取- 资源预加载 API(
preload、preinit)可让您控制组件的资源加载- 组件中的
<title>、<meta>和<link>标签自动提升到<head>- 服务器组件和客户端组件形成树 - 客户端组件无法导入服务器组件
服务器组件实际上是什么
在讨论更改内容之前,先澄清什么是服务器组件:专门在服务器上呈现并生成客户端水合的 HTML + RSC 有效负载的 React 组件。它们与服务器端呈现的客户端组件不同。
主要区别:
| 服务器组件 | 客户端组件 | |
|---|---|---|
| 运行于 | 仅限服务器 | 服务器(初始渲染)+客户端 |
| 可以使用钩子 | 没有 | 是的 |
| 可以使用浏览器API | 没有 | 是的 |
| 可以访问数据库 | 是(直接) | 否(通过 API) |
| 捆绑尺寸影响 | 零 | 是的 |
| 可以是异步的 | 是的 | 否(毫无悬念) |
// Server Component — async, no hooks, direct DB access
async function UserProfile({ userId }: { userId: string }) {
// Direct database query — no API needed
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
});
return (
<div>
<h1>{user.name}</h1>
{/* Client Component gets data as props */}
<UserActions userId={user.id} role={user.role} />
</div>
);
}
// Client Component — can use hooks, handles interactions
'use client';
function UserActions({ userId, role }: { userId: string; role: string }) {
const [isEditing, setIsEditing] = useState(false);
return (
<div>
<button onClick={() => setIsEditing(true)}>Edit</button>
{isEditing && <EditUserForm userId={userId} />}
</div>
);
}
反应 19 个动作
React 19 中最大的人体工程学改进是 Actions——一种处理由用户交互(特别是表单提交)触发的异步操作的标准化方法。
在 React 19 之前,表单处理是重复的样板:
// React 18 — manual state management
function ContactForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setSubmitting(true);
setError(null);
try {
await createContact({ name, email });
} catch (err) {
setError(err.message);
} finally {
setSubmitting(false);
}
}
return (
<form onSubmit={handleSubmit}>
{/* ... */}
</form>
);
}
React 19 Actions 显着简化了这一过程:
// React 19 — useActionState
'use client';
import { useActionState } from 'react';
async function createContactAction(
prevState: { error?: string },
formData: FormData
) {
'use server'; // Server Action — runs on the server
const name = formData.get('name') as string;
const email = formData.get('email') as string;
try {
await createContact({ name, email });
return { success: true };
} catch (err) {
return { error: err.message };
}
}
function ContactForm() {
const [state, formAction, isPending] = useActionState(
createContactAction,
{}
);
return (
<form action={formAction}>
<input name="name" required />
<input name="email" type="email" required />
{state.error && <p className="text-red-500">{state.error}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
useActionState 挂钩在一处管理挂起状态、表单数据和错误处理。操作函数上的 'use server' 指令将其标记为服务器操作 - 它在服务器上运行,可以直接访问数据库,并且客户端永远看不到实现。
服务器操作
服务器操作是带有 'use server' 指令的异步函数,当从客户端调用时,它们在服务器上运行。它们取代了大多数数据突变用例的 API 路由:
// app/actions/contacts.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@ecosire/db';
import { contacts } from '@ecosire/db/schema';
export async function createContact(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
if (!name || !email) {
throw new Error('Name and email are required');
}
await db.insert(contacts).values({
name,
email,
organizationId: await getOrganizationId(), // From session
});
revalidatePath('/dashboard/contacts'); // Invalidate cached page
redirect('/dashboard/contacts'); // Redirect after success
}
服务器操作可以直接在 form 元素中使用:
import { createContact } from '@/app/actions/contacts';
export default function NewContactPage() {
return (
<form action={createContact}>
<input name="name" placeholder="Name" required />
<input name="email" placeholder="Email" type="email" required />
<button type="submit">Create Contact</button>
</form>
);
}
没有 onSubmit 处理程序,没有 preventDefault(),没有手册 fetch() — 表单就可以工作。
use() 钩子
React 19 引入了 use() 钩子,用于在渲染内消耗资源(Promises 和 Context)。与 await 不同,它适用于 React 的 Suspense 系统:
// Before React 19 — had to resolve at the top level
export default async function Page() {
const posts = await getPosts(); // Blocks entire page
return <PostList posts={posts} />;
}
// React 19 — pass a promise, resolve with use()
export default function Page() {
const postsPromise = getPosts(); // Start fetch immediately
return (
<Suspense fallback={<PostsSkeleton />}>
<PostList postsPromise={postsPromise} />
</Suspense>
);
}
// Client Component using use() to consume the promise
'use client';
function PostList({ postsPromise }: { postsPromise: Promise<Post[]> }) {
const posts = use(postsPromise); // Suspends until resolved
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
use() 钩子还取代了 useContext() ——它可以有条件地使用上下文(与钩子不同,use() 可以在条件和循环内部调用):
'use client';
import { use } from 'react';
import { ThemeContext } from '@/contexts/theme';
function Button({ children }: { children: React.ReactNode }) {
const theme = use(ThemeContext); // Can be inside conditions
return (
<button className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'}>
{children}
</button>
);
}
使用 useOptimistic 进行乐观更新
useOptimistic 在服务器操作完成之前提供即时 UI 反馈,如果操作失败则自动恢复:
'use client';
import { useOptimistic, useTransition } from 'react';
interface Like {
id: string;
count: number;
userLiked: boolean;
}
function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: Like }) {
const [likes, setOptimisticLikes] = useOptimistic(
initialLikes,
(state, action: 'like' | 'unlike') => ({
...state,
count: action === 'like' ? state.count + 1 : state.count - 1,
userLiked: action === 'like',
})
);
const [isPending, startTransition] = useTransition();
async function toggleLike() {
const action = likes.userLiked ? 'unlike' : 'like';
startTransition(async () => {
setOptimisticLikes(action); // Instant UI update
// Server operation — if this fails, optimistic state reverts
await togglePostLike(postId, action);
});
}
return (
<button onClick={toggleLike} disabled={isPending}>
{likes.userLiked ? '♥' : '♡'} {likes.count}
</button>
);
}
乐观更新立即显示,然后提交(服务器成功)或恢复(服务器失败)。用户无需等待网络往返即可获得即时反馈。
组件的文档元数据
React 19 允许在任何组件内呈现 <title>、<meta> 和 <link> 标签 - 它们会自动提升到文档 <head>:
// Server Component — metadata hoists to <head> automatically
async function BlogPost({ slug }: { slug: string }) {
const post = await getPost(slug);
return (
<article>
<title>{post.title}</title>
<meta name="description" content={post.description} />
<link rel="canonical" href={`https://ecosire.com/blog/${slug}`} />
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
这适用于服务器和客户端组件。在 Next.js 等框架中,您仍将使用 generateMetadata() 来完全控制 OpenGraph 标签和 hreflang 属性 - React 19 的本机支持更为基本。但对于简单的情况,它消除了对 next/head 或类似库的需要。
资源加载API
React 19 提供了用于预加载资源的显式 API,让您可以对资源加载瀑布进行细粒度控制:
import { preload, preinit, prefetchDNS, preconnect } from 'react-dom';
function BelowFoldSection() {
// When this component renders, preload the hero image for next section
preload('/images/hero-next.webp', { as: 'image' });
// Preinit a script (loads and executes immediately)
preinit('https://cdn.example.com/analytics.js', { as: 'script' });
// DNS prefetch for external resources
prefetchDNS('https://fonts.googleapis.com');
// Establish connection early
preconnect('https://api.ecosire.com');
return <section>{/* content */}</section>;
}
这些 API 可以与 React 的渲染模型一起正常工作——它们批处理资源提示并在文档中的最佳点发出它们,甚至从组件树的深处发出它们。
常见陷阱和解决方案
陷阱 1:尝试在服务器组件中使用钩子
服务器组件不能使用 useState、useEffect、useContext 或任何其他挂钩。如果您需要这些,请将 'use client' 添加到组件中。错误通常是:Error: Hooks can only be called inside a function component。
陷阱 2:从客户端组件导入服务器组件
客户端组件无法导入服务器组件(反之亦然)。这是因为客户端组件在不存在服务器组件的浏览器中运行。如果您需要组合它们,请将服务器组件作为 children 属性传递:
// Wrong — Client Component cannot import Server Component
'use client';
import { ServerUserProfile } from './server-user-profile'; // Error
// Correct — pass as children from a Server Component parent
// Parent (Server):
<ClientShell>
<ServerUserProfile userId={userId} />
</ClientShell>
// ClientShell:
'use client';
function ClientShell({ children }: { children: React.ReactNode }) {
return <div className="shell">{children}</div>;
}
陷阱 3:将 props 从服务器传递到客户端时出现序列化错误
只有 JSON 可序列化数据才能作为 props 跨越服务器-客户端边界。函数、类实例、Map、Set 和 Date(作为对象)无法序列化。将日期转换为 ISO 字符串;用字符串标识符替换函数。
陷阱 4:服务器操作不验证输入
永远不要相信服务器操作中客户端提供的数据。在写入数据库之前始终使用 Zod 或类似工具进行验证:
'use server';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(2).max(255),
email: z.string().email(),
});
export async function createContact(formData: FormData) {
const result = schema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
});
if (!result.success) {
return { error: result.error.flatten().fieldErrors };
}
await db.insert(contacts).values(result.data);
}
常见问题
服务器组件在 Next.js 之外可用吗?
React 服务器组件是 React 的一项功能,但它们需要一个框架来处理服务器渲染基础设施——路由、捆绑、流。 Next.js App Router 是最成熟的实现。基于 Remix、Astro 和 Vite 的设置正在添加 RSC 支持。在没有框架的情况下使用 RSC 需要大量的定制基础设施工作。
服务器操作与 REST API 路由相比如何?
对于与 UI 并置的突变,服务器操作更简单 — 无需管理端点 URL,无需写入 fetch() 调用,无需错误处理样板。 REST API 路由更适合从 Web 应用程序外部(移动应用程序、Webhook、第三方集成)调用的操作、需要文档的公共 API 以及需要显式 HTTP 状态代码时。根据用例在同一应用程序中使用两者。
服务器组件对性能有什么影响?
服务器组件减少了 JavaScript 包的大小(组件代码永远不会发送到浏览器),消除客户端数据获取瀑布,并通过 Suspense 启用流式 HTML 交付。权衡是服务器计算成本——渲染发生在您的服务器上,而不是客户端的设备上。对于数据量大的页面来说,这几乎总是一个净胜。
我可以在代码库中混合使用 React 18 和 React 19 吗?
所有 React 代码都以 React 19 运行——没有每个文件的版本控制。问题是您现有的 React 18 代码是否可以在 React 19 中工作。大多数 React 18 代码都可以保持不变。主要的重大更改是围绕 refs (现在是常规属性)、ReactDOM.render 删除以及 @types/react 中的一些类型更改。运行 React 19 codemods 以进行自动迁移。
如何测试服务器组件?
服务器组件可以使用 async render 通过 React 测试库进行测试。对于服务器操作,将它们作为带有模拟数据库调用的普通异步函数进行测试。 Playwright 的端到端测试涵盖了完整的服务器组件 + 客户端组件集成,无需任何特殊设置 - 他们测试最终的 HTML 输出。
后续步骤
React 19 服务器组件代表了现代 Web 应用程序构建方式的根本转变。 ECOSIRE 的前端团队在此架构上构建了生产应用程序 — 249 个页面,其中包含服务器组件、服务器操作和为实际用户工作流程提供支持的乐观更新。
如果您需要从 React 18 迁移到 19 的帮助、构建新的 RSC 优先应用程序,或者只是需要团队中的专家前端工程,请探索我们的开发服务。
作者
ECOSIRE Research and Development Team技术写作
ECOSIRE 技术写作团队的内容涵盖 Odoo ERP、Shopify 电子商务、AI 智能体、Power BI 分析、GoHighLevel 自动化以及企业软件最佳实践。我们的指南帮助企业做出明智的技术决策。
相关文章
k6 负载测试:在发布之前对您的 API 进行压力测试
掌握 Node.js API 的 k6 负载测试。涵盖虚拟用户启动、阈值、场景、HTTP/2、WebSocket 测试、Grafana 仪表板和 CI 集成模式。
Next.js 16 应用程序路由器:生产模式和陷阱
生产就绪的 Next.js 16 个 App Router 模式:服务器组件、缓存策略、元数据 API、错误边界和要避免的性能陷阱。
Odoo 性能调优:PostgreSQL 和服务器优化
Odoo 19 性能调优专家指南。涵盖 PostgreSQL 配置、索引、查询优化、Nginx 缓存和企业部署的服务器大小调整。
更多来自性能与可扩展性
Webhook 调试和监控:完整的故障排除指南
通过这份涵盖故障模式、调试工具、重试策略、监控仪表板和安全最佳实践的完整指南掌握 Webhook 调试。
k6 负载测试:在发布之前对您的 API 进行压力测试
掌握 Node.js API 的 k6 负载测试。涵盖虚拟用户启动、阈值、场景、HTTP/2、WebSocket 测试、Grafana 仪表板和 CI 集成模式。
Nginx 生产配置:SSL、缓存和安全性
Nginx 生产配置指南:SSL 终止、HTTP/2、缓存标头、安全标头、速率限制、反向代理设置和 Cloudflare 集成模式。
Odoo 性能调优:PostgreSQL 和服务器优化
Odoo 19 性能调优专家指南。涵盖 PostgreSQL 配置、索引、查询优化、Nginx 缓存和企业部署的服务器大小调整。
在生产中测试和监控 AI 代理
在生产环境中测试和监控 AI 代理的完整指南。涵盖 OpenClaw 部署的评估框架、可观测性、漂移检测和事件响应。
使用 OpenClaw 进行合规性监控代理
部署 OpenClaw AI 代理以进行持续合规性监控。自动执行监管检查、策略执行、审计跟踪生成和合规性报告。