React 19 正式發(fā)布,看看該版本都帶來了那些更新!
12 月 6 日,React 19 正式發(fā)布,本文來看看該版本都帶來了那些更新!
React 19 更新內容
Actions
Actions 是一種簡化請求數(shù)據(jù)處理的方法統(tǒng)稱。
在 React 應用中,一個很常見的用例是執(zhí)行數(shù)據(jù)變更,然后根據(jù)響應更新狀態(tài)。例如,當用戶提交表單以更改其姓名時,需要發(fā)起 API 請求,并處理響應結果。在過去,需要手動處理等待狀態(tài)、錯誤、樂觀更新以及順序請求等。
例如,可以使用 useState
來管理等待和錯誤狀態(tài):
function UpdateName({}) {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
const handleSubmit = async () => {
setIsPending(true);
const error = await updateName(name);
setIsPending(false);
if (error) {
setError(error);
return;
}
redirect("/path");
};
return (
<div>
<input value={name} onChange={(event) => setName(event.target.value)} />
<button onClick={handleSubmit} disabled={isPending}>
Update
</button>
{error && <p>{error}</p>}
</div>
);
}
在 React 19 中,增加了對在轉換中使用異步函數(shù)的支持,以自動處理等待狀態(tài)、錯誤、表單和樂觀更新。
例如,可以使用useTransition
來自動處理等待狀態(tài):
function UpdateName({}) {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();
const handleSubmit = () => {
startTransition(async () => {
const error = await updateName(name);
if (error) {
setError(error);
return;
}
redirect("/path");
})
};
return (
<div>
<input value={name} onChange={(event) => setName(event.target.value)} />
<button onClick={handleSubmit} disabled={isPending}>
Update
</button>
{error && <p>{error}</p>}
</div>
);
}
異步轉換會立即將isPending
狀態(tài)設置為true
,發(fā)起異步請求,并在任何轉換完成后將isPending
切換回false
。這樣就可以在數(shù)據(jù)變更時保持當前 UI 的響應性和交互性。
在 React 19 中,基于 Actions 的概念,引入了useOptimistic
來管理樂觀更新,以及一個全新的 Hook React.useActionState
用于處理常見的 Actions 場景。在react-dom
中,添加了<form>
Actions 來自動管理表單,以及useFormStatus
來支持表單中常見的 Actions 場景。
在 React 19 中,上面的例子可以簡化為:
function ChangeName({ name, setName }) {
const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const error = await updateName(formData.get("name"));
if (error) {
return error;
}
redirect("/path");
return null;
},
null,
);
return (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>Update</button>
{error && <p>{error}</p>}
</form>
);
}
全新 Hook:useActionState
為了更簡單的處理常見的 Actions 場景,React 19 引入了一個全新的 Hook useActionState
:
const [error, submitAction, isPending] = useActionState(
async (previousState, newName) => {
const error = await updateName(newName);
if (error) {
// 可以返回Action的任何結果
// 在這里,只返回錯誤
return error;
}
// 處理成功情況
return null;
},
null,
);
useActionState
接受一個函數(shù)(即 Action),并返回一個包裝后的 Action 以供調用。當調用包裝后的 Action 時,useActionState
將返回Action
的最后結果作為data
,并返回Action
的等待狀態(tài)作為pending
。
React DOM: <form>
Actions
在 React 19 中,Actions 被集成到了 react-dom
新引入的 <form>
功能中?,F(xiàn)在支持將函數(shù)作為 <form>
、<input>
和 <button>
元素的 action
和 formAction
屬性傳遞,以使用 Actions 自動提交表單:
<form action={actionFunction}>
當<form>
相關的 Action 執(zhí)行成功時,React 會自動重置那些不受控組件的表單。如果需要手動重置<form>
,可以調用新的 React DOM API requestFormReset
。
React DOM 全新 Hook:useFormStatus
在設計系統(tǒng)中,通常需要編寫設計組件,這些組件需要獲取其所處 <form>
表單的相關信息,但又不想通過 props
逐級向下傳遞。雖然可以通過 Context 來實現(xiàn)這一點,但為了讓這種常見情況更簡單,React 19 添加了一個全新的 Hook:useFormStatus
。
import {useFormStatus} from 'react-dom';
function DesignButton() {
const {pending} = useFormStatus();
return <button type="submit" disabled={pending} />
}
useFormStatus
可以讀取父級<form>
的狀態(tài),就好像該表單是一個 Context 提供者一樣。
全新 Hook:useOptimistic
在執(zhí)行數(shù)據(jù)變更操作時,另一種常見的 UI 模式是:在異步請求正在進行的過程中,以樂觀的方式展示最終狀態(tài)。在 React 19 中,新增了一個名為useOptimistic
的 Hook,以簡化這一過程:
function ChangeName({currentName, onUpdateName}) {
const [optimisticName, setOptimisticName] = useOptimistic(currentName);
const submitAction = async formData => {
const newName = formData.get("name");
setOptimisticName(newName);
const updatedName = await updateName(newName);
onUpdateName(updatedName);
};
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>
);
}
useOptimistic
Hook 將在 updateName
請求過程中立即渲染 optimisticName
。當更新完成或出現(xiàn)錯誤時,React 將自動切換回 currentName
值。
全新 API:use
在 React 19 中,引入了一個新 API 來在渲染過程中讀取資源:use
。
例如,可以使用 use
來讀取一個 Promise,React 將掛起,直到promise
成功解析:
import {use} from 'react';
function Comments({commentsPromise}) {
// “use”會進行“掛起”操作,直到“Promise”被成功解析。
const comments = use(commentsPromise);
return comments.map(comment => <p key={comment.id}>{comment}</p>);
}
function Page({commentsPromise}) {
// 當“Comments”組件中“use”進行“掛起”操作時,
// 這個“Suspense”邊界(組件)將會被顯示出來。
return (
<Suspense fallback={<div>Loading...</div>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
)
我們還可以使用 use
來讀取 Context,這使得你可以有條件地讀取 Context,例如在提前返回之后讀?。?/p>
import {use} from 'react';
import ThemeContext from './ThemeContext'
function Heading({children}) {
if (children == null) {
return null;
}
// 由于存在提前返回的情況,使用useContext是行不通的。
const theme = use(ThemeContext);
return (
<h1 style={{color: theme.color}}>
{children}
</h1>
);
}
use
API 只能在渲染過程中調用,類似于 Hooks。與 Hooks 不同的是,use
可以有條件地調用。未來計劃支持更多使用 use
在渲染過程中消費資源的方式。
新的 React DOM 靜態(tài) API
React 19 為react-dom/static
添加了兩個新的 API,用于靜態(tài)網站生成:
prerender
prerenderToNodeStream
這兩個新 API 改進了 renderToString
,通過等待數(shù)據(jù)加載來生成靜態(tài) HTML。它們被設計為與流環(huán)境(如 Node.js Streams 和 Web Streams)一起工作。例如,在 Web Stream 環(huán)境中,可以使用 prerender
將 React 組件樹預渲染為靜態(tài) HTML:
import { prerender } from 'react-dom/static';
async function handler(request) {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}
prerender
API 會在返回靜態(tài) HTML 流之前等待所有數(shù)據(jù)加載完成。流可以被轉換為字符串,或者通過流式響應進行發(fā)送。它們不支持在加載內容時進行流式傳輸,而現(xiàn)有的 React DOM 服務端渲染 API 是支持這種流式傳輸?shù)摹?/p>
React Server Components
服務器組件
服務器組件是一個新的選項,它允許在打包之前,在與客戶應用或服務端渲染(SSR)服務器分離的環(huán)境中預先渲染組件。這個分離的環(huán)境就是 React 服務器組件中的“服務器”。服務器組件既可以在持續(xù)集成(CI)服務器上構建時運行一次,也可以在使用Web服務器時針對每個請求運行。
React 19 包含了從 Canary 版本引入的所有 React 服務器組件功能。這意味著,現(xiàn)在提供服務器組件的庫可以將 React 19 作為對等依賴項,并使用 react-server
導出條件,以便在支持全棧 React 架構的框架中使用。
服務器 Actions
服務器 Actions 允許客戶端組件調用在服務器上執(zhí)行的異步函數(shù)。
當使用 “use server” 指令定義一個服務端 Actions 時,框架會自動創(chuàng)建一個指向服務器函數(shù)的引用,并將這個引用傳遞給客戶端組件。當客戶端調用這個函數(shù)時,React 會發(fā)送一個請求到服務器執(zhí)行該函數(shù),并返回結果。
服務器 Actions 可以在服務器組件中創(chuàng)建,并通過 props
傳遞給客戶端組件,也可以被導入并在客戶端組件中使用。
其他更新
ref 作為 prop
從 React 19 開始,可以在函數(shù)組件中通過 prop
訪問 ref
:
function MyInput({placeholder, ref}) {
return <input placeholder={placeholder} ref={ref} />
}
//...
<MyInput ref={ref} />
新的函數(shù)組件將不再需要 forwardRef
,React 團隊將發(fā)布一個代碼轉換工具(codemod),以自動更新組件,使其使用新的 ref
prop。在以后的版本中,將棄用并移除 forwardRef
。
水合錯誤的差異信息展示
React 19 還改進了 react-dom 中針對水合錯誤的報錯報告。例如,之前在開發(fā)環(huán)境(DEV)中,并不會展示任何關于不匹配情況的信息,而是記錄多個錯誤;
現(xiàn)在,改為記錄一條包含不匹配差異信息的消息:
作為提供者
在 React 19 中,可以將<Context>
作為提供者,而不必使用<Context.Provider>
:
const ThemeContext = createContext('');
function App({children}) {
return (
<ThemeContext value="dark">
{children}
</ThemeContext>
);
}
新的 Context 提供者可以使用 <Context>
,React 團隊將發(fā)布一個代碼轉換工具(codemod)來轉換現(xiàn)有的提供者。在未來的版本中,將棄用 <Context.Provider>
。
refs 的清理函數(shù)
在 React 19 中,支持從 ref
的回調函數(shù)中返回一個清理函數(shù):
<input
ref={(ref) => {
// 引用已創(chuàng)建
// 新增:返回一個清理函數(shù),以便在元素從DOM中移除時重置引用。
return () => {
// 執(zhí)行引用清理操作
};
}}
/>
當組件卸載時,React 會調用從 ref
回調函數(shù)中返回的清理函數(shù)。這適用于 DOM 引用、指向類組件的引用以及useImperativeHandle
的情況。
由于引入了 ref 清理函數(shù),現(xiàn)在從 ref 回調函數(shù)中返回其他任何內容都會被 TypeScript 拒絕。解決辦法通常是停止使用隱式返回,例如:
- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />
原代碼返回了HTMLDivElement
的實例,而 TypeScript 無法判斷這是否是一個清理函數(shù),或者你并不想返回清理函數(shù)。
useDeferredValue 初始值
在 React 19 中,為useDeferredValue
添加了一個initialValue
選項:
function Search({deferredValue}) {
// 在初始渲染時,值為 ''
// 然后會基于 deferredValue 進行重新渲染
const value = useDeferredValue(deferredValue, '');
return (
<Results query={value} />
);
}
當提供 initialValue
時,useDeferredValue
將在組件的初始渲染中將其作為值返回,并在后臺計劃用返回的 deferredValue
進行重新渲染。
支持文檔元數(shù)據(jù)
在HTML文檔中,諸如<title>
、<link>
和<meta>
等元數(shù)據(jù)標簽通常被專門放置在<head>
部分。然而,在 React 中,確定并應用恰當?shù)脑獢?shù)據(jù)可能會變得復雜,因為這些組件可能遠離實際渲染<head>
標簽的位置,甚至 React 本身可能并不直接渲染<head>
。過去,這些元素需要在副作用中手動插入,或者通過像 react-helmet 這樣的庫來處理,并且在服務端渲染 React 應用時需要小心處理。
在 React 19 中,增加了對在組件中原生渲染文檔元數(shù)據(jù)標簽的支持:
function BlogPost({post}) {
return (
<article>
<h1>{post.title}</h1>
<title>{post.title}</title>
<meta name="author" content="Josh" />
<link rel="author" />
<meta name="keywords" content={post.keywords} />
<p>
Eee equals em-see-squared...
</p>
</article>
);
}
當 React 渲染這個組件時,它會識別 <title>
、<link>
和 <meta>
標簽,并自動將它們提升到文檔的 <head>
部分。通過原生支持這些元數(shù)據(jù)標簽,能夠確保它們在僅限客戶端應用、流式服務端渲染和服務器組件中正常工作。
樣式表支持
無論是外部鏈接的樣式表(如<link rel="stylesheet" href="...">
)還是內聯(lián)樣式表(如<style>...</style>
),由于樣式優(yōu)先級規(guī)則的存在,它們在 DOM 中的放置位置都需要謹慎。構建一個允許在組件內部進行組合的樣式表功能是很困難的,所以用戶往往要么將所有樣式加載到距離可能依賴它們的組件很遠的地方,要么使用封裝了這種復雜性的樣式庫。
在 React 19 中,解決了這種復雜性,并通過內置對樣式表的支持,在客戶端的并發(fā)渲染以及服務端流式渲染方面實現(xiàn)更深入的集成。如果你告知 React 樣式表的優(yōu)先級,它將會管理樣式表在 DOM 中的插入順序,并確保樣式表(如果是外部樣式表的話)在顯示依賴于這些樣式規(guī)則的內容之前加載該樣式表。
function ComponentOne() {
return (
<Suspense fallback="loading...">
<link rel="stylesheet" href="foo" precedence="default" />
<link rel="stylesheet" href="bar" precedence="high" />
<article class="foo-class bar-class">
{...}
</article>
</Suspense>
)
}
function ComponentTwo() {
return (
<div>
<p>{...}</p>
<link rel="stylesheet" href="baz" precedence="default" /> <!-- 將被插入到 foo 和 bar 之間 -->
</div>
)
}
在服務端渲染期間,React 將把樣式表包含在 <head>
中,這確保了瀏覽器在加載完成后才會繪制。如果在開始流式傳輸后發(fā)現(xiàn)樣式表,React 將確保在客戶端的 <head>
中插入樣式表,然后才顯示依賴于該樣式表的 Suspense 邊界的內容。
在客戶端渲染期間,React 將等待新渲染的樣式表加載完成后再提交渲染。如果在應用的多個地方渲染這個組件,React 將只在文檔中包含一次樣式表:
function App() {
return <>
<ComponentOne />
...
<ComponentOne /> // 不會導致 DOM 中出現(xiàn)重復的樣式表鏈接
</>
}
對于習慣于手動加載樣式表的用戶來說,這是一個將樣式表放置在依賴它們的組件旁邊的好機會,這樣能更便于局部分析,也更容易確保只加載實際依賴的樣式表。
樣式庫和與打包工具的樣式集成也可以采用這一功能,因此即使不直接渲染自己的樣式表,當使用的工具升級以使用該功能時,仍然可以從中受益。
支持異步腳本
在 HTML 中,普通腳本(<script src="...">
)和延遲腳本(<script defer="" src="...">
)按照文檔順序加載,這使得在組件樹的深處渲染這些類型的腳本變得具有挑戰(zhàn)性。然而,異步腳本(<script async="" src="...">
)會以任意順序加載。
在 React 19 中,通過允許在組件樹的任意位置渲染異步腳本,提供了對異步腳本更好的支持,即在實際依賴該腳本的組件內部渲染它們,而無需管理腳本實例的重新定位和去重。
function MyComponent() {
return (
<div>
<script async={true} src="..." />
Hello World
</div>
);
}
function App() {
return (
<html>
<body>
<MyComponent>
...
<MyComponent> // 不會導致DOM中出現(xiàn)重復的腳本
</body>
</html>
);
}
在所有的渲染環(huán)境中,異步腳本都會進行去重處理,所以即便它被多個不同的組件渲染,React 也只會加載并執(zhí)行該腳本一次。
在服務端渲染過程中,異步腳本會被包含在<head>
部分,并優(yōu)先級排在更關鍵的資源之后,這些資源會阻塞繪制,如樣式表、字體和圖片預加載。
支持預加載資源
在初始文檔加載以及客戶端更新期間,盡早告知瀏覽器它可能需要加載的資源,會對頁面性能產生顯著的影響。
React 19 包含了許多用于加載和預加載瀏覽器資源的新 API,旨在盡可能簡便地打造優(yōu)質的用戶體驗,避免因資源加載效率低下而受到影響。
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';
function MyComponent() {
preinit('https://.../path/to/some/script.js', { as: 'script' }); // 積極加載并執(zhí)行此腳本
preload('https://.../path/to/font.woff', { as: 'font' }); // 預加載此字體
preload('https://.../path/to/stylesheet.css', { as: 'style' }); // 預加載此樣式表
prefetchDNS('https://...'); // 當你實際上可能不會從此主機請求任何內容時
preconnect('https://...'); // 當你將請求某些內容但不確定是什么時
}
<html>
<head>
<!-- 鏈接/腳本根據(jù)其對早期加載的有用性進行優(yōu)先級排序,而不是調用順序 -->
<link rel="prefetch-dns" href="https://...">
<link rel="preconnect" href="https://...">
<link rel="preload" as="font" href="https://.../path/to/font.woff">
<link rel="preload" as="style" href="https://.../path/to/stylesheet.css">
<script async="" src="https://.../path/to/some/script.js"></script>
</head>
<body>
...
</body>
</html>
這些 API 可用于優(yōu)化初始頁面加載,比如將字體等額外資源的發(fā)現(xiàn)過程從樣式表加載環(huán)節(jié)中分離出來。它們還能通過預取預期導航將會用到的資源列表,然后在點擊甚至懸停操作時就立即預加載這些資源,從而加快客戶端更新的速度。
與第三方腳本和擴展的兼容性
在 React 19 中,改進了 hydration(水合,即客戶端激活)以適應第三方腳本和瀏覽器擴展。
在水合過程中,如果在客戶端渲染的元素與服務端生成的 HTML 中的元素不匹配,React 將強制客戶端重新渲染以修正內容。以前,如果元素是由第三方腳本或瀏覽器擴展插入的,它將觸發(fā)不匹配錯誤并導致客戶端重新渲染。
在 React 19 中,<head>
和 <body>
中的意外標簽將被跳過,避免了不匹配錯誤。如果 React 需要由于與水合不相關的錯誤而重新渲染整個文檔,它將保留由第三方腳本和瀏覽器擴展插入的樣式表。
更好的錯誤報告
React 19 中改進了錯誤處理,以消除重復并提供處理捕獲和未捕獲錯誤的選項。例如,當渲染過程中出現(xiàn)錯誤并被 Error Boundary 捕獲時,以前 React 會拋出錯誤兩次(一次是原始錯誤,然后是在嘗試自動恢復失敗后再次拋出),然后調用 console.error
顯示錯誤發(fā)生位置的信息。
這導致每個捕獲的錯誤都有三個錯誤:
在 React 19 中,只會記錄一個包含所有錯誤信息的單一錯誤:
在 React 19 中,引入了三個新的根選項來補充 onRecoverableError:
- onCaughtError:當 React 在 Error Boundary 中捕獲錯誤時調用。
- onUncaughtError:當拋出錯誤并且未被 Error Boundary 捕獲時調用。
- onRecoverableError:當拋出錯誤并自動恢復時調用。
支持自定義元素
React 19 增加了對自定義元素的完整支持,并通過了Custom Elements Everywhere的所有測試。
在以前的版本中,由于 React 將未識別的 props 視為 HTML 屬性而不是 JavaScript 屬性,所以在 React 中使用自定義元素一直很困難。在 React 19 中,增加了對 JS 屬性的支持,這在客戶端和服務端渲染(SSR)中都有效,具體策略如下:
- 服務端渲染:傳遞給自定義元素的 props 如果是原始值類型,如字符串、數(shù)字,或者值為 true,它們將作為 HTML 屬性渲染。具有非原始值類型(如對象、符號、函數(shù))或值為 false的 props 將被省略。
- 客戶端渲染:與自定義元素實例上的屬性匹配的 props 將被分配為 JS 屬性,否則它們將被分配為 HTML 屬性。
React 19 更新總結
- Actions:
useActionState
Hook:接受一個函數(shù)(Action),返回包裝后的 Action 供調用,調用時返回結果和等待狀態(tài),用于簡化常見的 Actions 場景處理。useOptimistic
Hook:在執(zhí)行數(shù)據(jù)變更的異步請求時,以樂觀方式展示最終狀態(tài),更新完成或出錯后自動切換回原值,優(yōu)化用戶界面反饋。<form>
Actions 集成:在react-dom
中,支持將函數(shù)作為<form>
、<input>
和<button>
元素的相關屬性傳遞,實現(xiàn)自動提交表單,成功時自動重置不受控組件表單,也可手動調用requestFormReset
API 重置。useFormStatus
Hook:方便編寫設計組件獲取所處<form>
表單信息,無需層層傳遞props
,可像讀取 Context 提供者狀態(tài)一樣讀取表單狀態(tài)。
use
API:用于在渲染時讀取資源,可讀取Promise
并讓 React 掛起直至其解析,也能讀取Context
,且可條件性調用。- 新的 React DOM 靜態(tài) API:
prerender
和prerenderToNodeStream
用于靜態(tài)網站生成,改進renderToString
功能,會等待數(shù)據(jù)加載后生成靜態(tài) HTML,適配 Node.js Streams 和 Web Streams 等流環(huán)境。 - React Server Components:提供在獨立于客戶端應用或 SSR 服務器環(huán)境中提前渲染組件的新選擇,相關庫可將 React 19 作為對等依賴,應用于支持全棧 React 架構的框架。
ref
作為 prop:現(xiàn)在可以在函數(shù)組件中直接使用ref
作為prop
。- 水合錯誤改進:改進了客戶端渲染和服務端渲染之間的水合錯誤報告。
<Context>
作為提供者:可以直接使用<Context>
作為提供者,而不是<Context.Provider>
。- ref 的清理函數(shù):現(xiàn)在可以從
ref
回調中返回一個清理函數(shù)。 useDeferredValue
的初始值:為useDeferredValue
添加了initialValue
選項。- 文檔元數(shù)據(jù)支持:支持在組件中渲染
<title>
、<link>
和<meta>
標簽,并自動提升到文檔的<head>
部分。 - 樣式表支持:提供了對樣式表的內置支持,包括外部鏈接和內聯(lián)樣式。
- 異步腳本支持:改進了對異步腳本的支持,允許在組件樹的任何位置渲染它們。
- 資源預加載支持:包括
prefetchDNS
、preconnect
、preload
和preinit
等 API,用于優(yōu)化資源加載。 - 與第三方腳本和擴展的兼容性:改進了水合以適應第三方腳本和瀏覽器擴展。
- 更好的錯誤報告:改進了錯誤處理,減少了重復錯誤,并提供了處理捕獲和未捕獲錯誤的選項。
- 對自定義元素的支持:增加了對自定義元素的全面支持。