前言
三年前,我们还在 Reddit 上吵得不可开交:
"Redux 太啰嗦!" "Zustand 太黑盒!" "Jotai 会内存泄漏!"
今天,React 19 直接把"外挂仓库"拆成了无数颗微状态胶囊 (Micro-State Capsules)------随用随取,随丢随灭。
状态不再集中,而是散落在组件树的最小粒度,靠框架自动合并、同步、持久化。
变化
| 场景 | 2022 痛点 | 2025 体感 | 补充示例 |
|---|---|---|---|
| 数据请求 | 手写 useEffect + swr,缓存键泛滥 |
use(promise) 同步写法,缓存自动化 |
见 3.1 |
| 全局主题 | Context.Provider 层层包裹,重渲染噩梦 |
use(style) 原子化 CSS 变量,0 渲染成本 |
见 3.2 |
| 路由状态 | 路由库各自维护 location,跨页同步靠 hack |
use(navigation) 把路由当状态,页面间共享像 useState |
见 3.3 |
| 客户端持久化 | zustand-persist 手写版本号、迁移逻辑 |
use(storage) 声明式注册,React 后台自动合并、压缩、迁移 |
见 3.4 |
实战
3.1 数据请求:3 行代码搞定"拉取-缓存-重试"
tsx
// 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 命中 缓存-->>组件: 返回缓存数据 else 未命中 React->>服务端: 发起请求 服务端-->>React: 数据 React-->>缓存: 写入缓存 React-->>组件: 返回数据 end
3.2 主题切换:0 行 JavaScript 渲染逻辑
tsx
// DarkModeToggle.tsx
export default function DarkModeToggle() {
const [theme, setTheme] = use(storage('theme', 'light'));
// 样式原子实时注入,不触发 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 路由即状态:语言切换不再刷新整页
tsx
// LangSwitcher.tsx
export default function LangSwitcher() {
// 把 /[lang]/blog 中的 lang 当成状态
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 渲染变更区域。 - 若同一页面有多语言段落,React 会流式返回 diff,首字节时间 < 50 ms。
3.4 持久化迁移:把 Redux Store 搬进"胶囊"
假设你有一个旧 userSlice 结构:
ts
// legacy:userSlice
interface UserSlice {
id: string;
name: string;
vip: boolean;
}
迁移 3 步曲
- 声明兼容类型
ts
// migrate.ts
export const userCapsule = storage<UserSlice>('user', {
id: '',
name: '',
vip: false,
version: 1, // React 会根据 version 自动执行 migrate 函数
});
- 在根组件做一次"搬家中转"
tsx
// App.tsx
function Bootstrap() {
const dispatch = useDispatch();
const legacyUser = useSelector(state => state.user);
// ② 把 Redux 数据写入胶囊,只需一次
const [, setUserCapsule] = use(userCapsule);
useEffect(() => setUserCapsule(legacyUser), [legacyUser]);
return <NextUI />;
}
- 业务组件直接订阅胶囊
tsx
// UserBadge.tsx
export default function UserBadge() {
const user = use(userCapsule); // ← 不再经过 Redux
return <span>{user.vip ? '👑' : '👤'} {user.name}</span>;
}
迁移流程图
graph LR %% 节点定义 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 -->|触发更新| OtherTab OtherTab -->|读取| UserBadge
迁移
- 渐进式切片
把 Redux Store 拆成页面级 slice → 封装toCapsule()转换函数 → 灰度 10 % 用户。 - 双调度共存
旧组件createLegacyRoot()跑旧调度器,新组件createRoot()跑微状态调度器,通过useSyncExternalStore双向同步。 - 类型即契约
一份GlobalState.d.ts映射旧字段 → TypeScript 自动提示"无人订阅"字段 → 安全删除。
结语
当缓存、持久化、路由、样式都被框架做成声明式原语 ,
我们终于可以把 100% 的脑力放在产品逻辑而非"管数据"上。