Skip to content
导航栏

数据获取

数据获取是任何应用程序的核心部分。下面介绍如何在React和Next. js中获取、缓存和重新验证数据。

服务端使用 fetch

Next.js 拓展了原生的 fetch Web API,增加了缓存(caching)和重新验证( revalidating)功能。可以在:服务端组件、路由处理程序、Server Actions 中搭配 async/await 使用 fetch。举个例子:

js
// app/page.js
async function getData() {
  const res = await fetch('https://api.example.com/...') 
  if (!res.ok) {
    // 这会触发最近的 `error.js` 错误边界
    throw new Error('Failed to fetch data')
  }
 
  return res.json()
}
 
export default async function Page() {
  const data = await getData()
 
  return <main></main>
}

缓存数据

默认情况下,Next.js 会自动缓存服务端 fetch 的返回值,也就是说,数据会在构建或者请求的时候被缓存,后续相同的请求会直接使用缓存中的数据,这有利于提高应用的性能。

js
// fetch 的 cache 选项用于控制该请求的缓存行为
// 默认就是 'force-cache', 平时写的时候可以省略
fetch('https://...', { cache: 'force-cache' })

注:不仅 GET 请求会被缓存,正常使用 POST 方法的 fetch 请求也会被自动缓存,但在路由处理程序中使用 POST 方法的 fetch 请求不会被缓存。

重新验证

有的时候缓存数据会过期,那么该如何更新缓存呢?在 Next.js 中,清除数据缓存并重新获取最新数据的过程就叫做重新验证(Revalidation)。

Next.js 提供了两种方式更新缓存:

一种是基于时间的重新验证(Time-based revalidation),即经过一定时间并有新请求产生后重新验证数据,适用于不经常更改且新鲜度不那么重要的数据。

一种是按需重新验证(On-demand revalidation),根据事件手动重新验证数据。按需重新验证又可以使用基于标签(tag-based)和基于路径(path-based)两种方法重新验证数据。适用于需要尽快展示最新数据的场景。

  • 基于时间的重新验证

    使用基于时间的重新验证,你需要在使用 fetch 的时候设置 next.revalidate 选项(以秒为单位):

    js
    fetch('https://...', { next: { revalidate: 3600 } })

    或者通过路由段配置项进行配置,使用这种方法,它会重新验证该路由段所有的 fetch 请求。

    js
    // layout.js | page.js
    export const revalidate = 3600

    注:在一个静态渲染的路由中,如果你有多个请求,每个请求设置了不同的重新验证时间,将会使用最短的时间用于所有的请求。而对于动态渲染的路由,每一个 fetch请求都将独立重新验证。

  • 按需重新验证

    使用按需重新验证,在路由处理程序或者 Server Action 中通过路径( revalidatePath) 或缓存标签 revalidateTag 实现。

    Next.js 有一个路由标签系统,可以跨路由实现多个 fetch 请求重新验证。我们来具体介绍下这个过程:

    • 当你使用 fetch 的时候,可以使用设置一个或者多个标签来标记请求
    • 然后你就可以调用 revalidateTag方法重新验证该标签对应的所有的请求

    举个例子:

    js
    // app/page.js
    export default async function Page() {
      const res = await fetch('https://...', { next: { tags: ['collection'] } })
      const data = await res.json()
      // ...
    }

    在这个例子中,你为 fetch 请求添加了一个 collection标签。现在,可以在 Server Action 中调用 revalidateTag,就可以让所有带 collection 标签的 fetch 请求重新验证。

    js
    // app/actions.ts
    'use server'
    
    import { revalidateTag } from 'next/cache'
    
    export default async function action() {
      revalidateTag('collection')
    }
  • 错误处理和重新验证 如果在尝试重新验证的过程中出现错误,缓存会继续提供上一个重新生成的数据,而在下一个后续请求中,Next.js 会尝试再次重新验证数据。

退出数据缓存

当 fetch 请求满足这些条件时不会被缓存:

  • fetch 请求添加了 cache: 'no-store' 选项
  • fetch 请求添加了 revalidate: 0 选项
  • fetch 请求在路由处理程序中并使用了 POST 方法
  • 使用headers 或 cookies 的方法之后使用 fetch请求
  • 配置了路由段选项 const dynamic = 'force-dynamic'
  • 配置了路由段选项fetchCache ,默认会跳过缓存
  • fetch 请求使用了 Authorization或者 Cookie请求头,并且在组件树中其上方还有一个未缓存的请求

