likes
comments
collection
share

React 从 RSC 到 v19 的主要新范式介绍及应用场景分析

作者站长头像
站长
· 阅读数 80
React 从 RSC 到 v19 的主要新范式介绍及应用场景分析

React v16.8 发布 hooks 以来,已经过去足足 5 年时间(2024),我们似乎已经很久没有学习 React 新的范式并应用到开发中了。这其中主要原因可能有两点:

  • Hooks (也被称为 signals (tc39 proposal)) 范式已经是响应式编程的终点,在每种框架甚至原生 JS 都将要实现它后,剩下的只有打补丁工作
  • React 团队的重心迁移到 SSR 上,新的范式全是从 SSR 的场景中衍生的,在商业利益上也和 Vercel (NextJS) 更加绑定。越来越脱离群众,以致社区也出现了分歧。

本文将从需求场景的演进历史出发介绍 React 19 主要新范式的源流,相信看完后会让你对 hooks 版本之后的 React 演进路线有更全面的认知,提升姿势水平,对社区的分歧也会有自己的判断。

本文全部代码都有 codesandbox 分享,推荐优先打开并 fork 代码体验。

示例一 NextJS SSR + postgreSQL +Shadcn

示例二 React 19 (canary) SPA

nextjs: react server component (RSC) + suspense

RSC ( react server component) 是 React 的新SSR 解决方案,也是 React 团队目前主要的业务发展方向,尽管社区不太买账。

overreacted.io/the-two-rea…

React Dan Abramov 的论点是将 ui=f(state) 这种范式拿到 server 端是有用的,所以我们需要 RSC 。

社区普遍的态度是不希望被强加 SSR,比如各种 Web App 场景。对 SSR 的强推说明 React 和 Vercel 的利益绑定太深。

所以社区出现了分歧, 许多 KOL 不再 follow React 团队的方向,比如 tanstack 出了 react-router ,对 Nextjs 取其精华去其糟粕(狗头) tanstack.com/router/late…

抛开这些不谈, 在必须 SSR 的场景下, RSC 的确带来了比较好的开发体验 ,让 SSR 组件的开发体验和 CSR 几乎没有差别,语法上没有任何区别。暴论:React 团队永远最在乎开发体验,其实不在乎性能和渲染损耗(都是假装的)。

async function getData() {
  const res = await fetch('https://api.example.com/...')
  if (!res.ok) {
    throw new Error('Failed to fetch data')
  }
  return res.json()
}
// 将 Function Compoent 语义化的当做普通函数对待
export default async function Page() {
  // 内联 await
  // Promise pending 时打到组件树上层的 loading <Suspense>
  // throw Error 时打到 Error boundary <Suspense>
  const data = await getData()
  // 限制也很明显 ,RSC 内不能有状态 ( 如 useState ) 否则 State 变化 await 也会重执行
  // 表现为state变一下 loading 一下,所以 RSC 预期只初次加载时执行一次(如刷新页面时)。
  return <main></main>
}

async FC 内联 await ,配合 suspense 边界,在 Nextjs 示例里这一套能很优雅的实现 SSR 的流式 UI。

顺便了解一下社区火热但一直没用过的 headless ui shadcn/ui,在 next 里天然适配。

ui.shadcn.com/docs/compon…

themes.fkaya.dev/

npx shadcn-ui@latest add input

简述一下就是不用npm装,组件代码直接拷贝到项目约定文件夹里,但需要项目先配好 tailwind ,组件所有样式全用 tailwind 实现,所以组件库不带样式文件即 headless。同时也是 React 库,基于 Radix UI , 用户体验也是开箱即用。

如果你没用过新版本的 NextJS 和 RSC ,推荐新建项目并走到 Loading and Streaming 这一步,预计十分钟左右。

nextjs.org/docs/app/bu…

React 从 RSC 到 v19 的主要新范式介绍及应用场景分析

同构范式:<form> + action

