React 19 升級指南

2024年4月25日 作者:Ricky Hanlon


React 19 新增的改進需要一些重大更改,但我們已努力使升級儘可能順利,並且我們預計這些更改不會影響大多數應用程式。

注意

React 18.3 也已釋出

為了使升級到 React 19 更容易,我們釋出了 react@18.3 版本,它與 18.2 相同,但添加了對已棄用 API 和 React 19 需要其他更改的警告。

我們建議您首先升級到 React 18.3,以幫助在升級到 React 19 之前識別任何問題。

有關 18.3 中更改的列表,請參閱 發行說明

在這篇文章中,我們將指導您完成升級到 React 19 的步驟

如果您想幫助我們測試 React 19,請按照此升級指南中的步驟操作,並報告您遇到的任何問題。有關新增到 React 19 的新功能列表,請參閱 React 19 發行文章


安裝

注意

現在需要新的 JSX 轉換

我們在 2020 年引入了一種 新的 JSX 轉換,以改進包大小並在不匯入 React 的情況下使用 JSX。在 React 19 中,我們添加了更多改進,例如使用 ref 作為 prop 和 JSX 速度改進,這些改進需要新的轉換。

如果未啟用新的轉換,您將看到此警告

控制檯
您的應用程式(或其依賴項之一)正在使用過時的 JSX 轉換。更新到現代 JSX 轉換以獲得更快的效能: https://reactjs.org.tw/link/new-jsx-transform

我們預計大多數應用程式都不會受到影響,因為該轉換已在大多數環境中啟用。有關如何升級的手動說明,請參閱 公告文章

安裝最新版本的 React 和 React DOM

npm install --save-exact react@^19.0.0 react-dom@^19.0.0

或者,如果您使用的是 Yarn

yarn add --exact react@^19.0.0 react-dom@^19.0.0

如果您使用的是 TypeScript,則還需要更新型別。

npm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0

或者,如果您使用的是 Yarn

yarn add --exact @types/react@^19.0.0 @types/react-dom@^19.0.0

我們還包含了一個程式碼修改工具,用於最常見的替換。請參見下面的 TypeScript 更改

程式碼修改工具

為了幫助進行升級,我們與 codemod.com 團隊合作,釋出了程式碼修改工具,這些程式碼修改工具將自動將您的程式碼更新到 React 19 中的許多新 API 和模式。

所有程式碼修改工具都可以在 react-codemod 倉庫 中找到,並且 Codemod 團隊也參與了程式碼修改工具的維護。要執行這些程式碼修改工具,我們建議使用 codemod 命令而不是 react-codemod,因為它執行速度更快,處理更復雜的程式碼遷移,並提供更好的 TypeScript 支援。

注意

執行所有 React 19 程式碼修改工具

使用 React 19 codemod 食譜執行本指南中列出的所有程式碼修改工具

npx codemod@latest react/19/migration-recipe

這將執行來自react-codemod的以下程式碼修改。

這並不包含TypeScript更改。請參見下面的TypeScript更改

包含程式碼修改的更改包括以下命令。

有關所有可用程式碼修改的列表,請參閱react-codemod 程式碼庫

重大更改

渲染錯誤不再重新丟擲

在之前的React版本中,渲染過程中丟擲的錯誤會被捕獲並重新丟擲。在開發環境中,我們還會記錄到console.error,導致錯誤日誌重複。

在React 19中,我們改進了錯誤處理方式,透過不重新丟擲錯誤來減少重複。

  • 未捕獲的錯誤:未被錯誤邊界捕獲的錯誤將報告給window.reportError
  • 已捕獲的錯誤:被錯誤邊界捕獲的錯誤將報告給console.error

此更改不應影響大多數應用程式,但如果您的生產錯誤報告依賴於重新丟擲的錯誤,則可能需要更新您的錯誤處理。為了支援這一點,我們在createRoothydrateRoot中添加了新的錯誤處理方法。

const root = createRoot(container, {
onUncaughtError: (error, errorInfo) => {
// ... log error report
},
onCaughtError: (error, errorInfo) => {
// ... log error report
}
});

更多資訊,請參閱createRoothydrateRoot的文件。

已移除棄用的React API

已移除:函式的propTypesdefaultProps

PropTypes已於2017年4月(v15.5.0)棄用。