在具体使用的时候,如果你想不缓存某个单独请求:

js
// layout.js | page.js
fetch('https://...', { cache: 'no-store' })
不缓存多个请求,可以借助路由段配置项:

// layout.js | page.js
export const dynamic = 'force-dynamic'

Next.js 推荐单独配置每个请求的缓存行为,这可以让你更精细化的控制缓存行为。

服务端使用三方请求库

也不是所有时候都能使用 fetch 请求,如果你使用了不支持或者暴露 fetch 方法的三方库(如数据库、CMS 或 ORM 客户端),但又想实现数据缓存机制,那可以使用 React 的 cache 函数和路由段配置项来实现请求的缓存和重新验证。

举个例子:

js
// app/utils.js
import { cache } from 'react'
 
export const getItem = cache(async (id) => {
  const item = await db.item.findUnique({ id })
  return item
})

现在我们调用两次 getItem :

js
// app/item/[id]/layout.js
import { getItem } from '@/utils/get-item'
 
export const revalidate = 3600 // 最多每小时重新验证一次
 
export default async function Layout({ params: { id } }) {
  const item = await getItem(id)
  // ...
}
// app/item/[id]/page.js
import { getItem } from '@/utils/get-item'
 
export const revalidate = 3600 // revalidate the data at most every hour
 
export default async function Page({ params: { id } }) {
  const item = await getItem(id)
  // ...
}

在这个例子中,尽管 getItem 被调用两次,但只会产生一次数据库查询。

客户端使用路由处理程序

如果你需要在客户端组件中获取数据,可以在客户端调用路由处理程序。路由处理程序会在服务端被执行,然后将数据返回给客户端,适用于不想暴露敏感信息给客户端(比如 API tokens)的场景。

如果你使用的是服务端组件,无须借助路由处理程序,直接获取数据即可。

客户端使用三方请求库

你也可以在客户端使用三方的库如 SWR 或 React Query 来获取数据。这些库都有提供自己的 API 实现记忆请求、缓存、重新验证和更改数据。

建议与最佳实践

有一些在 React 和 Next.js 中获取数据的建议和最佳实践,本节来介绍一下:

尽可能在服务端获取数据

尽可能在服务端获取数据,这样做有很多好处,比如:

  • 可以直接访问后端资源(如数据库)
  • 防止敏感信息泄漏
  • 减少客户端和服务端之间的来回通信,加快响应时间
  • ...

在需要的地方就地获取数据

如果组件树中的多个组件使用相同的数据,无须先全局获取,再通过 props 传递,可以直接在需要的地方使用 fetch 或者 React cache 获取数据,不用担心多次请求造成的性能问题,因为 fetch 请求会自动被记忆化。这也同样适用于布局,毕竟本来父子布局之间也不能传递数据。

适当的时候使用 Streaming

Streaming 和 Suspense都是 React 的功能,允许你增量传输内容以及渐进式渲染 UI 单元。页面可以直接渲染部分内容,剩余获取数据的部分会展示加载态,这也意味着用户不需要等到页面完全加载完才能与其交互。

串行获取数据

在 React 组件内获取数据时,有两种数据获取模式,并行和串行。

所谓串行数据获取,数据请求相互依赖,形成瀑布结构,这种行为有的时候是必要的,但也会导致加载时间更长。

所谓并行数据获取,请求同时发生并加载数据,这会减少加载数据所需的总时间。

我们先说说串行数据获取,直接举个例子:

js
// app/artist/page.js
// ...
 
async function Playlists({ artistID }) {
  // 等待 playlists 数据
  const playlists = await getArtistPlaylists(artistID)
 
  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}
 
