|
| 1 | +# React19 |
| 2 | + |
| 3 | +4月25日,React官方宣布React19在NPM上推出,可以先行下载使用。 |
| 4 | +正好此博客也是NextJS搭建的,尝试一下React19带来哪些变化。 |
| 5 | + |
| 6 | +## 准备 |
| 7 | + |
| 8 | +官方的建议是先安装稳定版本的React18.3,以在更新到19前发现一些潜藏的问题。 |
| 9 | +检查无误后,就可以安装React-19(还处于BETA)使用新API。 |
| 10 | +[React-v19的升级指南](https://react.dev/blog/2024/04/25/react-19-upgrade-guide#typescript-changes) |
| 11 | + |
| 12 | +## 变化 |
| 13 | + |
| 14 | +React在沉寂一长段时间后,听取社区的意见,改掉许多开发痛点以及优化框架。重磅推出了React19 |
| 15 | + |
| 16 | +**重大的改变有以下几点:** |
| 17 | + |
| 18 | +- React-Compiler:帮助开发人员自动优化页面,减少甚至抛弃useMemo和useCallback。 |
| 19 | +- Actions:新的`<form>`标签以及配套的Hooks表单操作。 |
| 20 | +- New Hooks:新的增强操作钩子如`use()`等等。 |
| 21 | +- Document Metadata:现在可以直接在单个组件里编写Meta数据。 |
| 22 | +- Web components:React 代码现在将使我们能够合并 Web 组件。 |
| 23 | + |
| 24 | +### React-Compiler |
| 25 | + |
| 26 | +ReactCompiler可以说是19里最让人激动的东西,它是一个新的编译器,用于帮程序员优化React代码。 |
| 27 | +比如说先前的useMemo,useCallback等等这一系列的优化钩子,不说使用起来麻烦,使用不当甚至还会造成负优化。于是React推出了Compiler直接自动处理代码,避免了负优化的现象。 |
| 28 | + |
| 29 | +简单来说,Compiler做到的事情就是将组件中每个元素,每一个函数都进行缓存,只有当发生变化的时候才会重新缓存,不然就接着使用。 |
| 30 | +本文主要是说使用而非原理,具体可以查看这篇文章[我已彻底拿捏 React Compiler](https://mp.weixin.qq.com/s/7XFn56O3ia5vHPqSaeo6GA) |
| 31 | + |
| 32 | +对于React-Compiler的启用,我们首先要对我们的项目做一个检测。 |
| 33 | + |
| 34 | +```shell |
| 35 | +npx react-compiler-healthcheck |
| 36 | +``` |
| 37 | + |
| 38 | +> 该脚本主要用于检测 |
| 39 | +> 1、项目中有多少组件可以成功优化:越多越好 |
| 40 | +> 2、是否使用严格模式,使用了优化成功率更高 |
| 41 | +> 3、是否使用了与 Compiler 不兼容的三方库 |
| 42 | +
|
| 43 | +这个框架的检测效果如下: |
| 44 | +<img src="/imgs/React19-BETA/test.png" alt="react-server-components" /> |
| 45 | +对于不同的框架使用Compiler的方法不同,Next启用Compiler需要先下载Next-canary版以及babel-plugin-react-compiler |
| 46 | + |
| 47 | +```shell |
| 48 | +npm install next@canary babel-plugin-react-compiler |
| 49 | +``` |
| 50 | + |
| 51 | +然后在`next.config.js`: |
| 52 | + |
| 53 | +```js |
| 54 | +// next.config.js |
| 55 | +/** @type {import('next').NextConfig} */ |
| 56 | +const nextConfig = { |
| 57 | + experimental: { |
| 58 | + reactCompiler: true, |
| 59 | + }, |
| 60 | +}; |
| 61 | + |
| 62 | +module.exports = nextConfig; |
| 63 | +``` |
| 64 | + |
| 65 | +便可以启动compiler对项目进行优化。 |
| 66 | +成功优化后,可以在React-dev-tool就会看到Memo星星。 |
| 67 | +<img src="/imgs/React19-BETA/compiler.png" alt="react-server-components"/> |
| 68 | + |
| 69 | +> 值得一说的是Compiler还在测试中,存在不少问题,比如与i18n的客户端组件存在一些冲突等,所以有待观望 |
| 70 | +
|
| 71 | +### New Hooks |
| 72 | + |
| 73 | +React19更新许多新的Hooks,包括`use()`,`useOptimistic()`,`useFormStatus()`,绝大多是都是为了Action也就是`<form>`标签所适配的。 |
| 74 | + |
| 75 | +因为没用到表单,所以我只先使用`use()`。 |
| 76 | + |
| 77 | +use用于获取资源的值,比如说Promise或者Context。和其他钩子不同,它可以在if语句中使用。 |
| 78 | + |
| 79 | +他的具体原理如下(取自官方文档): |
| 80 | + |
| 81 | +> 当使用 Promise 调用时, use API 会集成 和 Suspense 错误边界。当传递给的 use Promise 处于挂起状态时,组件调用 use 将挂起。如果调用 use 的组件包装在 Suspense 边界中,则将显示回退。 解析 Promise 后,Suspense 回退将替换为使用 use API 返回的数据呈现的组件。如果传递给 use 的 Promise 被拒绝,则将显示最近的错误边界的回退。 |
| 82 | +
|
| 83 | +基础用法如下: |
| 84 | + |
| 85 | +```js |
| 86 | +const value = use(resource); |
| 87 | +``` |
| 88 | + |
| 89 | +**值得注意的是:** |
| 90 | + |
| 91 | +- use 必须在 Component 或 Hook 中调用 API。 |
| 92 | +- 首选在服务器组件中创建 Promise 并将其传递给客户端组件,而不是在客户端组件中创建 Promise。在客户端组件中创建的 Promise 会在每次渲染时重新创建。从服务器组件传递到客户端组件的 promise 在重新渲染时是稳定的。 |
| 93 | +- 像useContext一样, use(context)总是在调用它的组件上方寻找最接近的上下文提供程序。它会向上搜索,并且不考虑要从中调用 use(context) 的组件中的上下文提供程序。 |
| 94 | +- 将 Promise 从服务器组件传递到客户端组件时,其解析值必须可序列化才能在服务器和客户端之间传递。函数等数据类型不可序列化,并且不能是此类 Promise 的解析值。 |
| 95 | + |
| 96 | +在此项目中的使用如下: |
| 97 | + |
| 98 | +```jsx |
| 99 | +"use client"; |
| 100 | +import { Suspense } from "react"; |
| 101 | +import { GhostPointer } from "./GhostPointer"; |
| 102 | +import { MyTypeWrite } from "./TypeWrite"; |
| 103 | +import { DailyWord } from "@/utils/getDailyWord"; |
| 104 | +import ErrorBoundary from "./ErrorBoundary"; |
| 105 | + |
| 106 | +export function Banner({ |
| 107 | + language, |
| 108 | + isGetDailyWord, |
| 109 | + wordsFetch, |
| 110 | +}: { |
| 111 | + wordsFetch?: Promise<DailyWord>; |
| 112 | + language: string; |
| 113 | + isGetDailyWord: boolean; |
| 114 | +}) { |
| 115 | + return ( |
| 116 | + <ErrorBoundary |
| 117 | + fallback={ |
| 118 | + <GhostPointer> |
| 119 | + <span |
| 120 | + style={{ |
| 121 | + display: "flex", |
| 122 | + lineHeight: "250px", |
| 123 | + fontSize: "4rem", |
| 124 | + justifyContent: "center", |
| 125 | + color: "white", |
| 126 | + }} |
| 127 | + > |
| 128 | + ⚠️Something went wrong |
| 129 | + </span> |
| 130 | + </GhostPointer> |
| 131 | + } |
| 132 | + > |
| 133 | + <Suspense |
| 134 | + fallback={ |
| 135 | + <GhostPointer> |
| 136 | + <span |
| 137 | + style={{ |
| 138 | + display: "flex", |
| 139 | + lineHeight: "250px", |
| 140 | + fontSize: "4rem", |
| 141 | + justifyContent: "center", |
| 142 | + color: "white", |
| 143 | + }} |
| 144 | + > |
| 145 | + Loading... |
| 146 | + </span> |
| 147 | + </GhostPointer> |
| 148 | + } |
| 149 | + > |
| 150 | + <GhostPointer> |
| 151 | + <MyTypeWrite |
| 152 | + language={language} |
| 153 | + wordsFetch={wordsFetch} |
| 154 | + isGetDailyWord={isGetDailyWord} |
| 155 | + /> |
| 156 | + </GhostPointer> |
| 157 | + </Suspense> |
| 158 | + </ErrorBoundary> |
| 159 | + ); |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +利用ErrorBoundary以及Suspense包裹目标组件,在解析中以及解析失败后有相对应的UI呈现。 |
| 164 | +再从服务端传入wordsFetch函数再进行use解析。 |
| 165 | + |
| 166 | +```tsx |
| 167 | +//layout |
| 168 | +import { Banner } from "../components/Banner"; |
| 169 | +import { getDailyWord } from "@/utils/getDailyWord"; |
| 170 | + |
| 171 | +export default async function FrontLayout({ |
| 172 | + children, |
| 173 | + params: { language }, |
| 174 | +}: { |
| 175 | + children: React.ReactNode; |
| 176 | + params: { language: string }; |
| 177 | +}) { |
| 178 | + const wordsFetch = getDailyWord(); |
| 179 | + return ( |
| 180 | + <div className="flex flex-col items-center"> |
| 181 | + <div className="w-[100vw]"> |
| 182 | + <Banner |
| 183 | + language={language} |
| 184 | + isGetDailyWord={true} |
| 185 | + wordsFetch={wordsFetch} |
| 186 | + ></Banner> |
| 187 | + </div> |
| 188 | + <section className="w-full">{children}</section> |
| 189 | + </div> |
| 190 | + ); |
| 191 | +} |
| 192 | +//TypeWrite |
| 193 | +("use client"); |
| 194 | +import { usePathname } from "next/navigation"; |
| 195 | +import { ReactTyped } from "react-typed"; |
| 196 | +import { getDailyWord } from "@/utils/getDailyWord"; |
| 197 | +import { Suspense, use, useState } from "react"; |
| 198 | +import { DailyWord } from "@/utils/getDailyWord"; |
| 199 | +import { splitPathname } from "@/utils/dealPathname"; |
| 200 | +import { useTranslation } from "@/app/i18n/client"; |
| 201 | +export function MyTypeWrite({ |
| 202 | + language, |
| 203 | + isGetDailyWord, |
| 204 | + wordsFetch, |
| 205 | +}: { |
| 206 | + language: string; |
| 207 | + isGetDailyWord: boolean; |
| 208 | + wordsFetch?: Promise<DailyWord>; |
| 209 | +}) { |
| 210 | + let word; |
| 211 | + const pathName = usePathname(); |
| 212 | + const title = splitPathname(pathName); |
| 213 | + const { t } = useTranslation(language, "translations"); |
| 214 | + if (isGetDailyWord && wordsFetch) { |
| 215 | + const words = use(wordsFetch); |
| 216 | + word = language === "zh-CN" ? words.note : words.content; |
| 217 | + } |
| 218 | + return ( |
| 219 | + <ReactTyped |
| 220 | + strings={!word ? [t(title)] : [word]} |
| 221 | + typeSpeed={50} |
| 222 | + style={{ |
| 223 | + display: "flex", |
| 224 | + lineHeight: "250px", |
| 225 | + fontSize: "4rem", |
| 226 | + justifyContent: "center", |
| 227 | + color: "white", |
| 228 | + }} |
| 229 | + /> |
| 230 | + ); |
| 231 | +} |
| 232 | +``` |
| 233 | + |
| 234 | +最终效果可见博客首页(代码存放于github) |
| 235 | + |
| 236 | +### 其他 |
| 237 | + |
| 238 | +React19的更新远不止于此,目前我只用上这两个方法。 |
| 239 | + |
| 240 | +还有关于乐观更新,表单操作等等的钩子尚未使用。 |
| 241 | + |
| 242 | +以及令人诟病的Ref转发也得到了优化。 |
| 243 | + |
| 244 | +报错提示更人性化等等等等。 |
| 245 | + |
| 246 | +在未来会慢慢投入使用,投入生产。 |
| 247 | + |
| 248 | +[React19官方博客](https://react.dev/blog/2024/04/25/react-19) |
| 249 | + |
| 250 | +[关于USE](https://react.dev/reference/react/use) |
| 251 | + |
| 252 | +[关于Compiler](https://react.dev/learn/react-compiler#) |
0 commit comments