SSR、Next.js与服务器组件

你好,我是宋一玮,欢迎回到React应用开发的学习。

上节课我们回顾了React的Suspense与组件的懒加载,介绍了React 19新API use 的两种用法 use(Promise)use(Context) ,还有refforwardRef 的变化,以及JSX中新支持的HTML标签<title><link><meta><style><script>

可能你已经注意到了,前两节课在介绍React 19新功能之前,我都会介绍必要的背景知识,比如介绍action之前先补充transition的知识,介绍 use(Promise) 之前先回顾了Suspense的知识。这符合本专栏一贯的讲解思路,即帮助你在学习新功能、新API的同时,了解它们能解决什么样的问题,并强调知识点之间的相关性。

这节课作为React 19新功能系列加餐中的最后一节,将聚焦React 19在服务器端的两大重量级新功能:服务器组件和服务器action。延续之前的讲解思路,我们接下来会先讲解服务器端渲染SSR是什么,为什么要在服务器端跑React,并简要介绍React SSR首选的全栈开发框架Next.js。

服务器端渲染SSR

随着React项目规模的扩大和依赖项的增加,生产环境构建的产物,尤其是*.js 文件体积会逐渐增大。我就经手过单个JS文件达到10MB以上的项目,这文件还是经过代码压缩的。这样的大文件会导致用户浏览器加载应用时,需要长时间面对白屏,不利于用户体验(如下图所示)。

这种情况下,我们可以利用代码分割技术减小单个文件的体积,不过同时我们也可以思考一下,有没有其他方式可以减少用户的白屏时间?

服务器端渲染(Server-Side Rendering,后文简称SSR)可以有效缓解这个问题。顾名思义,服务器端渲染可以理解成:把全部或部分渲染工作转移到服务器端。React有SSR,Vue、Angular也有SSR。不同框架实现SSR的方式不尽相同,但都能有效减少用户浏览器首次加载的白屏时间。

React的SSR

用户访问整个React应用时,首次请求的HTML文档只用于加载JS、CSS文件,并不包含任何实际内容,因此出现了白屏。如果HTML直接包含了首屏的有效内容,用户就可以更早地开始使用应用。在架构层面,除了前面一直采用的浏览器端运行应用之外,React还支持在服务器端渲染组件。

我们来回顾一下本专栏的样例应用oh-my-kanban,它的入口代码是这样导入 ReactDOM 对象的:

import ReactDOM from 'react-dom/client';

这是 ReactDOM 的客户端或者说浏览器端API。在同一个react-dom 包里,React还提供了另一套服务器端API,例如:

import { renderToString } from 'react-dom/server';


const html = renderToString(<App />);

这段代码可以在服务器端的Node.js环境中执行,配合Node.js下的Web服务器,如Express.js、Koa等,就可以直接将组件渲染出来的完整HTML直接返回给用户浏览器。这就是React的SSR。

但这样的HTML页面暂时不具有动态交互性,诸如 onClick 等逻辑在服务器端渲染时会被忽略。等到HTML在浏览器端加载后,浏览器会继续加载React项目构建产物中的 *.js 等文件,待加载完成会调用 ReactDOM 客户端API的 hydrateRoot 方法,在浏览器端重建虚拟DOM树,并把 onClick 等逻辑关联到对应的DOM元素上去,这样一来React应用就被还原成了“完全体”,而这个还原过程被称为“水合”(hydrate)。水合的关键代码如下:

import { hydrateRoot } from 'react-dom/client';


hydrateRoot(document.getElementById('root'), <App />);

浏览器加载服务器端渲染的HTML后到水合完成之前的这段时间,我们的React应用虽然并不能响应按钮点击等操作,但已经可以向用户展示关键内容了。SSR为React应用带来了更短的首屏加载时间,如下图所示,你可以与前面CSR的图对比一下。

此外SSR对搜索引擎也更加友好,有利于搜索引擎优化(SEO)。早期的搜索引擎爬虫技术对页面JS的支持有限,爬取网页时,只读取到了HTML的内容,而错过了JS动态生成的内容,导致搜索引擎认为该网页质量不高,网页在搜索结果中的排名就相应降低。

SSR产生的页面HTML会包含更多有效内容,有利于提高在搜索结果中的排名。但随着搜索引擎技术的进步,现代的爬虫已经可以正常处理JS和JS动态生成的内容,HTML静态内容的比例已经不再是搜索排名的关键因素,不过在这一背景下,SSR带来的页面加载速度的提升,仍对SEO有很大帮助。