export default async function Page({ params: { username } }) {
  // 等待 artist 数据
  const artist = await getArtist(username)
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

在这个例子中,Playlists 组件只有当 Artist 组件获得数据才会开始获取数据,因为 Playlists 组件依赖 artistId 这个 prop。这也很容易理解,毕竟只有先知道了是哪位艺术家,才能获取这位艺术家对应的曲目。

在这种情况下,可以使用 loading.js 或者 React 的 <Suspense> 组件,展示一个即时加载状态,防止整个路由被数据请求阻塞,而且用户还可以与未被阻塞的部分进行交互。

关于阻塞数据请求:

一种防止出现串行数据请求的方法是在应用程序根部全局获取数据,但这会阻塞其下所有路由段的渲染,直到数据加载完毕。 任何使用 await 的 fetch 请求都会阻塞渲染和下方所有组件的数据请求,除非它们使用了 <Suspense> 或者 loading.js。另一种替代方式就是使用并行数据请求或者预加载模式。

并行数据请求

要实现并行请求数据,可以在使用数据的组件外定义请求,然后在组价内部调用,举个例子:

js
import Albums from './albums'

// 组件外定义
async function getArtist(username) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}
 
async function getArtistAlbums(username) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}
 
export default async function Page({ params: { username } }) {
  // 组件内调用,这里是并行的
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)
 
  // 等待 promise resolve
  const [artist, albums] = await Promise.all([artistData, albumsData])
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums}></Albums>
    </>
  )
}

在这个例子中,getArtist 和 getArtistAlbums 函数都是在 Page 组件外定义,然后在 Page 组件内部调用。用户需要等待两个 promise 都 resolve 后才能看到结果。

为了提升用户体验,可以使用 Suspense 组件来分解渲染工作,尽快展示出部分结果。

预加载数据

防止出现串行请求的另外一种方式是使用预加载。可以创建一个 preload 函数进一步优化并行数据获取。使用这种方式,你不需要再使用 props 往下传递,举个例子:

js
// components/Item.js
import { getItem } from '@/utils/get-item'

export const preload = (id) => {
	void getItem(id)
}

export default async function Item({ id }) {
  const result = await getItem(id)
  // ...
}
// app/item/[id]/page.js
import Item, { preload, checkIsAvailable } from '@/components/Item'
 
export default async function Page({ params: { id } }) {
  // 开始加载 item 数据
  preload(id)
  // 执行另一个异步任务
  const isAvailable = await checkIsAvailable()
 
  return isAvailable ? <Item id={id} /> : null
}

使用 React cache server-only 和预加载模式

可以将 cache 函数,preload 模式和 server-only 包一起使用,创建一个可在整个应用使用的数据请求工具函数。

js
// utils/get-item.js
import { cache } from 'react'
import 'server-only'
 
export const preload = (id) => {
  void getItem(id)
}
 
export const getItem = cache(async (id) => {
  // ...
})

使用这种方式,可以快速获取数据、缓存返回结果并保证数据获取只发生在服务端。布局、页面或其他组件可以使用 utils/get-item。

Server Actions 和数据突变

Server Actions是在服务器上执行的异步函数。它们可以在服务器和客户端组件中使用,以处理 Next.js 应用程序中的表单提交和数据突变。

效果概述

Server Actions 是 Next.js 内置的关于服务端数据更改的解决方案。简单的来说,Server Actions 正如它的名字,指的是可以在服务端运行的函数,但它可以在客户端被调用,就像正常的函数一样,而无须手动创建一个 API 路由。

为了更加直观的了解 Server Actions 的应用场景,以表单提交为例。传统我们写一个表单提交,代码大致如下:

js
import { FormEvent } from 'react'

export default function Page() {
  async function oSubmit(event) {
    event.preventDefault()

    const formData = new FormDate(event.currentTarget)
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: formData,
    })

    const data = await response.json()
    // ...
  }
  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  )
}

我们首先会创建一个名为 /api/submit的 API 路由,然后用该 API 处理提交的数据,由此实现客户端与服务端通信。

这是使用 Next.js Pages Router 时的解决方案。但是使用 App Routers,借助 Server Actions,实现代码可以改为:

js
// page.js
export default function Page({ params }) {
  async function onSubmit() {
    'use server';
    // ...
  }
 
  return (
    <form action={onSubmit}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  );
}

无须创建 API 进行交互,直接定义一个 onSubmit 异步函数进行调用即可。不过要注意的是,这次使用的不再是 onSubmit 事件,而是 form 的 action 属性。