站在 SSR 的角度,必须要区分哪些逻辑在服务端执行, 哪些逻辑在客户端执行,RSC 发明的范式如下:

  • 对于component (Function Component), 默认为 server component 服务端渲染即 RSC,不能引入 useState 这种,即不能有状态,如果有状态则 state 变一下 FC 会重新 Render 一下不符合预期;如需使用 client component 则 tsx 文件头用 "use client"标识
  • 对于普通函数ts 文件,默认客户端执行,如需要服务端执行(如数据库操作),ts 文件头用 "use server" 标识

于是出现问题: 典型场景-表单提交,表单有状态,需要"use client" ,而提交动作在同构上下文中一般直接ORM 调库,则这块逻辑必须放进另一个文件,标识"use server"

codesandbox 示例,页面url是 /dashboard : codesandbox.io/p/devbox/po…

use client Form 组件

"use client";
....
import { submitName } from "./handleSubmit";
export function ProfileForm() {
  function onSubmit(values: z.infer<typeof formSchema>) {
    // server action here
    submitName(values.username);
  }
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} >
        <FormField
          render={({ field }) => (
            <FormItem>
              <FormControl>
                <Input placeholder="shadcn" {...field} />
              </FormControl>
            </FormItem>
          )}
        />
        <Button type="submit">Add</Button>
      </form>
    </Form>
  );
}

use server ts 文件

"use server";
import { user } from "@/db/schema";
import { db } from "@/db";
import { revalidatePath } from "next/cache";

export const submitName = async (name: string) => {
  if (name === "del") {
    await db.delete(user);
  } else {
    await db.insert(user).values({ name }).execute();
  }
  revalidatePath("/dashboard");
};

是否有办法将一个表单页面作为 Server component ,此时 server action 和表单 FC 在一个文件里? React 给出的解决方案是 react-dom 新增范式: <form> dom 元素的action : react.dev/reference/r…

它不仅是只用于 form , 设计上来看它希望这种范式作为全部状态提交的最佳实践,不再需要一个 Input 一个受控的模式,而是至少套一个 form 这样使用。我们可以从 shadcn 文档看出端倪 ui.shadcn.com/docs/compon… input 组件不再给出单独受控示例,只有结合 form 的使用示例

简要来说就是 react-dom 给原生 <form> 元素封装了受控逻辑通过 action 回调拿到值,所以即使 form 自身有状态和变化,但不会引起使用 form 的父组件的重执行。

如下组件没有任何状态,只会执行一次,form 内状态变化不会影响外层:

export default function Search() {
  function search(formData) {
    const query = formData.get("query");
    alert(`You searched for '${query}'`);
  }
  return (
    <>
      <h3>formAction</h3>
      <form action={search}>
        <input name="query" />
        <button type="submit">Search</button>
      </form>
    </>
  );
}

这种模式非常适合 RSC ,可以在同一个文件里既让 Form 组件作为 Server 组件,同时还能直接调 Server action

示例 nextjs + postgreSQL 同构调库,页面 url 是 /form-actions:

codesandbox.io/p/devbox/po…

import { user } from "@/db/schema";
import { db } from "@/db";
export default async function Home() {
  return (
    <main>
      <div>
        <form
          //** form 既是 Server 组件又能调 Server action**
          action={async (formData: FormData) => {
            "use server";
            const name = formData.get("name") as string;
            if (name === "del") {
              await db.delete(user);
            } else {
              await db.insert(user).values({ name }).execute();
            }
            revalidatePath("/form-actions");
          }}
        >
         
        </form>
      </div>
    </main>
  );
}

你可能会质疑在前端代码里调用数据存储是否有点草台,可以看下面章节。

边缘计算: Serverless 执行 SSR 首屏渲染并缓存,分布式应用

SSR 的首屏逻辑可以不部署在单点服务器,而部署到 serverless 节点运行,使用 serverless 提供的 KV (key value)存储 。nextjs 和 vercel 的利益绑定体现在此。

help.aliyun.com/zh/dcdn/use…