你可能会好奇,SSR与PHP、ASP.NET这样的传统服务器端页面技术有什么区别?传统服务器端页面技术确实也在服务器端生成HTML,但通常不涉及现代JavaScript框架。按用途来区分,SSR用于SPA(Single Page Application)单页应用,而传统服务器端页面技术一般用于MPA(Multi-Page Application)多页应用。

CSR、SSR与SSG

与SSR类似的架构概念还有:CSR(Client-Side Rendering)客户端渲染,就是本专栏一直在使用的架构,oh-my-kanban也是采用了这个架构;SSG(Static Site Generation)静态站点生成,利用React技术生成静态网页。

我对CSR、SSR和SSG做了简单对比:

React全栈开发框架Next.js

虽然React提供了与SSR相关的API,但要想实现一套完整的SSR方案,开发者额外需要做的事情还不少,如读取服务器端数据、服务器端路由等。React的生态是非常丰富的,但这也反过来增加了开发者做技术选型和整合多种库的成本。开发者们期待着“一站式”的解决方案。以此为契机,React社区中兴起了一批基于React的全栈Web开发框架,其中Next.js框架尤其受到广大开发者的欢迎。

Next.js框架简介

Next.js刚推出时是为了解决React SSR开发的一些痛点,在后续不断演进的过程中,它吸纳了业界的最佳实践,加入了对全栈Web开发的支持。截至15版本,Next.js主要支持了以下功能:

  • 内建的代码构建功能。

  • 在浏览器端或服务器端渲染React组件,在页面级别支持CSR、SSR、SSG。

  • 支持各种主流CSS方案。

  • 获取服务器端数据,内建缓存。

  • 服务器端与浏览器端路由。

  • 内建性能优化,以及对图片等资源文件的优化。

  • React 19的服务器组件和服务器action。

开发者使用Next.js开发React应用时,如果没有特别的需要,无须再引入React Router或React Query等库,从而省去了一些技术选型的工作。Next.js这种整合了多种框架和库的框架,也被称为元框架(meta-framework)。

但同时也需要注意的是,以上很多功能的前提是需要Next.js本身作为服务器运行时,部署上线时要把整个Next.js应用部署为Web服务器,传统CSR采用的静态Web服务器(如Nginx) + CDN是不够的。Next.js应用开发的范式与传统React应用开发也不太一样,很多时候要考虑服务器端的行为,当然,这也扩展了React应用的能力边界。

在SSR领域同样具有影响力的还有Remix框架(已经合并入React Router v7),在SSG领域则有Gatsby框架。目前React官方也开始鼓励开发者使用基于React的全栈框架,如Next.js、Remix或Gatsby来开始一个React项目。

创建Next.js项目

我们下面来创建一个Next.js项目,你可以顺便对比一下这与创建传统React项目有什么不同。首先在命令行运行:

npx create-next-app@latest

这里我用的Next.js版本是15.2.3。当命令提示What is your project named?时,输入oh-my-next-app;为了方便本节课程的实践,命令提示的其他问题中,除了Would you like to use App Router? 选择Yes,其他都选No。等命令创建完项目,用IDE查看生成的package.json文件,可以看到reactreact-dom的版本都是19.0.0

然后启动项目:

cd oh-my-next-app
npm run dev

在浏览器访问:http://localhost:3000/,可以看到如下页面:

你可以使用浏览器的查看页面源代码功能,查看当前页面的HTML代码,类似下面这样:

<body class="__variable_4d318d __variable_ea5f4b">
  <div class="page_page__556_G">
    <main class="page_main__nw1Wk">
      <img alt="Next.js logo" src="/next.svg" />
      <ol>
        <li>Get started by editing <code>app/page.js</code>.</li>
        <li>Save and see your changes instantly.</li>
      </ol>
      <!-- 省略其余HTML -->

你会发现与Vite或CRA创建的传统React项目不同,比起简单的一行<div id="root"></div>,Next.js渲染的首页HTML代码中包含了对应实际内容的静态HTML标签。虽然后面还有不少JS代码,但这部分HTML保证了页面在下载到浏览器的第一时间就能为用户展示出有效内容。这就是Next.js基于React提供的服务器端渲染能力。

在App Router中添加表单页面