回顾一下基础的 HTML 知识,action 属性和 onsubmit 事件都与表单提交行为有关。onsubmit 用于表单提交时执行 JavaScript,一般用于验证数据格式。action 属性表示处理表单提交的 URL,浏览器会将表单数据封装成一个 HTTP 请求,将其发送到 action 属性指定的地址。如果 obsubmit 事件处理程序返回 false,浏览器将取消表单提交,也就不会触发 action 指定地址的数据发送。

js
<form action="submit.php" onsubmit="return validateForm()">
  <input type="text" name="username">
  <button type="submit">提交</button>
</form>

<script>
function validateForm() {
  if (/* 验证不通过 */) {
    return false;
  }
  return true;
}
</script>

使用 Server Actions 还会带来一个好处,就是因为借助的是 HTML 的 action 属性,这使得即使 JavaScript 没有加载完毕或是禁用 JavaScript,表单依然是可以正常使用的。这就实现了渐进式功能增强。

除此之外,传统开发应用的时候,往往一个路由一个表单,因为提交表单的时候,页面正常会刷新,但使用 Server Action,支持一个路由多个表单,且浏览器不会在提交表单的时候刷新。这样就可以在一次网络请求中实现数据和 UI 更新。

开启使用

使用 Server Actions,你需要先通过 experimental 的 serverActions 配置项开启:

js
// next.config.js
module.exports = {
  experimental: {
    serverActions: true,
  },
}

Next.js v14 以后,Server Actions 默认可用,使用 Next.js v14及以后的版本就不用开启了。

Server Actions 可以在两个地方被定义:

  • 使用它的组件内部(仅限服务端组件)
  • 在一个单独的文件(客户端组件和服务端组件),目的在于实现复用。你也可以在一个文件里定义多个 Server Action

服务端组件中使用

在服务端组件中创建 Server Actions,你需要定义一个异步函数,该函数的函数体顶部使用 "use server"指令。"use server"是为了确保该函数只会在服务端被执行。示例代码如下:

js
// app/page.js
export default function ServerComponent() {
  async function myAction() {
    'use server'
    // ...
  }
}

要注意:该函数需要使用可序列化的参数以及可序列化的返回值。其原因在服务端组件一节也有讲过,函数返回的结果会被序列化后发送给客户端。

客户端组件中使用

如果想在客户端组件中使用 Server Actions,第一种方式是导入。首先在一个单独的文件中创建 Server Action,该文件顶部需要一个 "use server"指令。然后在客户端组件导入该 Server Action,示例如下:

js
'use server'
// app/actions.js
export async function myAction() {
  // ...
}

注意:当使用这种顶层的 'use server' 指令的时候,下面所有的导出都会被认为是 Server Actions,所以可以在一个文件里定义多个 Server Action。

js
'use client'
// app/client-component.jsx
import { myAction } from './actions'
 
export default function ClientComponent() {
  return (
    <form action={myAction}>
      <button type="submit">Add to Cart</button>
    </form>
  )
}

第二种方式是作为 props 传递给客户端组件,示例代码如下:

js
<ClientComponent updateItem={updateItem} />
'use client'
// app/client-component.jsx
export default function ClientComponent({ updateItem }) {
  return (
    <form action={updateItem}>
      <input type="text" name="name" />
      <button type="submit">Update Item</button>
    </form>
  )
}

绑定参数

可以使用 bind 方法为 Server Actions 绑定参数。这会创建一个新的 Server Action,其中部分参数被绑定。这个技巧有的时候会很实用,示例如下:

js
'use client'
 // app/client-component.jsx
import { updateUser } from './actions'
 
export function UserProfile({ userId }) {
  const updateUserWithId = updateUser.bind(null, userId)
 
  return (
    <form action={updateUserWithId}>
      <input type="text" name="name" />
      <button type="submit">Update User Name</button>
    </form>
  )
}

updateUser Server Action 的代码如下:

js
'use server'
// app/actions.js
export async function updateUser(userId, formData) {
  // ...
}

调用

现在我们已经知道如何开启和创建 Server Action 了,而关于如何调用,Server Actions 有三种调用方式:

第一种方式是使用 action,React 的 action prop 支持在 <form> 元素上调用一个 Server Action。在这里就不举例了,前面的例子都是这种形式。

第二种方式是使用 formAction,React 的 formAction prop 允许在 <form> 中处理 <button>, <input type="submit">, 和 <input type="image">元素。示例代码如下:

