React 19 → 19.2: New Primitives and the Compiler Era
React 19 → 19.2:新原語與 Compiler 時代
React 19 gave me that "wait… this is actually nice" feeling. It's less "new hooks for fun" and more "hey, those patterns we all hacked together for years? here are real primitives for them."
Three minor releases in under a year. Each one quietly replaced a category of boilerplate we'd accepted as normal. This article covers the highlights from React 19.0 through 19.2 and React Compiler 1.0 — what they are, what problems they solve, and what you need to watch out for when adopting them.
React 19 給了我一種「等等⋯⋯這真的還不錯」的感覺。它不是「為了好玩的新 hooks」,而是「嘿,那些我們 hack 了好多年的模式?現在有正式原語了。」
不到一年,三個 minor release。每一個都悄悄取代了我們早已習以為常的一整類 boilerplate。本文涵蓋 React 19.0 到 19.2 以及 React Compiler 1.0 的重點——它們是什麼、解決了什麼問題、以及採用時需要注意什麼。
Release Timeline
發布時間線
| Version | Date | Headline |
|---|---|---|
| React 19.0 | Dec 2024 | Actions, use(), Server Components, form handling |
| React 19.1 | Mar 2025 | Owner Stacks, Suspense improvements, useId format change |
| React 19.2 | Oct 2025 | <Activity>, useEffectEvent, Performance Tracks, Partial Pre-rendering |
| Compiler 1.0 | Oct 2025 | Automatic memoization at build time (separate package) |
| 版本 | 日期 | 主要變更 |
|---|---|---|
| React 19.0 | 2024 年 12 月 | Actions、use()、Server Components、form 處理 |
| React 19.1 | 2025 年 3 月 | Owner Stacks、Suspense 改進、useId 格式變更 |
| React 19.2 | 2025 年 10 月 | <Activity>、useEffectEvent、Performance Tracks、Partial Pre-rendering |
| Compiler 1.0 | 2025 年 10 月 | Build-time 自動 memoization(獨立套件) |
1. Actions: Finally, Boring CRUD Is Actually Boring
1. Actions:終於,無聊的 CRUD 真的變無聊了
Before React 19, every form submission needed manual state orchestration: isLoading, error, data, setIsLoading(true), try/catch, setIsLoading(false). Three hooks and a reducer for a "save" button.
Actions (useActionState, form actions, useOptimistic) replace this ceremony with a single primitive.
useActionState
import { useActionState } from 'react';
async function saveItem(prevState, formData) {
const name = formData.get('name');
const error = await api.saveItem({ name });
if (error) return { error };
return { error: null, success: true };
}
function ItemForm() {
const [state, dispatch, isPending] = useActionState(saveItem, { error: null });
return (
<form action={dispatch}>
<input name="name" />
<button disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
{state.error && <p className="error">{state.error}</p>}
</form>
);
}Key behaviors:
- The reducer is async — side effects are first-class, not hacks
isPendingcomes for free — no manualuseStatefor loading state- Actions are queued sequentially — each receives the previous call's result, preventing race conditions
<form action={fn}>auto-resets uncontrolled inputs on success
useOptimistic
For instant UI feedback while the server catches up:
const [optimistic, setOptimistic] = useOptimistic(
{ liked: false, count: 42 },
(current, liked) => ({
liked,
count: current.count + (liked ? 1 : -1),
})
);
function handleLike() {
startTransition(async () => {
setOptimistic(true); // UI updates immediately
await api.likePost(id); // server catches up
});
// If the API fails, optimistic state auto-reverts
}No cleanup logic. No rollback handlers. React converges to the real state in a single render when the Transition completes.
Gotchas
useActionState's reducer signature is(previousState, actionPayload)— forgettingpreviousStateis the #1 mistakesetOptimisticmust be called insidestartTransitionor an Action — calling it outside does nothing- If the reducer throws, queued actions are skipped entirely
在 React 19 之前,每個表單提交都需要手動的狀態編排:isLoading、error、data、setIsLoading(true)、try/catch、setIsLoading(false)。一個「儲存」按鈕需要三個 hooks 和一個 reducer。
Actions(useActionState、form actions、useOptimistic)用一個原語取代了這整套儀式。
useActionState
import { useActionState } from 'react';
async function saveItem(prevState, formData) {
const name = formData.get('name');
const error = await api.saveItem({ name });
if (error) return { error };
return { error: null, success: true };
}
function ItemForm() {
const [state, dispatch, isPending] = useActionState(saveItem, { error: null });
return (
<form action={dispatch}>
<input name="name" />
<button disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
{state.error && <p className="error">{state.error}</p>}
</form>
);
}關鍵行為:
- Reducer 是 async 的——副作用是一等公民,不是 hack
isPending開箱即用——不需要手動useState追蹤載入狀態- Actions 排隊順序執行——每個接收前一個呼叫的結果,防止 race condition
<form action={fn}>成功後自動 reset 非受控 input
useOptimistic
在伺服器還沒回應時,先給 UI 即時回饋:
const [optimistic, setOptimistic] = useOptimistic(
{ liked: false, count: 42 },
(current, liked) => ({
liked,
count: current.count + (liked ? 1 : -1),
})
);
function handleLike() {
startTransition(async () => {
setOptimistic(true); // UI 立即更新
await api.likePost(id); // 伺服器跟上
});
// 如果 API 失敗,樂觀狀態自動回滾
}不需要清理邏輯。不需要回滾 handler。Transition 結束時,React 在單次 render 中收斂到真實狀態。
注意事項
useActionState的 reducer 簽名是(previousState, actionPayload)——忘記previousState是最常見的錯誤setOptimistic必須在startTransition或 Action 內呼叫——在外面呼叫不會有任何效果- 如果 reducer 拋出錯誤,排隊中的 actions 會被整個跳過
2. <Activity>: Hide UI, Keep Everything
2. <Activity>:隱藏 UI,保留一切
We've all written this pattern: a tab component that conditionally renders content, destroying state every time the user switches tabs. Then we add workarounds — lifting state up, caching in a ref, wrapping in a context — all to preserve what should have been preserved naturally.
<Activity> is the official answer. It hides UI with display: none without unmounting, preserving React state and DOM state (scroll position, form inputs, video playback).
function TabContainer({ activeTab }) {
return (
<>
<Activity mode={activeTab === 'editor' ? 'visible' : 'hidden'}>
<CodeEditor /> {/* scroll position, cursor, undo history preserved */}
</Activity>
<Activity mode={activeTab === 'preview' ? 'visible' : 'hidden'}>
<Preview />
</Activity>
</>
);
}What Happens When Hidden
| Aspect | Behavior |
|---|---|
| Visibility | display: none |
| React state | Preserved |
| DOM state (inputs, scroll) | Preserved |
| Effects | Cleaned up (unmounted) |
| Updates | Deferred until idle |
The effect cleanup is the key design decision. When hidden, effects (subscriptions, timers, network connections) are torn down — just like unmounting. But the DOM and React state survive. When the component becomes visible again, effects re-mount with the latest state.
Pre-rendering Content
Because hidden Activity boundaries still participate in Suspense, you can pre-load data for pages the user hasn't visited yet:
<Activity mode={activeTab === 'analytics' ? 'visible' : 'hidden'}>
<Suspense fallback={<Skeleton />}>
<AnalyticsPage /> {/* data fetches even while hidden */}
</Suspense>
</Activity>Note: only Suspense-enabled data sources (use(), lazy(), Relay) are pre-fetched. Data fetched in useEffect is not, because effects don't mount when hidden.
Gotchas
Media elements keep playing — the DOM is preserved, not destroyed. You need explicit cleanup:
tsxuseLayoutEffect(() => { const video = ref.current; return () => { video.pause(); }; }, []);Use
useLayoutEffect(notuseEffect) so the pause is synchronous with the visual hide.Effect-based initialization needs restructuring — if you relied on an effect running once to set up state, that setup will happen again every time the component becomes visible.
我們都寫過這種模式:一個 tab 元件用條件渲染切換內容,每次使用者切換 tab 就銷毀狀態。然後我們加上各種 workaround——狀態上提、ref 快取、context 包裝——全都是為了保留本來就應該被保留的東西。
<Activity> 是官方的答案。它用 display: none 隱藏 UI 但不 unmount,同時保留 React state 和 DOM state(捲軸位置、表單輸入、影片播放進度)。
function TabContainer({ activeTab }) {
return (
<>
<Activity mode={activeTab === 'editor' ? 'visible' : 'hidden'}>
<CodeEditor /> {/* 捲軸位置、游標、undo 歷史全部保留 */}
</Activity>
<Activity mode={activeTab === 'preview' ? 'visible' : 'hidden'}>
<Preview />
</Activity>
</>
);
}隱藏時的行為
| 面向 | 行為 |
|---|---|
| 可見性 | display: none |
| React state | 保留 |
| DOM state(input、scroll) | 保留 |
| Effects | 被清除(unmounted) |
| Updates | 延遲到 idle |
Effect 清除是關鍵的設計決策。隱藏時,effects(訂閱、timer、網路連線)會被拆除——就像 unmounting 一樣。但 DOM 和 React state 存活下來。當元件重新變為可見時,effects 用最新狀態重新 mount。
預渲染內容
因為隱藏的 Activity boundary 仍然參與 Suspense,你可以為使用者尚未造訪的頁面預先載入資料:
<Activity mode={activeTab === 'analytics' ? 'visible' : 'hidden'}>
<Suspense fallback={<Skeleton />}>
<AnalyticsPage /> {/* 即使隱藏也會發起資料請求 */}
</Suspense>
</Activity>注意:只有 Suspense 相容的資料來源(use()、lazy()、Relay)會被預載。在 useEffect 中 fetch 的資料不會,因為隱藏時 effects 不會 mount。
注意事項
Media 元素會繼續播放——DOM 是被保留的,不是銷毀的。你需要明確的清理:
tsxuseLayoutEffect(() => { const video = ref.current; return () => { video.pause(); }; }, []);使用
useLayoutEffect(不是useEffect),讓暫停與視覺隱藏同步。依賴 effect 初始化的邏輯需要重構——如果你依賴 effect 只執行一次來設定狀態,那個設定會在元件每次變為可見時再次執行。
3. useEffectEvent: Read Latest Values Without Re-syncing
3. useEffectEvent:讀取最新值而不觸發重新同步
This hook solves a specific, common annoyance: your Effect needs to read the latest value of a prop or state variable, but you don't want that value to cause the Effect to re-synchronize.
The Problem
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => {
showNotification('Connected!', theme); // need latest theme
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // ← theme change = reconnect. Why?
}Every time theme changes, the chat room disconnects and reconnects. The user sees a flash. But we only need theme for the notification style, not for the connection logic.
The Solution
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme); // always reads latest theme
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => onConnected());
connection.connect();
return () => connection.disconnect();
}, [roomId]); // only roomId — theme changes don't reconnect
}useEffectEvent creates a function that always reads the latest committed render values but has an intentionally unstable identity — it changes every render, which is why it must never be in dependency arrays or passed to children.
Another Common Pattern: Timers
function Counter({ increment }) {
const [count, setCount] = useState(0);
const onTick = useEffectEvent(() => {
setCount(count + increment); // latest count and increment, always
});
useEffect(() => {
const id = setInterval(() => onTick(), 1000);
return () => clearInterval(id);
}, []); // interval never restarts
}Rules
- Call only at the top level of a component or custom Hook
- Call the returned function only from inside Effects
- Never include in dependency arrays
- Never pass to child components
- Requires
eslint-plugin-react-hooksv6+ for proper linting
這個 hook 解決了一個具體且常見的困擾:你的 Effect 需要讀取某個 prop 或 state 的最新值,但你不希望那個值的變化導致 Effect 重新同步。
問題
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => {
showNotification('Connected!', theme); // 需要最新 theme
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // ← theme 變化 = 重新連線。為什麼?
}每次 theme 變化,聊天室就斷開並重新連線。使用者看到一次閃爍。但我們只需要 theme 來決定通知的樣式,跟連線邏輯無關。
解法
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme); // 永遠讀取最新 theme
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => onConnected());
connection.connect();
return () => connection.disconnect();
}, [roomId]); // 只有 roomId——theme 變化不再觸發重連
}useEffectEvent 建立一個永遠讀取最新 committed render 值的函式,但它有刻意不穩定的 identity——每次 render 都會變,這就是為什麼它絕對不能放在 dependency array 或傳給子元件。
另一個常見模式:Timer
function Counter({ increment }) {
const [count, setCount] = useState(0);
const onTick = useEffectEvent(() => {
setCount(count + increment); // 永遠是最新的 count 和 increment
});
useEffect(() => {
const id = setInterval(() => onTick(), 1000);
return () => clearInterval(id);
}, []); // interval 永遠不會重啟
}規則
- 只能在 component 或 custom Hook 的最頂層呼叫
- 回傳的函式只能在 Effects 內部呼叫
- 絕對不要放入 dependency array
- 絕對不要傳給子元件
- 需要
eslint-plugin-react-hooksv6+ 才有正確的 lint 支援
4. React Compiler 1.0: The End of Memoization Rituals
4. React Compiler 1.0:Memoization 儀式的終結
Let's be honest: useMemo, useCallback, and React.memo were never fun. They were performance tax — code you wrote not because it made your logic clearer, but because React's rendering model demanded it. Code reviews devolved into "you forgot to memoize this callback" debates.
React Compiler 1.0, released October 7, 2025, is a Babel plugin that automatically memoizes components and hooks at build time. It eliminates most manual memoization.
How It Works
- Converts Babel AST into a Control Flow Graph (CFG)-based High-Level Intermediate Representation (HIR)
- Performs data-flow and mutability analysis
- Granularly memoizes values used in rendering
The compiler can do things manual memoization literally cannot:
// The compiler memoizes AFTER a conditional return
// Manual useMemo cannot do this (hooks can't be called conditionally)
export default function ThemeProvider(props) {
if (!props.children) {
return null;
}
// ← useMemo here would violate Rules of Hooks
const theme = mergeTheme(props.theme, use(ThemeContext));
return (
<ThemeContext value={theme}>
{props.children}
</ThemeContext>
);
}Results at Meta
- Initial page loads: up to 12% faster
- Specific user interactions: 2.5x faster
- Memory usage: neutral
Getting Started
npm install --save-exact babel-plugin-react-compiler@latestFramework support:
- Next.js 15.3.1+ — native integration, optimized with swc
- Expo SDK 54+ — enabled by default for new apps
- Vite — via
vite-plugin-react - Rsbuild — supported
What Happens to useMemo/useCallback/React.memo?
- New code: rely on the compiler. Use manual memoization only where you need precise control (e.g., a memoized value used as an effect dependency)
- Existing code: safe to leave in place — the compiler works alongside manual memoization. Remove only after testing.
Gotchas
- Pin the exact version if you lack comprehensive test coverage — future compiler versions may change memoization behavior
- The compiler validates the Rules of React and will skip optimization for components that violate them
- Backward compatible with React 17+ (requires
react-compiler-runtimefor React 17–18)
坦白說:useMemo、useCallback 和 React.memo 從來都不好玩。它們是效能稅——你寫這些程式碼不是因為它讓邏輯更清晰,而是因為 React 的渲染模型逼你這麼做。Code review 常常退化成「你忘了 memoize 這個 callback」的辯論。
React Compiler 1.0,2025 年 10 月 7 日發布,是一個 Babel plugin,在 build time 自動 memoize 元件和 hooks。它消除了大部分手動 memoization。
工作原理
- 將 Babel AST 轉換為基於 Control Flow Graph (CFG) 的 High-Level Intermediate Representation (HIR)
- 進行資料流和可變性分析
- 粒度化 memoize 渲染中使用的值
Compiler 能做到手動 memoization 字面上做不到的事:
// Compiler 在條件 return 之後 memoize
// 手動 useMemo 做不到這件事(hooks 不能在條件分支中呼叫)
export default function ThemeProvider(props) {
if (!props.children) {
return null;
}
// ← 在這裡放 useMemo 會違反 Rules of Hooks
const theme = mergeTheme(props.theme, use(ThemeContext));
return (
<ThemeContext value={theme}>
{props.children}
</ThemeContext>
);
}Meta 的成果
- 初始頁面載入:快了最多 12%
- 特定使用者互動:快了 2.5 倍
- 記憶體使用:持平
開始使用
npm install --save-exact babel-plugin-react-compiler@latest框架支援:
- Next.js 15.3.1+ — 原生整合,用 swc 優化
- Expo SDK 54+ — 新專案預設啟用
- Vite — 透過
vite-plugin-react - Rsbuild — 支援
useMemo/useCallback/React.memo 怎麼辦?
- 新程式碼:信賴 compiler。只在你需要精確控制時使用手動 memoization(例如 memoized 值被當作 effect dependency)
- 既有程式碼:留著是安全的——compiler 會跟手動 memoization 並存。只在測試後才移除。
注意事項
- 釘死版本號碼——如果你缺乏完整的測試覆蓋率,未來的 compiler 版本可能改變 memoization 行為
- Compiler 會驗證 Rules of React,並跳過違反規則的元件的優化
- 向下相容 React 17+(React 17–18 需要額外安裝
react-compiler-runtime)
5. Performance Tracks: React-Specific Timeline in DevTools
5. Performance Tracks:DevTools 中的 React 專用 Timeline
React work has always been hard to find in Chrome DevTools' Performance panel. You'd see anonymous function calls, reconciliation work mixed into the browser's main thread, and no clear way to know which component caused which update.
React 19.2 adds custom timeline tracks that show React-specific scheduling alongside standard browser events.
Scheduler Track
Four subtracks organized by priority:
| Subtrack | What It Shows |
|---|---|
| Blocking | Synchronous updates from user interactions (clicks, keypresses) |
| Transition | Non-blocking work via startTransition |
| Suspense | Suspense boundary work (fallbacks, reveals) |
| Idle | Lowest priority work |
Each entry shows the event that scheduled the update, when the render happened, when the commit happened, and when effects ran. In dev mode, cascading update warnings include stack traces pointing to the responsible component.
Components Track
A flamegraph showing individual component render and effect durations. Labels include Mount, Unmount, Reconnect (Activity), and Disconnect (Activity). Clicking an entry shows changed props (dev mode only).
How to Use
- Open Chrome DevTools → Performance tab
- Record a session
- React tracks appear automatically
For production-like profiling:
import ReactDOM from 'react-dom/profiling'; // instead of 'react-dom/client'React 的工作一直很難在 Chrome DevTools 的 Performance 面板中找到。你會看到匿名函式呼叫、reconciliation 工作混在瀏覽器的 main thread 裡,完全無法判斷哪個元件觸發了哪次更新。
React 19.2 新增了自訂 timeline tracks,在標準瀏覽器事件旁邊顯示 React 專有的排程資訊。
Scheduler Track
四個子軌道,按優先級組織:
| 子軌道 | 顯示內容 |
|---|---|
| Blocking | 來自使用者互動(click、keypress)的同步更新 |
| Transition | 透過 startTransition 的非阻塞工作 |
| Suspense | Suspense boundary 工作(fallback、reveal) |
| Idle | 最低優先級工作 |
每個條目顯示觸發更新的事件、render 時間、commit 時間、以及 effects 執行時間。在 dev mode 下,cascading update 的警告會附上 stack trace,指向負責的元件。
Components Track
一個 flamegraph,顯示每個元件的 render 和 effect 時間。標籤包括 Mount、Unmount、Reconnect(Activity)和 Disconnect(Activity)。點擊條目可以看到變更的 props(僅 dev mode)。
如何使用
- 打開 Chrome DevTools → Performance tab
- 錄製一段 session
- React tracks 自動出現
接近 production 的 profiling:
import ReactDOM from 'react-dom/profiling'; // 取代 'react-dom/client'6. Other Notable Changes
6. 其他值得注意的變更
From 19.0: Foundations
use()hook — read Promises and Context in render, with Suspense integrationrefas a prop — no moreforwardRef; ref cleanup functions supported<Context>as provider — replaces<Context.Provider>- Document metadata —
<title>,<meta>,<link>rendered in components, auto-hoisted to<head> - Hydration error diffs — single error with diff instead of multiple cryptic messages
- Server Components & Server Actions — stable, with
"use server"directive
From 19.2: Refinements
- Batching Suspense boundary reveals for SSR — server-rendered Suspense boundaries are batched for a short window to align client/server. If LCP approaches 2.5s, batching stops to protect Core Web Vitals.
- Partial Pre-rendering — pre-render a static shell to CDN, then resume with dynamic content:js
const { prelude, postponed } = await prerender(<App />, { signal }); // Later: const resumeStream = await resume(<App />, postponed); - cacheSignal (RSC only) — abort cleanup when
cache()lifetime ends - Web Streams for Node.js SSR —
renderToReadableStreamand friends now work in Node.js - eslint-plugin-react-hooks v6 — flat config is the default; legacy config users need
recommended-legacy useIdprefix change — now_r_(was<<r>>in 19.1,:r:in 19.0). May affect snapshot tests.
19.0 的基礎
use()hook — 在 render 中讀取 Promise 和 Context,與 Suspense 整合ref作為 prop — 不再需要forwardRef;支援 ref cleanup function<Context>作為 provider — 取代<Context.Provider>- Document metadata — 在元件中渲染
<title>、<meta>、<link>,自動提升到<head> - Hydration 錯誤 diff — 一個錯誤附帶 diff,而非多個難以理解的訊息
- Server Components 與 Server Actions — 穩定版,使用
"use server"指令
19.2 的精進
- 批量 Suspense boundary reveals(SSR) — server 渲染的 Suspense boundary 會在短暫的時間窗口內批量 reveal,對齊 client/server 行為。如果 LCP 接近 2.5 秒,批量會停止以保護 Core Web Vitals。
- Partial Pre-rendering — 將靜態 shell 預渲染到 CDN,之後 resume 動態內容:js
const { prelude, postponed } = await prerender(<App />, { signal }); // 之後: const resumeStream = await resume(<App />, postponed); - cacheSignal(僅 RSC) — 在
cache()生命週期結束時 abort cleanup - Web Streams 支援 Node.js SSR —
renderToReadableStream等 API 現在可在 Node.js 中使用 - eslint-plugin-react-hooks v6 — flat config 為預設;使用 legacy config 的需要
recommended-legacy useIdprefix 變更 — 現在是_r_(19.1 是<<r>>,19.0 是:r:)。可能影響 snapshot 測試。
7. Migration Checklist
7. 遷移清單
If you're still on React 18, here's what to watch for:
- Breaking changes in 19.0:
defaultPropsfor function components removed (use ES default params),propTypesremoved, string refs removed,ReactDOM.render/ReactDOM.hydrateremoved (usecreateRoot/hydrateRoot),react-dom/test-utilsremoved - ESLint plugin v6: if using legacy config, change
extends: ['plugin:react-hooks/recommended']toextends: ['plugin:react-hooks/recommended-legacy'] - React Compiler: opt-in, incremental adoption. Works alongside existing memoization. Pin exact version.
useIdformat: changed across versions (:r:→<<r>>→_r_). Update snapshot tests.forwardRefremoval path: not removed yet, butrefas a prop is the new standard. Migrate incrementally.
Recommended Adoption Order
- Upgrade to React 19.0, fix breaking changes
- Adopt
useActionStateand form actions for new forms - Add
<Activity>for tab/drawer state preservation - Replace
useRef+useEffectworkarounds withuseEffectEvent - Enable React Compiler on a per-file or per-directory basis
- Use Performance Tracks to validate improvements
如果你還在 React 18,以下是需要注意的事項:
- 19.0 的 breaking changes:function component 的
defaultProps被移除(改用 ES default params)、propTypes被移除、string refs 被移除、ReactDOM.render/ReactDOM.hydrate被移除(改用createRoot/hydrateRoot)、react-dom/test-utils被移除 - ESLint plugin v6:如果使用 legacy config,將
extends: ['plugin:react-hooks/recommended']改為extends: ['plugin:react-hooks/recommended-legacy'] - React Compiler:opt-in,漸進式採用。與既有的 memoization 並存。釘死版本號碼。
useId格式:跨版本變更(:r:→<<r>>→_r_)。更新 snapshot 測試。forwardRef淘汰路線:尚未移除,但ref作為 prop 是新標準。漸進式遷移。
建議的採用順序
- 升級到 React 19.0,修復 breaking changes
- 對新表單採用
useActionState和 form actions - 為 tab/drawer 的狀態保留加入
<Activity> - 用
useEffectEvent取代useRef+useEffect的 workaround - 逐檔案或逐目錄啟用 React Compiler
- 用 Performance Tracks 驗證改善效果