React 狀態(tài)管理的“碎片化”
前言
三年前,我們還在 Reddit 上吵得不可開交:
“Redux 太啰嗦!” “Zustand 太黑盒!” “Jotai 會內(nèi)存泄漏!”
今天,React 19 直接把“外掛倉庫”拆成了無數(shù)顆微狀態(tài)膠囊(Micro-State Capsules)——隨用隨取,隨丟隨滅。
狀態(tài)不再集中,而是散落在組件樹的最小粒度,靠框架自動合并、同步、持久化。
變化
| 場景 | 2022 痛點 | 2025 體感 | 補充示例 |
|---|---|---|---|
| 數(shù)據(jù)請求 | 手寫 useEffect + swr,緩存鍵泛濫 |
use(promise) 同步寫法,緩存自動化 |
見 3.1 |
| 全局主題 | Context.Provider 層層包裹,重渲染噩夢 |
use(style) 原子化 CSS 變量,0 渲染成本 |
見 3.2 |
| 路由狀態(tài) | 路由庫各自維護 location,跨頁同步靠 hack |
use(navigation) 把路由當狀態(tài),頁面間共享像 useState |
見 3.3 |
| 客戶端持久化 | zustand-persist 手寫版本號、遷移邏輯 |
use(storage) 聲明式注冊,React 后臺自動合并、壓縮、遷移 |
見 3.4 |
實戰(zhàn)
3.1 數(shù)據(jù)請求:3 行代碼搞定“拉取-緩存-重試”
// UserCard.tsx
export default function UserCard({ id }: { id: string }) {
// ① 接口返回 Promise,React 自動去重、緩存、過期重驗證
const user = use(fetchUser(id)); // ← 同步寫法,卻具備 swr 全部能力
return (
<article>
<h1>{user.name}</h1>
<img src={user.avatar} alt={user.name} />
</article>
);
}
流程圖:React 19 如何管理 use(promise)
sequenceDiagram
組件->>React: use(promise)
React->>緩存: 命中?
alt 命中
緩存-->>組件: 返回緩存數(shù)據(jù)
else 未命中
React->>服務端: 發(fā)起請求
服務端-->>React: 數(shù)據(jù)
React-->>緩存: 寫入緩存
React-->>組件: 返回數(shù)據(jù)
end
3.2 主題切換:0 行 JavaScript 渲染邏輯
// DarkModeToggle.tsx
export default function DarkModeToggle() {
const [theme, setTheme] = use(storage('theme', 'light'));
// 樣式原子實時注入,不觸發(fā) React 渲染
use(style`
:root {
--bg: ${theme === 'dark' ? '#1e1e1e' : '#ffffff'};
--fg: ${theme === 'dark' ? '#ffffff' : '#1e1e1e'};
}
`);
return (
<button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? '??' : '??'}
</button>
);
}
架構圖:主題膠囊在瀏覽器各線程間的流向
graph TD
A[組件setTheme] -->|序列化| B[Storage Worker]
B -->|BroadcastChannel| C(其他標簽頁)
B -->|IDB| D(磁盤)
C -->|重新讀取| E[CSS 變量]
D -->|下次加載| F[Hydrate]
3.3 路由即狀態(tài):語言切換不再刷新整頁
// LangSwitcher.tsx
export default function LangSwitcher() {
// 把 /[lang]/blog 中的 lang 當成狀態(tài)
const [lang, setLang] = use(navigation().param('lang'));
return (
<select value={lang} onChange={e => setLang(e.target.value)}>
<option value="en">English</option>
<option value="zh">中文</option>
</select>
);
}
關鍵點
- 改變
lang等價于router.push,但 Next.js 15 會只 RSC 渲染變更區(qū)域。 - 若同一頁面有多語言段落,React 會流式返回 diff,首字節(jié)時間 < 50 ms。
3.4 持久化遷移:把 Redux Store 搬進“膠囊”
假設你有一個舊 userSlice 結構:
// legacy:userSlice
interface UserSlice {
id: string;
name: string;
vip: boolean;
}
遷移 3 步曲
- 聲明兼容類型
// migrate.ts
export const userCapsule = storage<UserSlice>('user', {
id: '',
name: '',
vip: false,
version: 1, // React 會根據(jù) version 自動執(zhí)行 migrate 函數(shù)
});
- 在根組件做一次“搬家中轉”
// App.tsx
function Bootstrap() {
const dispatch = useDispatch();
const legacyUser = useSelector(state => state.user);
// ② 把 Redux 數(shù)據(jù)寫入膠囊,只需一次
const [, setUserCapsule] = use(userCapsule);
useEffect(() => setUserCapsule(legacyUser), [legacyUser]);
return <NextUI />;
}
- 業(yè)務組件直接訂閱膠囊
// UserBadge.tsx
export default function UserBadge() {
const user = use(userCapsule); // ← 不再經(jīng)過 Redux
return <span>{user.vip ? '??' : '??'} {user.name}</span>;
}
遷移流程圖
graph LR
%% 節(jié)點定義
ReduxStore["Redux Store"]
Bootstrap["Bootstrap組件<br/>useSelector"]
SetCapsule["setUserCapsule"]
StorageWorker["Storage Worker"]
IndexedDB[(IndexedDB)]
Broadcast["BroadcastChannel"]
OtherTab["其他標簽頁"]
UserBadge["UserBadge組件<br/>use(userCapsule)"]
%% 連線
ReduxStore -->|讀取| Bootstrap
Bootstrap -->|寫入| SetCapsule
SetCapsule --> StorageWorker
StorageWorker -->|持久化| IndexedDB
StorageWorker -->|同步| Broadcast
Broadcast -->|觸發(fā)更新| OtherTab
OtherTab -->|讀取| UserBadge
遷移
- 漸進式切片
把 Redux Store 拆成頁面級 slice → 封裝toCapsule()轉換函數(shù) → 灰度 10 % 用戶。 - 雙調(diào)度共存
舊組件createLegacyRoot()跑舊調(diào)度器,新組件createRoot()跑微狀態(tài)調(diào)度器,通過useSyncExternalStore雙向同步。 - 類型即契約
一份GlobalState.d.ts映射舊字段 → TypeScript 自動提示“無人訂閱”字段 → 安全刪除。
結語
當緩存、持久化、路由、樣式都被框架做成聲明式原語,
我們終于可以把 100% 的腦力放在產(chǎn)品邏輯而非“管數(shù)據(jù)”上。

浙公網(wǎng)安備 33010602011771號