这个项目使用了Next.js 15版本推荐的App Router功能,在项目 app 目录下,Next.js会根据目录结构和文件名自动配置页面路由。以上的首页对应的是app/page.js文件,我们马上来试试新加一个页面。在 app 目录新建一个名叫 contact 的子目录,在这个子目录里新建一个 page.js 文件,在文件中加入第一节加餐中用于演示表单action的代码:

import { useState } from 'react';


function ContactForm() {
  const [name, setName] = useState();
  const formAction = async (formData) => {/* 省略action代码 */};
  return (
    <form action={formAction}>
      {/* 省略JSX */}
    </form>
  );
}


export default ContactForm;

Next.js基于上述目录结构,自动创建了 /contact 路由。尝试访问 http://localhost:3000/contact,你会看到页面报错:

Error:   × You’re importing a component that needs useState. This React hook only works in a client component. To fix, mark the file (or its parent) with the "use client" directive.

这时我们需要在刚才的 app/contact/page.js 文件的开头插入一行语句:

'use client';

这样一来,页面就可以正常渲染了。在React 19中,'use client' 和下面要讲到的 'use server' 是两个特殊的指示符(directive),用于向构建工具提供指令。这里的 'use client' 就是告诉构建工具,这个文件的代码都是运行在客户端(即浏览器端)的。

接下来我们在首页添加一个指向/contact的导航链接。

import Link from 'next/link';


export default function Home() {
  return (
    <Link href="/contact">联系人管理</Link>
    {/* 省略其他JSX */}
  );
}

浏览器中查看 http://localhost:3000/ 首页,点击刚添加的“联系人管理”链接,页面会跳转到刚刚创建的 /contact 页面。注意这个跳转实际上是一个基于前端路由的视图切换,并不会刷新整个浏览器,至于地址栏中URL的变化,则是Next.js调用浏览器History API实现的。目前地址栏的URL为 http://localhost:3000/contact,如果你强行刷新浏览器,浏览器会重新加载联系人页面,这意味着后端路由也是等效的。

Next.js框架的功能比起原生React丰富了很多,基于Next.js进行应用开发也会涉及很多全栈框架所特有的最佳实践。这些内容多到足够开一个新专栏了,这里我们点到为止。

React 19的服务器组件

React 19新加入了服务器组件(React Server Components,缩写为RSC)。服务器组件就是只在服务器端运行的React组件它完全在服务器端渲染,渲染结果会随着SSR渲染的HTML传递给客户端(即浏览器端)。

开发者可以在服务器组件中运行服务器端代码,比如读取数据库或远程API,将返回的数据直接用JSX渲染出来。为了演示服务器端代码与浏览器端代码的差别,我们不妨来试试在服务器组件里用Node.js API读取本地文件。首先,我们在刚才的Next.js项目根目录建一个文本文件 data.json

{
    "contacts": [
        { "id": 1, "name": "张三" },
        { "id": 2, "name": "李四" },
        { "id": 3, "name": "王五" }
    ]
}

然后来到app/page.js文件,将 Home 组件的函数改为异步函数,并加入读取文件的代码:

import Link from 'next/link';
import fs from 'node:fs/promises';


export default async function Home() {
  const content = await fs.readFile('./data.json', 'utf-8');
  const { contacts } = JSON.parse(content);
  return (
    <div>
      <ul>
        {contacts.map(contact => (
          <li key={contact.id}>{contact.name}</li>
        ))}
      </ul>
      <Link href="/contact">联系人管理</Link>
    </div>
  );
}

刷新页面,可以看到浏览器页面中展示了来自本地文件的联系人列表。这样做的好处如下图所示,用户访问这个页面的首次请求就可以得到包含了实际数据的HTML,而不需要等浏览器端JS加载、React完成水合后再发起对数据的请求。

这里要纠正一个可能的误会,并不是只有async 异步函数才能定义服务器组件。异步函数并不是服务器组件的特征,而是一个特性。前面我们之所以把组件函数改为异步函数,是因为在JS中读取文件或数据库的代码大都是异步的,服务器组件对异步函数的支持为我们提供了便利。其实在改为异步函数之前,这个 Home 组件就已经是服务器组件了,Next.js的App Router默认将包含页面组件在内的所有组件,无论同步函数还是异步函数声明的,都当作服务器组件。

需要注意的是,与传统React组件不同,服务器组件本身不会在浏览器端渲染(或重新渲染),也不会在浏览器端进行水合,甚至服务器组件的源代码都不会被打包进浏览器加载的JS中。没有水合意味着,开发者不能在服务器组件中使用任何交互性的Hooks或JS代码,比如useStateuseEffect 、事件处理等等。