在React 19中,我們從React包中移除了propType檢查,使用它們將被靜默忽略。如果您正在使用propTypes,我們建議遷移到TypeScript或其他型別檢查解決方案。

我們還將函式元件中的defaultProps替換為ES6預設引數。類元件將繼續支援defaultProps,因為沒有ES6替代方案。

// Before
import PropTypes from 'prop-types';

function Heading({text}) {
return <h1>{text}</h1>;
}
Heading.propTypes = {
text: PropTypes.string,
};
Heading.defaultProps = {
text: 'Hello, world!',
};
// After
interface Props {
text?: string;
}
function Heading({text = 'Hello, world!'}: Props) {
return <h1>{text}</h1>;
}

注意

使用以下程式碼修改將propTypes轉換為TypeScript:

npx codemod@latest react/prop-types-typescript

已移除:使用contextTypesgetChildContext的舊版上下文

舊版上下文已於2018年10月(v16.6.0)棄用。

舊版上下文僅在類元件中使用contextTypesgetChildContext API可用,並被contextType取代,原因是存在一些容易被忽略的細微錯誤。在React 19中,我們移除了舊版上下文,使React更小更快。

如果您仍在類元件中使用舊版上下文,則需要遷移到新的contextType API。

// Before
import PropTypes from 'prop-types';

class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
};

getChildContext() {
return { foo: 'bar' };
}

render() {
return <Child />;
}
}

class Child extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};

render() {
return <div>{this.context.foo}</div>;
}
}
// After
const FooContext = React.createContext();

class Parent extends React.Component {
render() {
return (
<FooContext value='bar'>
<Child />
</FooContext>
);
}
}

class Child extends React.Component {
static contextType = FooContext;

render() {
return <div>{this.context}</div>;
}
}

已移除:字串ref

字串ref已於2018年3月(v16.3.0)棄用。

類元件在被ref回撥替換之前支援字串ref,原因是存在多個缺點。在React 19中,我們移除了字串ref,使React更簡單易懂。

如果您仍在類元件中使用字串ref,則需要遷移到ref回撥。

// Before
class MyComponent extends React.Component {
componentDidMount() {
this.refs.input.focus();
}

render() {
return <input ref='input' />;
}
}
// After
class MyComponent extends React.Component {
componentDidMount() {
this.input.focus();
}

render() {
return <input ref={input => this.input = input} />;
}
}

注意

使用ref回撥程式碼修改字串ref。

npx codemod@latest react/19/replace-string-ref

已移除:模組模式工廠

模組模式工廠已於 2019年8月 (v16.9.0) 被棄用。

這種模式很少使用,並且支援它會導致 React 比必要的大一點且慢一點。在 React 19 中,我們將移除對模組模式工廠的支援,您需要遷移到常規函式。

// Before
function FactoryComponent() {
return { render() { return <div />; } }
}
// After
function FactoryComponent() {
return <div />;
}

已移除: React.createFactory

createFactory 已於 2020年2月 (v16.13.0) 被棄用。

在廣泛支援 JSX 之前,使用 createFactory 很常見,但如今很少使用,可以用 JSX 代替。在 React 19 中,我們將移除 createFactory,您需要遷移到 JSX。

// Before
import { createFactory } from 'react';

const button = createFactory('button');
// After
const button = <button />;

已移除: react-test-renderer/shallow

在 React 18 中,我們更新了 react-test-renderer/shallow 以重新匯出 react-shallow-renderer。在 React 19 中,我們將移除 react-test-render/shallow,建議直接安裝該軟體包。

npm install react-shallow-renderer --save-dev
- import ShallowRenderer from 'react-test-renderer/shallow';
+ import ShallowRenderer from 'react-shallow-renderer';

注意

請重新考慮淺渲染

淺渲染依賴於 React 內部機制,並可能阻止您進行將來的升級。我們建議您將測試遷移到 @testing-library/react@testing-library/react-native

已移除:已棄用的 React DOM API

已移除: react-dom/test-utils

我們已將 actreact-dom/test-utils 移動到 react 包中。

控制檯
ReactDOMTestUtils.act 已被棄用,建議使用 React.act。從 react 而不是 react-dom/test-utils 匯入 act。更多資訊請參見 https://reactjs.org.tw/warnings/react-dom-test-utils

要修復此警告,您可以從 react 匯入 act

- import {act} from 'react-dom/test-utils'
+ import {act} from 'react';