js
<form action={handleSubmit}>
    <input type="submit" name="name" formAction={handleName} />
    <button type="submit">Submit</button>
</form>

不过说起 formaction,这其实是 HTML5 中的属性,formaction 属性只能作用于具有提交功能的按钮(也就是<button><input type="submit"><input type="image">)。如果通过具有 formaction 属性的按钮提交表单,数据发送的地址会是 formaction 指定的地址而非 form 上的 action 指定的地址。

第三种方式是使用 startTransition,不需要借助 action 或 formAction 属性,而是使用 React 的 startTransition,不过这种方式就不具备渐进式增强的特性了。使用示例代码如下:

js
'use client'

import { useTransition } from 'react'
import { addTodo } from '@/actions/addTodo';

export default function CourseComment() {
  let [isPending, startTransition] = useTransition()

  return (
    <button onClick={() => startTransition((text) => { 
      addTodo(text)})
    }>
      Add Todo
    </button>
  )
}

关于渐进式增强

渐进式增强下,即使没有 JavaScript 或者 JavaScript 被禁用,<form> 依然可以正常运行。这就使得在表单的 JavaScrpt 尚未加载完毕或者加载失败的时候,用户也能与表单交互并提交数据。

React Actions 支持渐进式增强,这又具体分两种情况:

  • 如果在服务端组件调用 Server Action,那表单就是可以在没有 JavaScript 的时候正常运行
  • 如果在客户端组件调用 Server Action,表单依然是可交互的,但是该 Action 会被放到一个队列中,直到该表单完全可交互(水合)。React 会提高该 action 的优先级,所以它依然会很快发生。

大小限制

默认情况下,发送到 Server Action 的请求体最大是 1M 大小,这是为了防止在解析大量数据时消耗过多的服务器资源。

然而,可以通过 serverActionsBodySizeLimit 配置项来修改这个限制。它可以是一个关于字节的数字或是支持字节的任何格式化字符串,,比如 1000, '500kb' 或者是 '3mb'。

js
// next.config.js
module.exports = {
  experimental: {
    serverActions: true,
    serverActionsBodySizeLimit: '2mb',
  },
}

使用常见问题

参考示例

Next.js 提供了 forms 与 Server Actions 的示例代码,我们可以通过该命令行创建:

shell
npx create-next-app@latest -e next-forms

这是该示例代码源码地址,官方也做了对应的视频介绍。

一个基础的示例代码如下:

js
// app/page.jsx
export default function Home() {
  async function handleFormAction(formData) {
    'use server';
    const name = formData.get('name');
  }

  return (
    <form action={handleFormAction}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  )
}

从这个例子中,我们看到,使用 Server Actions,我们可以很方便的定义服务端处理函数,也可以很方便的从表单数据中取值。

我们也可以结合 React 的 useFormState hook,当然这个 hook 还在实验中。

js
'use client'

import { experimental_useFormState as useFormState } from 'react-dom'

export default function Home() {

  async function createTodo(prevState, formData) {
    return prevState.concat(formData.get('todo'));
  }

  const [state, formAction] = useFormState(createTodo, [])

  return (
    <form action={formAction}>
      <input type="text" name="todo" />
      <button type="submit">Submit</button>
      <p>{state.join(',')}</p>
    </form>
  ) 
}

使用 useFormState 的时候要注意,该组件需要在客户端组件中使用。

重新验证

Server Actions 允许你按需重新验证数据。可以使用 revalidatePath:

js
'use server'
// app/actions.js
import { revalidatePath } from 'next/cache'
 
export default async function submit() {
  await submitForm()
  revalidatePath('/')
}

或者使用 revalidateTag:

js
'use server'
 // app/actions.js
import { revalidateTag } from 'next/cache'
 
export default async function submit() {
  await addPost()
  revalidateTag('posts')
}

重定向

如果你希望 Server Action 结束之后重定向到其他路由,可以使用 redirect 到一个相对或者固定地址。

js
'use server'
 // app/actions.js
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
 
export default async function submit() {
  const id = await addPost()
  revalidateTag('posts') // Update cached posts
  redirect(`/post/${id}`) // Navigate to new route
}

表单验证