这时你可能会困惑,React没有了交互性,这难道不是一种倒退吗?你大可放心,服务器组件可以跟客户端组件,也就是传统React组件配合使用。你可以把服务器组件的子组件抽取到独立的文件中,在文件顶部加入'use client'指示符,这个文件所包含的组件就会被视为客户端组件,在这里你可以放心地使用各种Hooks、事件处理函数,以及其他浏览器端的JS交互代码。

在Next.js中,客户端组件默认也会被SSR(“客户端组件”这个名字会有点误导,你可以理解成传统React组件)。当浏览器中的React完成初始化,服务器组件虽然不会参与渲染,但会把专用的序列化数据同步到浏览器端,跟随其他客户端组件形成一个完整的组件树,水合过程还是会照常进行,水合完成后客户端组件的交互代码就会生效。上一小节加入的 ContactForm 组件就是一个客户端组件,你可以在里面使用 useState 等Hooks。

服务器组件与客户端组件之间可以通过props通信。服务器组件传递给客户端组件的props必须是可序列化(serializable)的数据类型,或者是Promise(请回忆一下上节课的 use(Promise) ),甚至可以传递服务器action的函数引用。但因为服务器组件并不包含状态,使得它与客户端组件之间的通信基本是单向的。如果你希望通过客户端组件的交互影响服务器组件,通常需要借助路由

服务器组件的子组件可以是服务器组件也可以是客户端组件。那么客户端组件的子组件会是什么呢?

在客户端组件所在文件中,用 import 语句导入的子组件、后代组件也都会自动被视为客户端组件。你如果像第一节加餐那样,把表单按钮抽取成 FormButtons 组件并放在同目录的FormButtons.jsx文件中,那么即使你不在文件开头加入'use client'FormButtons也仍旧会是客户端组件。这样的规则使得你无法通过 import 的方式把服务器组件用作客户端组件的子组件。也许你会意外,'use server'并不是用来声明服务器组件的,而是有其他用途,我们待会儿会讲。

但这并不意味着客户端组件的子组件只能是客户端组件。服务器组件传递给客户端组件的props除了前面列举的类型,还可以传递React元素(即JSX),这里的元素并不限定是客户端组件还是服务器组件的元素。这就带来了一种灵活的组件混合模式:通过 children prop,将服务器组件传递给客户端组件。如以下代码所示:

import ClientComponent from './ClientComponent.jsx';
import ServerComponent from './ServerComponent.jsx';
// Page默认为服务器组件
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  );
}

React 19的服务器Action

第一节加餐曾介绍过React 19的action,是指用于触发transition的函数。在服务器组件环境下,React 19更是支持了服务器action(Server Actions),即能被客户端组件调用的、在服务器端执行的action

让我们回忆一下第一节加餐中的表单action,我们一般会在表单action里将表单数据提交到服务器端API,进而保存到数据库里,开发者需要额外实现一套服务器端API,也需要编写调用 fetch API的逻辑。如果表单action能直接在服务器端执行,那就可以省略服务器端API,直接将表单数据保存到数据库里!

我们马上试试把前面表单页面的 formAction 改成服务器action。在 app/contact 目录下新建一个 actions.js 文件,代码如下:

'use server';


import { redirect } from 'next/navigation';
import fs from 'node:fs/promises';


export async function saveContactAction(formData) {
  const content = await fs.readFile('./data.json', 'utf-8');
  const { contacts } = JSON.parse(content);
  contacts.push({
    id: contacts.length + 1,
    name: formData.get('name'),
  });
  const newContent = JSON.stringify({ contacts }, null, 4);
  await fs.writeFile('./data.json', newContent, 'utf-8');
  redirect('/');
}

文件的第一行是'use server'指示符,这代表该文件中所有函数都是服务器函数。我们创建了一个名叫saveContactAction的服务器action,利用Node.js API将新联系人写入到 data.json 文件中。注意这单纯是为了演示action执行服务器端代码的能力,在实际项目中比起读写文件,还是操作数据库更常见一些。在action的末尾,我们调用Next.js的导航API redirect跳转回首页。

要调用这个服务器action,只需修改联系人表单ContactForm组件:

import { saveContactAction } from './actions.js';