所有其他 test-utils 函式均已移除。這些實用程式不常用,並且使其過於容易依賴於元件和 React 的底層實現細節。在 React 19 中,呼叫這些函式將導致錯誤,並且它們的匯出將在未來的版本中被移除。

請參閱 警告頁面 以瞭解替代方案。

注意

使用程式碼修改工具將 ReactDOMTestUtils.act 修改為 React.act

npx codemod@latest react/19/replace-act-import

已移除: ReactDOM.render

ReactDOM.render 已於 2022年3月 (v18.0.0) 被棄用。在 React 19 中,我們將移除 ReactDOM.render,您需要遷移到使用 ReactDOM.createRoot

// Before
import {render} from 'react-dom';
render(<App />, document.getElementById('root'));

// After
import {createRoot} from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);

注意

使用程式碼遷移工具將 ReactDOM.render 遷移到 ReactDOMClient.createRoot

npx codemod@latest react/19/replace-reactdom-render

已移除:ReactDOM.hydrate

ReactDOM.hydrate 已於 2022年3月 (v18.0.0) 被棄用。在 React 19 中,我們將移除 ReactDOM.hydrate,您需要遷移到使用 ReactDOM.hydrateRoot

// Before
import {hydrate} from 'react-dom';
hydrate(<App />, document.getElementById('root'));

// After
import {hydrateRoot} from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);

注意

使用程式碼遷移工具將 ReactDOM.hydrate 遷移到 ReactDOMClient.hydrateRoot

npx codemod@latest react/19/replace-reactdom-render

已移除:ReactDOM.unmountComponentAtNode

ReactDOM.unmountComponentAtNode 已於 2022年3月 (v18.0.0) 被棄用。在 React 19 中,您需要遷移到使用 root.unmount()

// Before
unmountComponentAtNode(document.getElementById('root'));

// After
root.unmount();

更多資訊請參見 createRoothydrateRootroot.unmount()

注意

使用程式碼遷移工具將 ReactDOM.unmountComponentAtNode 遷移到 root.unmount

npx codemod@latest react/19/replace-reactdom-render

已移除:ReactDOM.findDOMNode

ReactDOM.findDOMNode 已於 2018年10月 (v16.6.0) 被棄用。

我們移除 findDOMNode 是因為它是一個遺留的應急方案,執行速度慢,難以重構,只返回第一個子節點,並破壞了抽象級別(更多資訊請參見 這裡)。您可以使用 DOM refs 來替換 ReactDOM.findDOMNode

// Before
import {findDOMNode} from 'react-dom';

function AutoselectingInput() {
useEffect(() => {
const input = findDOMNode(this);
input.select()
}, []);

return <input defaultValue="Hello" />;
}
// After
function AutoselectingInput() {
const ref = useRef(null);
useEffect(() => {
ref.current.select();
}, []);

return <input ref={ref} defaultValue="Hello" />
}

新的棄用警告

已棄用:element.ref

React 19 支援 ref 作為屬性,因此我們棄用 element.ref,並用 element.props.ref 替代。

訪問 element.ref 將會發出警告。

控制檯
不再支援訪問 element.ref。ref 現在是一個普通的屬性。它將在未來的版本中從 JSX 元素型別中移除。

已棄用:react-test-renderer

我們棄用 react-test-renderer 是因為它實現了與使用者使用的環境不匹配的渲染器環境,促進了測試實現細節,並依賴於對 React 內部機制的內省。

測試渲染器是在更多可行的測試策略(如 React Testing Library)可用之前建立的,我們現在建議改用現代測試庫。

在 React 19 中,react-test-renderer 將記錄棄用警告,並已切換到併發渲染。我們建議將您的測試遷移到 @testing-library/react@testing-library/react-native 以獲得現代且良好支援的測試體驗。

顯著變化

StrictMode 改進

React 19 包含若干針對 Strict Mode 的修復和改進。

在開發環境下,當 Strict Mode 進行雙重渲染時,useMemouseCallback 將在第二次渲染期間重用第一次渲染時記憶化的結果。已經相容 Strict Mode 的元件應該不會注意到行為上的差異。

與所有 Strict Mode 行為一樣,這些功能旨在在開發過程中主動發現元件中的錯誤,以便在將它們釋出到生產環境之前修復它們。例如,在開發過程中,StrictMode 會在初始掛載時雙重呼叫 ref 回撥函式,以模擬掛載元件被 Suspense 替代時的行為。

