React 19 升级指南

前言

略掉了这些内容:

  1. Server action,因为新特性跟升级无关
  2. Form,因为社区表单方案足够优秀且成熟
  3. Class component 相关设计,比如 str ref 和老 ctx
  4. 老 react-dom 的设计,比如 render 和 findDomNode

只谈重点。

函数组件

[LOW] 新的 Ref 设计

暂时没有兼容性问题,可以缓步重构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// before
const Comp: FC<any> = forwardRef((props, ref) => {
return ...
})
Comp.displayName = "Comp"

// after
const Comp: FC<any> = ({ref}) => {
return ...
}

// 两者在新版 React 中等价,ref 默认会在 props 里,且不再需要 forwardRef

<Comp ref={ref} />

老写法估计还会兼容好几个大版本。

[LOW] 新的 Context 设计

暂时没有兼容性问题,可以缓步重构。

1
2
3
4
5
6
7
// before
const PageContext = createContext<{}>()
<PageContext.Provider />

// after
const PageContext = createContext<{}>()
<PageContext />
  • 提供了 codemod 工具快速迁移
  • 几乎无害,只有好处
  • 老写法用的太多,没有几个大版本也不可能删掉

[MEDIUM] Ref 的 cleanup

好实践,建议用起来

组件的 ref 很适合用来执行 side effect,但是此前一直没有一个用于清除此 side effect 的地方,所以 19 就加了一个。

1
2
3
4
5
6
7
8
9
10
<video
ref={(ref) => {
// some stuff
ref.current.play();
return () => {
// 像 useEffect 一样的 cleanup
ref.current.stop();
};
}}
/>
  • 对现有代码几乎没有影响
  • 是好实践,之后写 ref 的时候应该尽量去用

[LOW] 内置了 helmet

了解即可。

现在支持在 head 里插 titlemetascriptlink 了:

1
2
3
4
5
6
const Page = () => {
return <>
// 在任何地方都可以,会被自动插入到 head 里
<title>ok<title>
</>
}

但这只是给库作者的 building block,写业务的时候还是需要 react-helmet,所以就当这个功能没有吧。

[MEDIUM] use

好实践,但是用起来可能有些理解成本,建议观望一阵。

use 是一个用于消费 (读取) 资源 (resource) 的函数。注意,它只是函数,因此不用遵守 rule of hooks

React 社区中一直都有资源概念,但是讨论了很久都没得出答案,这里只谈 React 19 中所定义的 resource,即 ContextPromise

Context 的用例:

1
2
3
4
5
6
7
8
9
10
const SomeCtx = createContext<any>();

// 在任意 react 组件树内
const SomeComp = () => {
const a = use(SomeCtx);
for (const i = 0; i <= 10; i++) {
// 是的,它不用遵守 rule of hooks,所以只要在组件树里,什么地方都能用
const c = use(SomeCtx);
}
};

Promise 的用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
const fetchData = () => fetch("some url");

// 注意不能放在 component 里创建 promise,会让 react 无法处理
const promise = fetchData();

const SomeComp = () => {
const data = use(promise);
data instanceof Promise; // false
// use 在面对 Promise 的时候可以当作 await 来用
// 当 use 里面的 Promise 没有 resolve 的时候,use 会直接 throw 这个 promise,
// 此 Promise 被上层的 ErrorBoundary 捕获(try catch)之后,会让 Suspense 暂时显示 fallback
// 之后当 Promise resolve 的时候,这个组件被重新渲染,就始终拿到了 data 而非 Promise of data。
};
  • 这些东西在 18 的时候就慢慢在测了,基本上稳定了
  • useContext 短期内不会受影响
  • ⚠️ 注意:即使 use 可以在 server component 里用,也没法用来 consume context,因为 context 在 server 拿不到

新 Hooks

[LOW] useOptimistic

好实践,但是用起来可能有些理解成本,建议观望一阵。

字面意思,用于实现乐观更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function ChangeName({ currentName }) {
const [optimisticName, setOptimisticName] = useOptimistic(currentName);

const submitAction = async (formData) => {
const newName = formData.get("name");
setOptimisticName(newName);
const updatedName = await updateName(newName);
// 如果这里 throw 了,optimisticName 会变回原来的名字
};

return (
<form action={submitAction}>
<p>Your name is: {optimisticName}</p>
<p>
<label>Change Name:</label>
<input
type="text"
name="name"
disabled={currentName !== optimisticName}
/>
</p>
</form>
);
}

类型变更

[MEDIUM] useRef 必须传一个参数,否则类型不对

会影响项目中的类型检查,建议尽快迁移。

哪怕是 undefined 也要传一个。

规则大概可以用两条来概述:

  • 如果传 null 则是不可变的 ref
  • 否则要么传入初始值,要么传入 undefined,这样创建的是可变 ref
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Comp = () => {
// 不可变 ref 一般用于拿 ref
const immuRef = useRef<HTMLInputElement>(null)

// 可变 ref 之后必须传 undefined
const timerRef = useRef<number | undefined>(undefined)

useEffect(() => {
// ts 报错
immuRef.current = ...

// ts 不报错
timerRef.current = window.setTimeout(() => {})
})
}

[HIGH] ReactElement 类型

会影响项目中的类型检查,建议尽快迁移。

这个类型主要是用于遗留代码的,所以项目中所有用到的地方都是误用。

因此这个类型被限制的更严格了,我们的做法不是去适应它,而是赶紧弃用它。

正确的用法是:

1
2
3
4
5
6
7
8
9
10
type SomeCompProps = {
// 而非 ReactElement
children: ReactNode;
};

// 或者前者这个 children 可以直接从 React.PropsWithChildren 直接组合出来

type SomeCompProps = {
// ...other props
} & PropsWithChildren;

[HIGH] 全局的 JSX namespace 被删除

会影响项目中的类型检查,建议尽快迁移。

之后必须手动引入了。

1
2
// 必须手动引入
import type { JSX } from "react";

所有的项目中的 JSX.Element 几乎都是误用,可以跟上面一样可以换成 ReactNode,因此…


React 19 升级指南
http://blog.akr.moe/react-19-upgrade/
作者
Akara Chen
发布于
2025年3月14日
许可协议