如何缓存POST, 对body摘要

developers.cloudflare.com/workers/exa…

你可能会问这个 serverless 函数实例里的 KV 存储是单点的还是分布式的, 是分布式的,比较涨姿势。

help.aliyun.com/zh/dcdn/use…

虽然 Serverless 架构的价值存在争议,比如有人质疑科技公司就是不时的创造新概念让企业来买单,但 Vercel 这一套就是当前最先进的 Web 无服务架构,并且做到了最佳易用性。

它也契合当前的互联网安全要求,很自然的做到应用层和存储层都只存在于各国的数据中心。

aws.amazon.com/cn/blogs/ch…

这一套也被称为 Jamstack (JavaScript、APIs、Markup) 架构,只适合一些内容更新不太频繁的网站(比如新闻、电商、文档)。它不适合 Feeds 流、聊天室、论坛、个性化推荐这样高度动态化的网站,以及邮箱、编辑器这样偏重型的 Web App。

很多人会问它和传统的 PHP, JSP , WordPress 比的优越性在哪。传统的一体化架构还是单点部署,而 Serverless 架构的整个应用层是纯分布式部署运行的。即使国内流量入口都是 APP,但 APP 内的页面也能接入这一套,实现最理想的云服务架构。

zhuanlan.zhihu.com/p/281085404

当然你可能又会问,如果回归农业文明,纯静态资源分布式部署到 CDN 是不是也算?比如 Dapp (en.wikipedia.org/wiki/Decent…) , 或者 SPA 也是纯静态资源,直接前端发 SQL连库,是不是也算应用层分布式了?好像也是的,但还是缺少 APP 必要的少许 Server 端能力,比如连库的密码,Oauth2 的secretId 总不能写在前端吧, 所以简单的 Server 能力由 Serverless云函数补齐。ToC 业务的首屏秒开也是典型业务场景。

React 19

React 19 公测版最主要的新 Hooks 是 useActionStateuse

  • useActionState 是将 <form> action 这种新范式引入客户端逻辑的产物。

  • 上文 async FC 内直接 await 也是 RSC 的特性, use() 也是将其引入客户端逻辑的结果

React 19 use()

这个相对来说还比较有用,但不是给业务代码使用的。

client component 不支持 async FC 里直接 await ,则 use() 用法就相当于 await, 和 RSC 里表现一致

codesandbox: codesandbox.io/p/sandbox/r…

import { use, Suspense, useState } from "react";
import { fetcher } from "./fetcher";

//client component 不支持 async
function Comments() {
  // 类似于 RSC 中 async  FC 里直接 await
  // fetcher(1) resolve 前打到组件树 Suspense
  // 作为 use 参数的 promise 不能 FC 内联创建
  const comments = use(fetcher(1));
  return (
    <>
      <p>RenderContentUse</p>
      <p>{comments?.title}</p>
    </>
  );
}

export function RenderContentUse() {
  const [name, setName] = useState("");
  // 同样 name 变一下  Comments loading 一下, 和 RSC 一致
  return (
    <>
      <h3>Render Content using React 19</h3>
      <Suspense fallback={<div>Loading...</div>}>
        <input value={name} onChange={(event) => setName(event.target.value)} />
        <Comments />
      </Suspense>
    </>
  );
}


总得来说还需要处理额外的 cache 逻辑 ,也会报 warning

Warning: A component was suspended by an uncached promise. Creating promises inside a Client Component or hook is not yet supported, except via a Suspense-compatible library or framework.

也直接告诉你了 uncached , 不要直接用,推荐用 library or framework,或直接使用 SWR 这种库

React 18 fiber 功能 useTransition

我们可以回溯性的看下 React 18 最重要的 fiber hooks useTransition ,使用它实现查询操作

codesandbox 示例 useTransition: codesandbox.io/p/sandbox/r…

import { useTransition, useState } from "react";

