Blog.

React RSC 的渲染和刷新

LiuuY

Dan Abramov 在 X 上提了一个关于 React Server Component 的问题

the only Client Component out of these is Toggle. it has state (isOn, initially false). it returns <>{isOn ? children : null}</>. what happens when you setIsOn(true)? A. Details gets fetched; B. Details appears instantly

function Note({ note }) {
  return (
    <Toggle>
      <Details note={note} />
    </Toggle>
  )
}

答案是 B,只有 54.6% 的人答对了。

可运行的代码如下,我们在 <Details />(是一个 Server Component)中,加上打印信息,用于观察该组件在服务器端何时构建的。我们刷新页面,在按钮点击之前就能在服务器端看到打印信息:

当点击按钮后,服务器端没有新的打印信息。我们可以知道,在页面加载的时候,这个 Server Component 已经发送到服务器了。所以应该选 B。

<Toggle /> 组件的核心逻辑是:

<div>
  {isOn ? props.children : <p>not showing children</p>}
</div>

这就会产生几个疑问🤔️。

  1. isOnfalse 的时候,子组件(<Details />)不应该渲染,实际情况是已经渲染好了,只是没有显示出来。

  2. isOntruefalse 再到 true,也没有销毁后重新创建(即没有「刷新」)。

  3. 假设 <Details /> 是需要登录才能查看,只有当用户点击按钮后且是登录用户,才渲染该组件,这样才安全。可是使用了 Server Components 之后,无论是否点击按钮,都渲染了组件,明显会导致问题。

要了解这是为什么,我们需要了解 RSC 的渲染逻辑。

RSC 渲染逻辑

首先我们需要有个核心前提:Client Components 不能依赖(import)Server Components,或者说,Server Components 的渲染不可以依赖 Client Components 的行为。

'use client'

import ServerComponents from './ServerComponents.jsx' // ❌

Server Components 只能成为 Client Components 的 Props/Children:

// OutletServerComponents.server.jsx
import ClientComponents from './ClientComponents.jsx'
import ServerComponents from './ServerComponents.jsx'

export function OutletServerComponents() {
  return (
    <>
      <ClientComponents>
        <ServerComponents />
      </ ClientComponents>

      <ClientComponents child={ServerComponents}>
      </ ClientComponents>
    </>
  )
}

所以我们可以知道,即如果有前提:组件树中有 Server Components,那组件树的根组件一定为 Server Components,否则作为根组件的 Client Components 则不可以依赖(import)Server Components,那同理,其他子组件也就只能是 Client Components 不可以依赖(import)Server Components,从而使整个组件树都是 Client Components ,与前提「组件树中有 Server Components」矛盾。

同时,React 规定:组件树上所有的 Server Components 都会一次性发送到浏览器(也可能先进行 SSR/SSG)。

所以,无论 <Toggle /> 如何渲染,<Details /> 都会一次性渲染好发送到前端:

为什么 React 会做出这样的设计决策?为了避免 Server Components 和 Client Components 的依赖,在页面加载时,就可以避免由于依赖而导致的请求 Waterfall。例如上述案例中,如果 <Details /> 依赖 <Toggle /> 而渲染,那么就会导致组件请求需要有先后,从而影响了性能,而这恰恰就是 RSC 要解决的问题之一。

如何刷新 RSC ?

是否 Server Components 到了浏览器后就无法更新了呢?是的,Server Components 由于在服务器端渲染,从而不会在浏览器中刷新(Rerender)。

但是 RSC 都是要结合路有使用的,组件树都是对应了路由,所以更新 Server Components 就是刷新页面路有获取新的组件树,需要注意的是,这里的「刷新」并不是指浏览器硬刷新,而是指前端软刷新,它不会导致前端的状态丢失,同时也不是一定要全屏刷新(整个页面的组件树刷新),可以是嵌套路由的刷新。

我们修改代码,每次点击按钮改变 searchParams 的方式,使得每次点击都重新渲染了 <Details />

除了使用路由刷新,对于 Next.js 也可以使用缓存刷新(Revalidating)的方式,但也同样是组件树级别。

如何条件渲染 RSC ?

对于统一页面(同一个组件树)来说,Server Components 从设计上就不允许条件渲染。但是我们可以通过 Dynamic Rendering 的方式,在 Server Components 中获取 Cookie、Headers 或者 SearchParams,进行判断,而渲染不同的内容。

我们在点击按钮的时候,在 Client Components (<Toggle />) 修改 searchParamsauth=trueauth=false,从而在 Server Components(<Details />) 中渲染不同的内容。

总结

现阶段,RSC 只能是整个组件树级别的刷新,并不能指定某些组件刷新。不同的路有对应着不同的组件树,所以 RSC 的刷新就是路由/缓存的刷新。但是我们可以通过 Dynamic Rendering 的方式,在 Server Components 中获取 Cookie、Headers 或者 SearchParams,进行判断,而渲染不同的内容。

注:组件树级别的刷新并不意味着,所有组件(包括 Client Components 和 Server Components)都会重新渲染,而还是遵循 React Reconciliation 的逻辑,只更新改变了的内容。