
🎙️ 前言
React 19 出来已经快有一年了,公告 在 24 年 12 月就出了,然即使身为「升级狂魔」的我,也没有第一时间进行升级。只因为我深知 React 的每次大版本升级,都将在社区掀起一阵腥风血雨,从构建到组件库再到各种 Linter 都需要紧跟其脚步,只好一等再等。
其实主要是因为项目依赖了 Antd,而 Antd 5 并没有官方宣称支持 React 19,所以只好竭力按耐住渴望升级的欲火。昨天(2025/11/23),距离 React 19 第一版过去了将近整整一年(真墨迹...),看到 Antd 推了 6.0.0,时机终于到了。

React 19 升级公告的具体内容这里就不啰嗦了,已经有好多人聊过,个人比较关注的有以下几个:
forwardRef终于开始要退出历史舞台 🎉🎉🎉Context.Provider可以直接用Context代替- use,注意它不是
hook - Ref 相关的类型变更
🪁 如何升级
官方提供了 升级文档,提到升级工具:
shell
npx codemod@latest react/19/migration-recipe
以我的项目来讲,可能由于一直关注依赖升级,这一步并没有动任何代码。
作为 TS 项目,还需要:
shell
npx types-react-codemod@latest preset-19 ./path-to-app
然而它会给所有的 ReactElement 改成 ReactElement<any>,还得叫我替换还原回来。
👻 实际问题
接下来主要来讲一下我在升级中遇到的问题,主要是 TS 的类型问题。
forwardRef
终于可以用 props.ref 代替臭名昭著的 forwardRef,我认为这是最令人欣喜的新特性。被 forwardRef 折磨了这许久,终于 React 要干掉它了。
但对于 TS 代码来说,可能会遇到一些问题,比如我之前这么写(ref 没有写成可选参数):
ts
function MyComp(props: MyCompProps, ref: Ref<MyCompRef>): ReactElement;
升级 19 后会报错签名不符,如下图:

这种情况,如果是 NPM 包,建议先发个兼容包,改 ref 为可选即可(然后再发最低依赖为 19 的大版本包):
ts
function MyComp(props: MyCompProps, ref?: Ref<MyCompRef>): ReactElement;
useReducer
会写 TS 的人都知道,同一个方法,使用泛型的话,可以有多种不同的定义方式,好的定义能让开发者事半功倍。
useReducer 的类型定义就变了,其实升级文档里有提到,但心急的开发者估计会看漏,Better useReducer typings,而且 Codemod 工具并不会修正这里的问题。
所以对于类型定义完整的 TS 项目,会有冲击,导致构建失败。
ts
// React 18 的 `useReducer` 5 个重载
function useReducer<R extends ReducerWithoutAction<any>, I>(
reducer: R,
initializerArg: I,
initializer: (arg: I) => ReducerStateWithoutAction<R>
): [ReducerStateWithoutAction<R>, DispatchWithoutAction];
function useReducer<R extends ReducerWithoutAction<any>>(
reducer: R,
initializerArg: ReducerStateWithoutAction<R>,
initializer?: undefined
): [ReducerStateWithoutAction<R>, DispatchWithoutAction];
function useReducer<R extends Reducer<any, any>, I>(
reducer: R,
initializerArg: I & ReducerState<R>,
initializer: (arg: I & ReducerState<R>) => ReducerState<R>
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
function useReducer<R extends Reducer<any, any>, I>(
reducer: R,
initializerArg: I,
initializer: (arg: I) => ReducerState<R>
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
function useReducer<R extends Reducer<any, any>>(
reducer: R,
initialState: ReducerState<R>,
initializer?: undefined
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
// React 19 的 `useReducer` 只剩下了两种
function useReducer<S, A extends AnyActionArg>(
reducer: (prevState: S, ...args: A) => S,
initialState: S
): [S, ActionDispatch<A>];
function useReducer<S, I, A extends AnyActionArg>(
reducer: (prevState: S, ...args: A) => S,
initialArg: I,
init: (i: I) => S
): [S, ActionDispatch<A>];
主要区别是,19 把之前的 R(Reducer)拆成了 S(State) 和 A(Action)。
于是之前能跑通的构建失败了:

这种情况就得自己改了:

diff
-const [state, dispatch] = useReducer<TModelReducer, null>(reducer, null, createInitialState);
+const [state, dispatch] = useReducer<IModelState, null, [TModelAction]>(reducer, null, createInitialState);
注意,泛型第三个参数必须是元组,需要中括号括起来(个人认为他们可以优化更彻底些,不需要是元组)。
useRef
useRef 类型定义也变了,参数变成了必填,也会导致构建失败:
ts
// React 18
function useRef<T>(initialValue: T): MutableRefObject<T>;
function useRef<T>(initialValue: T | null): RefObject<T>;
function useRef<T = undefined>(initialValue?: undefined): MutableRefObject<T | undefined>;
// React 19
function useRef<T>(initialValue: T): RefObject<T>;
function useRef<T>(initialValue: T | null): RefObject<T | null>;
function useRef<T>(initialValue: T | undefined): RefObject<T | undefined>;
这意味着,需要将所有的空 useRef<T>() 加上默认值,哪怕传的是 undefined 也要写一下。
不过有一个好处,就是以前写成 useRef<T | null>(null) 的,现在只需要写 useRef<T>(null),见下图 React 18 和 19 下 IDE 类型推导的区别:

以前的 RefObject<T> 其实是现在的 RefObject<T | null>,现在的 RefObject<T> 是真的 RefObject<T>。
Ref 的各种类型
上面的 useRef 类型定义,你可能已经注意到 MutableRefObject 的地方都被换成了 RefObject。以下是两个版本跟 Ref 有关的类型定义剪影:
React 18:
ts
interface RefObject<T> {
readonly current: T | null;
}
interface MutableRefObject<T> {
current: T;
}
type Ref<T> = RefCallback<T> | RefObject<T> | null;
type ForwardedRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null;
React 19:
ts
interface RefObject<T> {
current: T; // 🎉 去掉 readonly
}
interface MutableRefObject<T> { // 💥 deprecated
current: T;
}
type Ref<T> = RefCallback<T> | RefObject<T | null> | null;
type ForwardedRef<T> = ((instance: T | null) => void) | RefObject<T | null> | null;
变化:
RefObject不再只读(即不再需要MutableRefObject),可以全局替换MutableRefObject→RefObjectForwardedRef虽然未标记deprecated,但从其实现来看,可以全局替换ForwardedRef→Ref
终于不再纠结那么多的 Ref 类型了。
☄️ 从 useImperativeHandle 再看 forwardRef
useImperativeHandle 是一个可能相对比较冷门的 Hook,但它真的很管用,可以让父组件调用子组件提供的方法,从而避免非常不 React 的写法(比如使用事件通知这种耦合性、不确定性比较强的写法)。
当一个组件复杂到一定程度的时候,为了避免 Props Drilling 的问题,我通常会用 Context 作为组件内全局数据状态管理的工具。所有与 props、state、effect 相关的逻辑通通在一个不涉及 UI 的「Model」层进行封装,UI 与 Model 之间惟一的桥梁就是 Hooks。

React ≤ 18 的绕脑写法
React 19 之前,这种模式在使用 useImperativeHandle 的时候会写出很绕的代码,为了 ref 能够最终有效,我需要至少写两次 forwardRef,而且比较晦涩,这让我苦不堪言(所以我之前对于写 useImperativeHandle 多少是有些惧怕的)。
React 18 及之前,可能需要这么写。
组件 Model + UI:
tsx
import {
ReactElement,
ForwardedRef,
forwardRef
} from 'react';
import Model, {
ModelProps,
ImperativeRef
} from '../model';
import Ui from '../ui';
export default forwardRef(function TheComponent(props: ModelProps, ref: ForwardedRef<ImperativeRef>): ReactElement {
return <Model {...props}>
<Ui ref={ref} />
</Model>;
});
UI 层:
tsx
import {
ReactElement,
ForwardedRef,
useImperativeHandle,
forwardRef
} from 'react';
import {
ImperativeRef,
useRefImperative
} from '../model';
export default forwardRef(function Ui(_props: unknown, ref: ForwardedRef<ImperativeRef>): ReactElement {
const imperativeRef = useRefImperative();
useImperativeHandle(ref, () => imperativeRef, [imperativeRef]);
return <... />;
});
很绕,是不是?需要在组件最外层,把 ref 传递到 UI 层,然后再在 UI 层 useImperativeHandle,为此,原本可以无参的 UI 组件,甚至还要写个 _props: unknown。
但凡脑子不好一点都想不出这么绕的法子 😳。但为了能够在正确的位置使用 useImperativeHandle,这的确是我能想到的比较「高明」的办法了。
React 19 的写法
React 19 变得相当简单,forwardRef 全部干掉后,改造 Model 内部的 useRefImperative,使 Model 更内聚:
ts
import {
useImperativeHandle
} from 'react';
import {
IImperativeRef
} from '../types';
import useModelProps from './_use-model-props';
export default function useRefImperative(): void {
const {
ref
} = useModelProps();
useImperativeHandle(ref, (): IImperativeRef => ({
...
}), [...]);
}
使用的话,就只需要在 UI 组件,简单调用一下 useRefImperative 即可:
tsx
import {
ReactElement
} from 'react';
import {
useRefImperative
} from '../model';
export default function Ui(): ReactElement {
useRefImperative();
return <... />;
});
至此,我们再也不需要惧怕写 useImperativeHandle 了。
🐌 组件库怎么办
React 目前尚未对 forwardRef 标记 deprecated,但也说不久的将来会这么做。
组件库就会比较尴尬,考虑到存量应用,组件库没法在升级 React 19 后直接废弃 forwardRef。也就是说,组件库一时间还没办法享受弃用 forwardRef 的带来的红利,除非组件库启用大版本不兼容升级。
拿 Antd 举例,Antd 5 支持的最小 React 版本是 16.9.0,Antd 6 却并没有直接提升到 React 19,而仅仅声明最小 React 版本是 18。所以,它的实现依然使用了 forwardRef(而且只能用 forwardRef)。

🪭 总结
这次的升级,BREAK CHANGE 不多,总的来说比较平滑顺畅,我用了 1 天的时间升级完了五个项目,总结下来就这些:
- 所有的
forwardRef可以改成props.ref(如果是 NPM 包,需要先兼容,后发大版本) useReducer的类型为 BREAK CHANGE,但也只需要改类型,修改相对简单useRef<T>()传空将导致构建失败,可改成useRef<T>(null)或useRef<T>(undefined)MutableRefObject→RefObjectForwardedRef→RefContext.Provider→Context- 组件库,除非声明支持最小 React 版本为 19,不要杀
fowardRef(也不要用props.ref)