React 19 Server Components: What Changed and Why

Deep dive into React 19 Server Components: Actions, use() hook, asset loading, optimistic updates, and the architectural patterns that make RSC production-ready.

E
ECOSIRE Research and Development Team
|2026年3月19日6 分钟阅读1.3k 字数|

属于我们的Performance & Scalability系列

阅读完整指南

React 19 服务器组件:发生了什么变化以及原因

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(preloadpreinit)可让您控制组件的资源加载
  • 组件中的 <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:尝试在服务器组件中使用钩子

服务器组件不能使用 useStateuseEffectuseContext 或任何其他挂钩。如果您需要这些,请将 '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 优先应用程序,或者只是需要团队中的专家前端工程,请探索我们的开发服务

E

作者

ECOSIRE Research and Development Team

在 ECOSIRE 构建企业级数字产品。分享关于 Odoo 集成、电商自动化和 AI 驱动商业解决方案的洞见。

通过 WhatsApp 聊天