function ContactForm() {
  // 删除原来的formAction
  return (
    <form action={saveContactAction}>

这样在浏览器端提交表单后,saveContactAction会在服务器端被执行,然后浏览器跳转回首页,首页上会显示新提交的联系人。其实这个过程还是存在浏览器端对服务器端API的调用的,只不过Next.js框架为你代劳了。

除了文件第一行,'use server'也可以写在单个函数的顶部,如:

async function saveContactAction(formData) {
  'use server';
  // 其他代码
}

要使用服务器action,除了像前面这样、在客户端组件 import 导入服务器action,也可以由服务器组件通过props将服务器action的引用传递给客户端组件。

React官方文档最近把服务器action改名成了服务器函数(Server Functions),表示并不是所有的服务器函数都是服务器action。不过我们在遇到具体的、action之外的用例之前,不妨先暂时沿用服务器action这个名称。

React 19的服务器组件和服务器action带来了前端开发范式的变化,让SSR的功用更上一层楼。不过截止到2025年3月,Next.js是事实上唯一完整支持这两个新特性的全栈框架,所以我们这节加餐一大半都在介绍Next.js。我很期待其他React元框架也陆续加入相应的支持。

小结

这节课我们首先介绍了什么是服务器端渲染SSR,为什么需要SSR,以及SSR与传统CSR的区别。接着,我们简要介绍了React在全栈Web应用开发领域首选的元框架Next.js,演练了如何创建Next.js项目,并在App Router中加入新的页面。最后,我们重点介绍了React 19的新功能:服务器组件和服务器action,也通过实例演示了如何在Next.js中使用这两个功能。

至此,我们完成了关于React 19新功能的全部三节加餐,希望这三节加餐能帮助你掌握React 19的最新变化。

思考题

  1. 在React 19引入服务器组件功能后,我们与服务器通信的能力得到了增强。能不能请你说一说,在React 19中分页显示数据库中的列表数据,都有哪些方法?

  2. 你能用第一节加餐讲过的 useActionState 结合服务器action实现一版联系人表单吗?

回顾本专栏之初定下的期望:“学习一门技术,务必要有大于一门技术的收获”,希望这次加餐也助你达成了这个目标。重聚虽然短暂,但我相信,只要你一如既往,对前端技术充满着好奇和热情,那我们一定会在不远的将来再见。

精选留言

  • 空间

    2025-05-08 08:29:04

    我前年用过一小段儿next.js。我的体验是:
    优点:
    1,可以满足部分首屏加载速度的需求,
    2,可以满足一些搜索引擎优化的需求,
    3,可以满足一些敏感数据不在前端加载的需求。
    4,SSG可以满足一部分固定内容页面加载速度的需求, 比如新闻网站的新闻页面。

    缺点:
    大大增加了程序的复杂度,因为前后端代码交织在一起,你需要考虑哪些是前端在跑,哪些是后端在跑,哪些是同时在跑。前后端分离的架构变成了前后端紧耦合,同时增加了很多相关的api,这意味着理解和维护这样的代码都会更难。

    个人感觉,只要你没有上面三个优点中提到的强烈的需求,那建议多想想投入产出。如果必须要用,最好在网站里需要的部分用,其他的部分还保持前后端分离,因为他也还是支持原来那种写法的。
    作者回复

    你好,空间,非常好的总结。我对你说的第三点尤其赞同,用next.js开发时可以把一些敏感数据或敏感的业务逻辑限制在服务器端,降低了泄漏的风险。感谢你的分享。

    2025-05-15 10:07:05

  • O

    2025-04-29 10:04:08

    Next.js框架了解不深,但是学习这节课,有一个比较深的感受,就是Next.js在内聚前后端,我们通常的开发模式是前后端分离的,前端写一套逻辑,后端写一套逻辑,前端调用后端接口,而不管nextjs出于什么目的,反而在将前后端内聚,实现前后端的交叉,减少分界,对于习惯了以前的方式,着实需要适应一下,这种模式将来会影响到有多深远,再往后看以后的发展
    作者回复

    你好,O,我赞同你对Next.js的评价,它确实有别于传统前后端分离的开发方式。其实自从Node.js成为服务器后端开发的主流技术之一,我们前端开发者就天然具有了全栈开发的选项,比如用React开发前端、Express.js开发后端,甚至可以开发一些前后端共用的验证逻辑。

    Next.js在React SSR中看到了机会,尝试用它特有的方式解决一些前后端协同开发的痛点,正如我们这节课介绍的服务器action,就可以减少许多封装服务器端REST接口的工作。当然,Next.js背后的Vercel公司是一家云厂商,其云服务的立场也是值得关注的。

    不过使用Next.js也会带来一些新挑战,包括但不限于:
    1. 开发者最终仍然需要掌握哪些代码会在服务器端执行,哪些会在客户端执行,毕竟我们不能在浏览器端调用Node.js API,也不能在服务器端操作Window对象。这也导致两端的代码文件会混在一起,一个具体的例子:我在之前的项目里写了一个ldapUtil.ts,存在libs目录下,然后我赫然发现这个libs目录下还存着一个叫businessHooks.ts的文件……当时想过要不要再建个libs/server和libs/client的目录,但考虑到项目复杂度不高,还是作罢了。
    2. 前端开发者在扩展到全栈开发时,仍需掌握服务器端开发的知识和最佳实践。不能简单地把CSR的代码全文照搬到Next.js里。下面的代码:
    let count = 0;
    async function SomePage() {
    count++;
    return (<div>{count}</div>);
    }
    如果不做额外的设置,count变量会被所有请求共享,多个不同用户请求的页面会互相影响,严重时还会导致服务器端的内存泄露。

    总体而言,即使存在着一些问题,我认为Next.js带来的能力边界的扩展,和开发效率的提升还是颇具吸引力的。同时我也很期待Next.js之外的React元框架能发展起来,避免Vercel一家独大。

    2025-05-07 19:05:19

  • O

    2025-04-29 09:53:07

    第二个问题:
    'use client';
    import { saveContactAction } from './actions.jsx';
    import { useActionState } from 'react';

    export default function ContactForm() {
    const [_state, formAction, pending] = useActionState(
    async (_currentState, formData) => {
    await saveContactAction(formData)
    }, {});
    return (
    <>
    <h2>contactAction</h2>
    <form action={formAction}>
    <input type="text" name="name" placeholder="联系人名称" />
    <button type="submit">提交</button>
    {pending && <p>提交中...</p>}
    </form>
    </>
    );
    }
    作者回复

    你好,O,感谢你的回答。这个答案很好地演示了useActionState和服务器action混用时的写法。

    2025-05-07 18:16:53

  • O

    2025-04-28 18:10:28

    第一个问题主要考虑几个因素:列表数据、分页
    1、列表数据首次加载可以使用服务端渲染:在服务端组件,获取到的数据,可以传给客户端子组件,作为初始化的数据;
    2、分页可以使用服务端action,因为action触发的地方有两个,一个是表单的action,一个是startTransition,这里没有表单,所以可以调用startTransition,实现服务端action;
    // ./pagination/page.jsx
    import { getDataByPage } from './actions';
    import PaginationChild from './paginationChild.jsx';

    export default async function A () {
    const {data,total} = await getDataByPage(1)

    return <div>
    <h2>Pagination</h2>
    <PaginationChild initData={data} initTotal={total}></PaginationChild>
    </div>;
    }
    // ./pagination/paginationChild.jsx
    'use client';
    import { startTransition, useState } from 'react';
    import { getDataByPage } from './actions.jsx';

    export default function PaginationChild ({initData,initTotal}) {
    const [data, setData] = useState(initData);
    const [total, setTotal] = useState(initTotal);
    const getPage = (page) => {
    startTransition(async() => {
    const {data,total} = await getDataByPage(page)
    setData(data);
    setTotal(total);
    });
    }
    return <div>
    <ul> data:{data.map((item) => <li key={item}>{item}</li>)}</ul>
    <span>total: {total}</span>
    <div>
    <button onClick={() => getPage(1)}>第1页</button>
    <button onClick={() => getPage(2)}>第2页</button>
    </div>
    </div>;
    }
    // ./pagination/actions.jsx
    'use server';

    export async function getDataByPage(page) {
    // 模仿数据库
    await new Promise((resolve) => setTimeout(resolve, 1000));
    if(page === 1){
    return {
    data: ["id 1", "id 2", "id 3", "id 4", "id 5", "id 6", "id 7", "id 8", "id 9", "id 10"],
    total: 100,
    };
    }else{
    return {
    data: ["id 11", "id 12", "id 13", "id 14", "id 15", "id 16", "id 17", "id 18", "id 19", "id 20"],
    total: 100,
    };
    }
    }



    作者回复

    你好,O,感谢你的回答。这个答案很完整,既保证了在服务器端首次渲染时就加载了数据,又在客户端翻页时复用了相同的服务器action。非要鸡蛋里挑骨头的话,我还是建议在return JSX时加上括号:

    return (
    <div></div>
    );

    2025-05-07 18:14:19