我們如何將 MUI X 遷移到 React 19
作為一套熱門 React UI 組件的維護者,我們計畫在 React 19 穩定版本發布後盡快將我們的程式碼庫遷移到 React 19,該版本於 2024 年底發布。這證明是一項重大的任務,需要仔細的規劃和執行。
本文描述了我們的遷移策略,以及我們如何解決在此過程中遇到的一些關鍵問題。我們希望這對其他也需要在其程式碼庫中支援 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://react.dev.org.tw/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 在引用上變得不穩定,從而破壞下游的 memoizations。
為了解決這個問題,我們新增了一個 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 執行測試
- 使用 React 19 的
RefObject
和早期版本的MutableRefObject
更新類型參考
此階段中最大的變更是圍繞 useRef()
hook 更新。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 是一項重大的任務。透過將其分解為兩個階段,我們能夠在我們進行自身遷移的同時,快速為使用者提供 React 19 相容性。
在遷移期間進行的工具和重構將使未來更容易維護向後相容性,因為 forwardRef
更新和 apiRef
類型變更都可以從一個地方完成。
雖然這個專案是由 MUI X 團隊主導的,但我們特別感謝維護 Material UI 的同事們,感謝他們在為 @mui/material
的 v5 和 v6 新增 React 19 支援方面提供的巨大幫助。他們還為我們的兩個儲存庫用於建置和測試組件的內部工具提供了必要的更新。
我們希望我們的經驗對您有所幫助,並縮短您自己的 React 19 遷移所需的時間!