import { fetcher } from "./fetcher";
export function SearchContentUseTransition({}) {
  const [name, setName] = useState("");
  const [title, setTitle] = useState("");
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();
  const handleSubmit = async () => {
    // fiber 低优先级更新用 startTransition 包裹
    // https://react.dev/reference/react/useTransition
    startTransition(async () => {
      const { title, body } = await fetcher(name);
      setTitle(title);
      if (!title) {
        setError(true);
      } else {
        setError(false);
      }
    });
  };

  return (
    <div>
      <h3>Search Content using React 18</h3>

      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        Search
      </button>
      {isPending && <p>pending...</p>}
      {title && <p>{title}</p>}
      {error && <p>404 not found</p>}
    </div>
  );
}

React 19 功能 useActionState

将上面 useTransition 例子改写为 useActionState 实现:

codesandbox : codesandbox.io/p/sandbox/r…

import { useTransition, useState, useActionState } from "react";

import { fetcher } from "./fetcher";

export function SearchContentUseActionState() {
  const [payload, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      console.log("previousState", previousState);
      const name = formData.get("name");
      const payload = await fetcher(name);
      return payload;
    }
  );

  return (
    <>
      <h3>Search Content using React 19</h3>
      <form action={submitAction}>
        <input type="text" name="name" />
        <button type="submit" disabled={isPending}>
          Search
        </button>
        {isPending && <p>pending...</p>}
        {payload?.title && <p>{payload.title}</p>}
        {!payload?.title && <p>404 not found</p>}
      </form>
    </>
  );
}

  • 从用户角度看 useActionState 就是把 useTransition 多包了一层,它有什么价值?
    • react.dev/reference/r… useActionState 就是为client component 提供 <form> action 范式的 adaptor
    • 关键点在于 react.dev/reference/r… 这个不只是扩展 <form>, 而是相当于出了一整套数据 post 操作的标准,同时适用于 Client 和 Server 的写法范式,即同构范式
  • 反题:现在有几个 UI 库是把 <form> 元素直接暴露在业务代码里使用的 (除了 NextJs 自己的 shadcn/ui)?如果我不写 SSR,纯客户端逻辑里使用这个 action 有什么收益?引入新的范式复杂度,也没有解决额外的问题?
    • 无力反驳,比 useTransition 更难用。从React 角度看可能是支持了前后端同构的范式,但从用户角度看成本高收益小基本没用,可见前途更渺茫。还是回到最初的问题,我们是否需要 SSR ,如果不需要则没什么价值。
    • 挽尊的来看,也可以说吸取了 useTransition 的教训,这些新 Hooks 都是给库用的,并不是给用户用的。比如 antd 这种可以重构自己的 <form> , react-query 可以封装新的 fiber hooks

还有两个新 hooks useFormStatus 和 useOptimistic ,比较简单,适合和 form action 这个范式配套使用,可以看下面文档,里面也有 codesandbox 示例。

总结

经过上文,可以预期的是如果不写SSR/不写工具库,这些新 hooks 范式还是不会影响到我的开发。即使是工具库作者,是否大规模重构来 follow 这些范式也是值得斟酌的,毕竟收益有限。

这些新 Hooks 的确有些挽尊式更新的意思,就是将 SSR 场景的各种新范式更新进了 React。只能说 v18 之后的 React 新功能不太应该放进 'react' 这个包里,叫 'react-server' 或 'react-next' 更合适。

总的来说, useTransition / useActionState 这些 hooks 都和 React 自己发明的范式太耦合,和 Signals (tc39 proposal) 这种通用范式距离越来越远。没有用户会无端在业务代码里添加额外的范式复杂度,它基本上没有收益(如果不用 SSR),只能是将项目和 React (现在是 NextJS/Vercel) 本身的利益捆绑的越来越深

写文章本身也是一个学习的过程,也请读者能指出文章中的疏忽错漏之处。如果本文对你有所帮助,欢迎点赞收藏。

转载自:https://juejin.cn/post/7363650007256252431
评论
请登录