Next.js 推荐基本的表单验证使用 HTML 元素自带的验证如 required、type="email"等。

对于更高阶的服务端数据验证,可以使用 zod 这样的 schema 验证库来验证表单数据的结构:

js
// app/actions.js
import { z } from 'zod'
 
const schema = z.object({
  // ...
})
 
export default async function submit(formData) {
  const parsed = schema.parse({
    id: formData.get('id'),
  })
  // ...
}

显示加载状态

当表单提交数据的时候,可以使用 useFormStatushook 来显示加载状态。useFormStatus hook 只能用于 form 元素的子级。

关于 useFormStatus的具体使用用法,可以参考 React 官方文档中的介绍。

示例代码如下:

js
'use client'
// app/submit-button.jsx
import { useFormStatus } from 'react-dom'
 
export function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button type="submit" aria-disabled={pending}>
      {pending ? 'Adding' : 'Add'}
    </button>
  )
}
<SubmitButton />可以被用在使用 Server Action 的表单中:

// app/page.jsx
import { SubmitButton } from '@/app/submit-button'
 
export default async function Home() {
  return (
    <form action={...}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}

错误处理

Server Action 可以返回可序列化的对象。举个例子,当一个条目创建失败,你需要处理这个错误:

js
'use server'
// app/actions.js
export async function createTodo(prevState, formData) {
  try {
    await createItem(formData.get('todo'))
    return revalidatePath('/')
  } catch (e) {
    return { message: 'Failed to create' }
  }
}

然后你就可以在客户端组件中,读取这个值并显示这个错误信息:

js
'use client'
// app/add-form.jsx
import { useFormState, useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'
 
const initialState = {
  message: null,
}
 
function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button type="submit" aria-disabled={pending}>
      Add
    </button>
  )
}
 
export function AddForm() {
  const [state, formAction] = useFormState(createTodo, initialState)
 
  return (
    <form action={formAction}>
      <label htmlFor="todo">Enter Task</label>
      <input type="text" id="todo" name="todo" required />
      <SubmitButton />
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
    </form>
  )
}

当然你也可以结合 error.jsx 展示错误时的 UI:

js
'use client'
// error.jsx
export default function Error() {
  return (
    <h2>error</h2>
  )
}
// page.jsx
import { experimental_useFormState as useFormState } from 'react-dom'

function AddForm() {
  async function serverActionWithError() {
    'use server';   
    throw new Error(`This is error is in the Server Action`);
  }

  return (
    <form action={serverActionWithError}>
      <button type="submit">Submit</button>
    </form>
  ) 
}

export default AddForm

这样当 Server Action 发生错误的时候,就会展示错误 UI。

乐观更新

所谓乐观更新,举个例子,当用户点击一个点赞按钮的时候,传统的做法是等待接口返回成功时再更新 UI,乐观更新是先更新 UI,同时发送数据请求,至于数据请求后的错误处理,则根据自己的需要自定义实现。

React 提供了 useOptimistic 这个 hook,结合 Server Actions 使用的示例代码如下:

js
'use client'
// app/page.jsx
import { experimental_useOptimistic as useOptimistic } from 'react'
import { send } from './actions'
 
export function Thread({ messages }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [...state, { message: newMessage }]
  )
 
  return (
    <div>
      {optimisticMessages.map((m) => (
        <div>{m.message}</div>
      ))}
      <form
        action={async (formData) => {
          const message = formData.get('message')
          addOptimisticMessage(message)
          await send(message)
        }}
      >
        <input type="text" name="message" />
        <button type="submit">Send</button>
      </form>
    </div>
  )
}

设置 Cookies

可以在 Server Action 中使用 cookies 函数设置 cookie:

js
'use server'
// app/actions.js
import { cookies } from 'next/headers'
 
export async function create() {
  const cart = await createCart()
  cookies().set('cartId', cart.id)
}

读取 Cookies

依然是使用 cookies 函数:

js
'use server'
// app/actions.js
import { cookies } from 'next/headers'
 
export async function read() {
  const auth = cookies().get('authorization')?.value
  // ...
}

删除 Cookies

还是使用 cookies 函数:

js
'use server'
// app/actions.js
import { cookies } from 'next/headers'
 
export async function delete() {
  cookies().delete('name')
  // ...
}