Suspense 的改進

在 React 19 中,當元件掛起時,React 將立即提交最近 Suspense 邊界的回退內容,而無需等待整個同級樹渲染完成。回退內容提交後,React 將安排對掛起同級元件的另一次渲染,以“預熱”樹中其餘部分的延遲請求。

Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Both Panel components contain isActive with value false.
Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Both Panel components contain isActive with value false.

以前,當元件掛起時,會先渲染掛起的同級元件,然後提交回退內容。

The same diagram as the previous, with the isActive of the first child Panel component highlighted indicating a click with the isActive value set to true. The second Panel component still contains value false.
The same diagram as the previous, with the isActive of the first child Panel component highlighted indicating a click with the isActive value set to true. The second Panel component still contains value false.

在 React 19 中,當元件掛起時,會先提交回退內容,然後渲染掛起的同級元件。

此更改意味著 Suspense 回退內容顯示速度更快,同時仍然可以預熱掛起樹中的延遲請求。

移除 UMD 構建

過去,UMD 廣泛用作一種方便的方法來載入 React,無需構建步驟。現在,有更現代的替代方案可以在 HTML 文件中將模組載入為指令碼。從 React 19 開始,React 將不再生成 UMD 構建,以降低其測試和釋出流程的複雜性。

要使用 script 標籤載入 React 19,我們建議使用基於 ESM 的 CDN,例如 esm.sh

<script type="module">
import React from "https://esm.sh/react@19/?dev"
import ReactDOMClient from "https://esm.sh/react-dom@19/client?dev"
...
</script>

依賴於 React 內部元件的庫可能會阻止升級

此版本包含對 React 內部元件的更改,這可能會影響忽略我們不要使用內部元件(例如 SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED)的請求的庫。這些更改對於實現 React 19 中的改進是必要的,並且不會破壞遵循我們指南的庫。

根據我們的 版本策略,這些更新未列為重大更改,我們也沒有包含有關如何升級它們的文件。建議是刪除任何依賴於內部元件的程式碼。

為了反映使用內部元件的影響,我們已將 SECRET_INTERNALS 字尾重新命名為

_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE

將來,我們將更積極地阻止從 React 訪問內部元件,以阻止其使用並確保使用者不會被阻止升級。

TypeScript 更改

移除已棄用的 TypeScript 型別

我們根據 React 19 中移除的 API 清理了 TypeScript 型別。一些已移除的型別已被移動到更相關的包中,而另一些則不再需要來描述 React 的行為。

注意

我們已經發布了 types-react-codemod 來遷移大多數與型別相關的重大更改。

npx types-react-codemod@latest preset-19 ./path-to-app

如果您有很多對 element.props 的不安全訪問,您可以執行此附加的 codemod。

npx types-react-codemod@latest react-element-default-any-props ./path-to-your-react-ts-files

檢視 types-react-codemod 以瞭解受支援的替換列表。如果您覺得缺少 codemod,可以在 缺少的 React 19 codemod 列表 中進行跟蹤。

ref 需要清理

此更改包含在 react-19 程式碼修改預設中,作為 no-implicit-ref-callback-return

由於引入了 ref 清理函式,從 ref 回撥函式返回任何其他內容現在將被 TypeScript 拒絕。解決方法通常是停止使用隱式返回。

- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />

原始程式碼返回 HTMLDivElement 的例項,TypeScript 將無法知道這是否應該是一個清理函式。

useRef 需要一個引數

此更改包含在 react-19 程式碼修改預設中,作為 refobject-defaults

長期以來,TypeScript 和 React 的工作方式一直受到 useRef 的詬病。我們更改了型別,以便 useRef 現在需要一個引數。這大大簡化了它的型別簽名。它現在將表現得更像 createContext

// @ts-expect-error: Expected 1 argument but saw none
useRef();
// Passes
useRef(undefined);
// @ts-expect-error: Expected 1 argument but saw none
createContext();
// Passes
createContext(undefined);

現在這也意味著所有 ref 都是可變的。您將不再遇到因為用 null 初始化而無法修改 ref 的問題。

const ref = useRef<number>(null);

// Cannot assign to 'current' because it is a read-only property
ref.current = 1;

MutableRef 現已棄用,取而代之的是單個 RefObject 型別,useRef 將始終返回該型別。

interface RefObject<T> {
current: T
}

declare function useRef<T>: RefObject<T>

