我们将 MUI X 迁移到 React 19 的过程
作为一套流行的 React UI 组件的维护者,我们计划在 React 19 稳定版本发布后尽快将我们的库代码库迁移到 React 19,该版本于 2024 年底发布。事实证明,这是一项重大的 undertaking,需要仔细的计划和执行。
本文介绍了我们的迁移策略,以及我们如何解决在此过程中遇到的一些关键问题。我们希望这篇文章对其他也需要在其代码库中支持 React 的两个版本的开发者有所帮助。
迁移策略
对我们来说,继续支持旧版本的 React 至关重要,因为我们的许多用户依赖于现有的 React 18 应用程序,这些应用程序无法立即迁移。
我们理解升级主要版本需要时间和计划,尤其是在大型生产应用程序中,我们希望支持用户的迁移时间表。与此同时,我们不希望阻止 React 19 的早期采用者将最新的 React 版本与我们的软件包一起使用。
因此,我们分两个阶段进行迁移
- 首先,我们在保持代码库在 React 18 上的同时,添加了 React 19 兼容性
- 然后,我们将整个代码库迁移到 React 19,同时保持与以前 React 版本的兼容性
这缩短了发布与 React 19 兼容的软件包版本所需的时间。
阶段 1:添加 React 19 兼容性
我们的第一步是查看 React 19 中的破坏性更改列表。
我们很幸运,不需要在源代码中进行太多更改,但是由于与严格模式和错误报告相关的修改,我们的测试必须进行大量更改。这些更改导致了我们的 spies 的不同调用计数和不同的控制台输出,因此我们必须根据 React 主要版本来预期不同的值。
@mui/internal-test-utils
提供了一个导出 reactMajor
,用于提取测试中使用的 React 版本的主要版本。我们正在使用它来有条件地设置测试期望。
错误消息修改
const errorMessage1 = 'MUI X: Could not find the animation ref context.';
const errorMessage2 =
'It looks like you rendered your component outside of a ChartsContainer parent component.';
const errorMessage3 =
'The above error occurred in the <UseSkipAnimation> component:';
const expectedError =
reactMajor < 19
? [errorMessage1, errorMessage2, errorMessage3]
: `${errorMessage1}\n${errorMessage2}`;
严格模式修改
// Spy call count
// 1x during state initialization
// + 1x during state initialization (StrictMode)
// + 1x when sortedRowsSet is fired
// + 1x when sortedRowsSet is fired (StrictMode) = 4x
// Because of https://reactjs.ac.cn/blog/2024/04/25/react-19-upgrade-guide#strict-mode-improvements
// from React 19 it is:
// 1x during state initialization
// + 1x when sortedRowsSet is fired
const expectedCallCount = reactMajor >= 19 ? 2 : 4;
性能问题
在 React 19 中,您可以将 ref
作为函数组件的 prop 访问。不再需要 forwardRef
。这为我们创建了一个 问题,这个问题是由我们的一位社区成员发现的。
因为 ref
现在也是一个 prop,所以在 ref prop 之后扩展 props 可能会覆盖 ref。ForwardRef
组件上 ref
prop 的存在——即使是 undefined
——也会使组件 props 在引用上不稳定,从而破坏下游的 memoization。
为了解决这个问题,我们添加了一个 forwardRef
shim,它在类型级别上强制执行正确的 prop 排序。
// Compatibility shim that ensures stable props object for forwardRef components
// Fixes https://github.com/facebook/react/issues/31613
// We ensure that the ref is always present in the props object (even if that's not the case for older versions of React) to avoid the footgun of spreading props over the ref in the newer versions of React.
export const forwardRef = <T, P = {}>(
render: React.ForwardRefRenderFunction<T, P & { ref: React.Ref<T> }>,
) => {
if (reactMajor >= 19) {
const Component = (props: any) => render(props, props.ref ?? null);
Component.displayName = render.displayName ?? render.name;
return Component as React.ForwardRefExoticComponent<P>;
}
return React.forwardRef(
render as React.ForwardRefRenderFunction<T, React.PropsWithoutRef<P>>,
);
};
该 shim 提供了两个关键好处
- 类型安全 - 如果 props 扩展不正确,TypeScript 将发出警告
- 向前兼容性 - 使用 shim 的组件将在所有受支持的 React 版本中正常工作
这是我们的实现方式
// Before
const GridRoot = React.forwardRef((props, ref) => {
const state = useGridState();
return <div ref={ref} {...props} {...state} />;
});
// After
const GridRoot = forwardRef((props, ref) => {
const state = useGridState();
return <div {...props} {...state} ref={ref} />;
});
阶段 2:迁移到 React 19
在确保兼容性之后,我们开始致力于将代码库迁移到 React 19。这包括
- 将所有软件包依赖项更新到其与 React 19 兼容的版本(包括将文档网站迁移到 Next.js 15)
- 迁移测试实用程序以与 React 19 一起工作
- 确保所有组件都与新的 React 19 功能一起工作
- 更新 CI 以使用 React 18 运行测试
- 使用
RefObject
更新 React 19 的类型引用,并使用MutableRefObject
更新早期版本的类型引用
此阶段最大的变化是围绕 useRef()
钩子更新。Data Grid 组件中的 apiRef
必须仅针对 React 19 从 MutableRefObject
更新为 RefObject
,以避免尚未迁移的用户的类型错误。
我们自己的 RefObject
为了为不同 React 版本的 Data Grid 组件中的 apiRef
提供不同的类型,我们创建了自己的 RefObject
类型。
我们利用了 useRef()
在 React 19 中需要参数这一事实,以确保 RefObject
对于 React < 19 被评估为 MutableRefObject
,否则被评估为 RefObject
。
// in React 19 useRef requires a parameter, so `() => any` will not match anymore
export type RefObject<T> = typeof React.useRef extends () => any
? React.MutableRefObject<T>
: React.RefObject<T>;
结论
迁移到 React 19 是一项重要的 undertaking。通过将其分解为两个阶段,我们能够在致力于我们自己的迁移的同时,快速为我们的用户提供 React 19 兼容性。
在迁移期间进行的实用程序和重构将使将来更容易维护向后兼容性,因为 forwardRef
更新和 apiRef
类型更改都可以在一个地方完成。
虽然这个项目是由 MUI X 团队发起的,但我们特别感谢维护 Material UI 的同事们,感谢他们在为 @mui/material
的 v5 和 v6 版本添加 React 19 支持方面提供的巨大帮助。他们还为我们的两个存储库用于构建和测试组件的内部工具提供了必要的更新。
我们希望我们的经验可以对您有所帮助,并缩短您自己的 React 19 迁移所需的时间!