useRef 仍然有一個方便的過載用於 useRef<T>(null),它會自動返回 RefObject<T | null>。為了簡化由於 useRef 的必需引數而導致的遷移,添加了一個方便的過載用於 useRef(undefined),它會自動返回 RefObject<T | undefined>

檢視 [RFC] 使所有 ref 可變,瞭解有關此更改的先前討論。

ReactElement TypeScript 型別的更改

此更改包含在 react-element-default-any-props 程式碼修改中。

如果元素型別為 ReactElement,則 React 元素的 props 現在預設為 unknown,而不是 any。如果您將型別引數傳遞給 ReactElement,則不會影響您。

type Example2 = ReactElement<{ id: string }>["props"];
// ^? { id: string }

但是,如果您依賴於預設值,則現在必須處理 unknown

type Example = ReactElement["props"];
// ^? Before, was 'any', now 'unknown'

只有當您有很多依賴於不安全的元素 props 訪問的遺留程式碼時,才需要它。元素內省只作為一種應急措施存在,您應該透過顯式的 any 明確表示您的 props 訪問是不安全的。

TypeScript 中的 JSX 名稱空間

此更改包含在 react-19 程式碼修改預設中,作為 scoped-jsx

長期以來的一個請求是刪除全域性 JSX 名稱空間,而支援使用 React.JSX。這有助於防止全域性型別的汙染,從而防止使用 JSX 的不同 UI 庫之間的衝突。

您現在需要將 JSX 名稱空間的模組擴充套件包裝在 `declare module ”…”` 中。

// global.d.ts
+ declare module "react" {
namespace JSX {
interface IntrinsicElements {
"my-element": {
myElementProps: string;
};
}
}
+ }

確切的模組說明符取決於您在 tsconfig.jsoncompilerOptions 中指定的 JSX 執行時。

  • 對於 "jsx": "react-jsx",它將是 react/jsx-runtime
  • 對於 "jsx": "react-jsxdev",對應的包應該是 react/jsx-dev-runtime
  • 對於 "jsx": "react""jsx": "preserve",對應的包應該是 react

改進的 useReducer 型別宣告

useReducer 現在擁有了改進的型別推斷,這要感謝 @mfp22

然而,這需要一個breaking change,useReducer不再接受完整的reducer型別作為型別引數,而是需要不傳入任何引數(依賴上下文型別推斷)或者同時傳入狀態和action型別。

新的最佳實踐是 *不要* 向 useReducer 傳遞型別引數。

- useReducer<React.Reducer<State, Action>>(reducer)
+ useReducer(reducer)

在一些邊緣情況下,你可以透過傳入 Action 元組顯式地指定狀態和action型別,但這可能無法正常工作。

- useReducer<React.Reducer<State, Action>>(reducer)
+ useReducer<State, [Action]>(reducer)

如果你內聯定義 reducer,我們建議你改為註釋函式引數。

- useReducer<React.Reducer<State, Action>>((state, action) => state)
+ useReducer((state: State, action: Action) => state)

如果你將 reducer 移到 useReducer 呼叫之外,也必須這樣做。

const reducer = (state: State, action: Action) => state;

變更日誌

其他breaking change

  • react-domsrchref 屬性中的 JavaScript URL 導致錯誤 #26507
  • react-dom:從 onRecoverableError 中移除 errorInfo.digest #28222
  • react-dom:移除 unstable_flushControlled #26397
  • react-dom:移除 unstable_createEventHandle #28271
  • react-dom:移除 unstable_renderSubtreeIntoContainer #28271
  • react-dom:移除 unstable_runWithPriority #28271
  • react-is:從 react-is 中移除已棄用的方法 28224

其他值得注意的更改

  • react:批次同步、預設和連續lane #25700
  • react:不要預渲染已掛起的元件的同級元件 #26380
  • react:檢測由渲染階段更新引起的無限更新迴圈 #26625
  • react-dom:popstate中的過渡現在是同步的 #26025
  • react-dom:在伺服器端渲染期間移除佈局效果警告 #26395
  • react-dom:發出警告,並且不要為src/href設定空字串(錨點標籤除外) #28124

要檢視完整的更改列表,請參閱 變更日誌


感謝 Andrew ClarkEli WhiteJack PopeJan KassensJosh StoryMatt CarrollNoah LemenSophie AlpertSebastian Silbermann 對本文的審閱和編輯。