@skyroc/core-state
基于 Jotai 的状态管理封装 — 存储解耦、跨平台、支持 React 组件外访问
概述
@skyroc/core-state 是对 Jotai 的薄层封装,解决三个核心问题:
- 存储解耦:通过 StorageRegistry 将持久化存储从原子定义中分离,应用层注册适配器,库代码按名称引用
- 非 Hook 访问:暴露
globalStore及辅助函数,支持在 axios 拦截器、事件处理器等非组件场景中读写原子 - 部分更新:
atomWithPartial封装 "合并补丁" 模式,内置无操作跳过(no-op skip),避免不必要的重渲染
架构
App Layer @core/state
───────────────────────── ──────────────────────────────────
registerStorage('local', ...) ──► StorageRegistry (Map<name, adapter>)
registerStorage('session', ...) │
▼
<JotaiProvider> globalStore (Jotai createStore)
└─ <Provider store={globalStore}>
▼
createAtomWithStorage(key, val) ─► getStorage('local') ─► jotaiAtomWithStorage
atomWithPartial(initialValue) ─► baseAtom + 派生读写原子(合并 + no-op 检测)
getAtomValue / setAtomValue ─► globalStore.get / globalStore.set(React 外)
updateAtomValue ─► globalStore.get + globalStore.set(函数式)安装
pnpm add @skyroc/core-state jotaiPeer dependencies:jotai >= 2.0.0,react >= 18.0.0
快速上手
1. 注册存储适配器(应用入口)
在应用入口(main.tsx 或初始化文件)完成注册,后续所有原子按名称引用。
import { registerStorage } from '@skyroc/core-state';
import { storage } from '@skyroc/storage'; // 或任意存储工具
// localStorage 适配器
registerStorage('local', {
getItem: key => storage.get(key),
setItem: (key, value) => storage.set(key, value),
removeItem: key => storage.remove(key),
});
// sessionStorage 适配器(手动序列化示例)
registerStorage('session', {
getItem: key => {
const raw = sessionStorage.getItem(key);
return raw ? JSON.parse(raw) : null;
},
setItem: (key, value) => sessionStorage.setItem(key, JSON.stringify(value)),
removeItem: key => sessionStorage.removeItem(key),
});2. 挂载 Provider
import { JotaiProvider } from '@skyroc/core-state';
const App = () => (
<JotaiProvider>
<Router />
</JotaiProvider>
);JotaiProvider 内部将 <Provider> 绑定到包级 globalStore,确保 React 组件内读写与组件外操作共享同一 store 实例。
3. 创建持久化原子
import { createAtomWithStorage } from '@skyroc/core-state';
// 默认使用 'local' 存储
const themeAtom = createAtomWithStorage('theme', { mode: 'light' });
// 使用 session 存储
const tabAtom = createAtomWithStorage('activeTab', 'home', { storageName: 'session' });
// 直传适配器(绕过 registry)
const customAtom = createAtomWithStorage('key', defaultVal, { storage: myAdapter });4. 部分更新原子
import { atomWithPartial } from '@skyroc/core-state';
const uiAtom = atomWithPartial({ siderCollapse: false, mixSiderFixed: false });
// 在组件中使用
const [ui, setUi] = useAtom(uiAtom);
setUi({ siderCollapse: true }); // 只更新 siderCollapse
setUi(prev => ({ siderCollapse: !prev.siderCollapse })); // updater 函数形式
// 值未变时跳过更新,不触发重渲染
setUi({ siderCollapse: true }); // 若当前已是 true,无操作5. React 组件外访问
import { getAtomValue, setAtomValue, updateAtomValue } from '@skyroc/core-state';
// axios 拦截器中读取 token
const token = getAtomValue(authAtom);
// 直接写入
setAtomValue(authAtom, newAuthState);
// 函数式更新(基于当前值)
updateAtomValue(counterAtom, prev => prev + 1);API
JotaiProvider
function JotaiProvider({ children }: PropsWithChildren): JSX.Element将子树绑定到包级 globalStore,是组件外访问能力的前提。通常放在应用根节点。
globalStore
const globalStore: ReturnType<typeof createStore>Jotai store 实例,通过 JotaiProvider 注入到 React 组件树,同时供 getAtomValue / setAtomValue 等在组件外使用。
getAtomValue
function getAtomValue<Value>(atom: Atom<Value>): Value在 React 组件外读取原子当前值。
const currentTheme = getAtomValue(themeAtom);setAtomValue
function setAtomValue<Value, Args extends unknown[], Result>(
atom: WritableAtom<Value, Args, Result>,
...args: Args
): Result在 React 组件外写入原子。泛型覆盖任意写签名的原子,包括 atomWithPartial(接受 PartialUpdater):
// 普通原子
setAtomValue(countAtom, 42);
// atomWithPartial — 同样通过 setAtomValue 传入部分补丁
setAtomValue(uiAtom, { siderCollapse: true });
setAtomValue(uiAtom, prev => ({ siderCollapse: !prev.siderCollapse }));updateAtomValue
function updateAtomValue<Value, Result = unknown>(
atom: WritableAtom<Value, [Value], Result>,
updater: (prev: Value) => Value
): Result针对写签名为 (next: Value) => unknown 的普通原子的函数式更新。若原子使用自定义写签名(如 atomWithPartial),请直接调用 setAtomValue。
updateAtomValue(counterAtom, prev => prev + 1);atomWithPartial
function atomWithPartial<T extends object>(
initialValue: T
): WritableAtom<T, [PartialUpdater<T>], void>
type PartialUpdater<T extends object> = Partial<T> | ((prev: T) => Partial<T>)创建支持部分更新的原子:
- 合并语义:写入时将补丁
Object.assign合并到现有状态 - no-op 跳过:若补丁中所有字段通过
Object.is与当前值相等,跳过写入、不触发订阅者
const adminStateAtom = atomWithPartial({
siderCollapse: false,
mixSiderFixed: false,
theme: 'dark',
});
// 组件外
setAtomValue(adminStateAtom, { siderCollapse: true });
// 组件内
const [state, setState] = useAtom(adminStateAtom);
setState({ theme: 'light' }); // 其余字段保持不变createAtomWithStorage
function createAtomWithStorage<T>(
key: string,
initialValue: T,
options?: CreateAtomWithStorageOptions
): WritableAtom<T, [T | typeof RESET], void>创建持久化原子,委托 jotai/utils 的 atomWithStorage,并在上层处理适配器解析和容错:
参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
key | string | — | 存储键名 |
initialValue | T | — | 存储中无值时的初始值 |
options.storageName | string | 'local' | 从 registry 解析的存储名称,storage 存在时忽略 |
options.storage | AtomStorage | — | 直传适配器,绕过 registry |
options.getOnInit | boolean | true | 是否在原子创建时同步读取存储;SSR 场景可设为 false 避免 hydration 不一致 |
容错行为:
- 适配器
getItem返回null/undefined→ 使用initialValue - 适配器
getItem抛出异常 → 静默回退到initialValue,不中断模块加载
跨标签页同步:
若适配器实现了 subscribe,外部存储变更(如 localStorage 的 storage 事件)会自动推送到原子:
registerStorage('local', {
getItem: key => JSON.parse(localStorage.getItem(key) ?? 'null'),
setItem: (key, value) => localStorage.setItem(key, JSON.stringify(value)),
removeItem: key => localStorage.removeItem(key),
subscribe: (key, callback) => {
const handler = (e: StorageEvent) => {
if (e.key === key) {
callback(e.newValue ? JSON.parse(e.newValue) : null);
}
};
window.addEventListener('storage', handler);
return () => window.removeEventListener('storage', handler);
},
});registerStorage
function registerStorage(name: string, storage: AtomStorage): void注册命名存储适配器。同名重复注册会覆盖之前的适配器。
getStorage
function getStorage(name: string): AtomStorage按名称获取已注册的适配器。
抛出: Error — 若 name 未注册,错误信息格式为:
[core-state] Storage "xxx" is not registered. Call registerStorage("xxx", adapter) before use.hasStorage
function hasStorage(name: string): boolean检查某个名称是否已注册,不抛出异常,适合条件注册场景:
if (!hasStorage('local')) {
registerStorage('local', localStorageAdapter);
}unregisterStorage
function unregisterStorage(name: string): boolean移除指定名称的注册。返回 true 表示该名称存在并已移除,false 表示名称不存在。
__clearStorageRegistry(仅测试)
function __clearStorageRegistry(): void清空全部注册。双下划线前缀表示内部 API,不应在生产代码中使用。
类型参考
AtomStorage
存储适配器接口:
interface AtomStorage {
/** 读取值;键不存在时返回 null / undefined */
getItem: (key: string) => unknown;
/** 写入值 */
setItem: (key: string, value: unknown) => void;
/** 删除键 */
removeItem: (key: string) => void;
/**
* 订阅外部变更(可选)
* 适用于跨标签页同步等场景
* 必须返回取消订阅函数
*/
subscribe?: (key: string, callback: (value: unknown) => void, initialValue: unknown) => () => void;
}序列化约定:getItem 应返回已反序列化的值(非原始字符串),setItem 负责序列化后写入。这避免了将已解析对象再次传入 JSON.parse 的错误。
PartialUpdater
type PartialUpdater<T extends object> = Partial<T> | ((prev: T) => Partial<T>)atomWithPartial 的写参数类型,支持直接传入补丁对象或基于当前值的 updater 函数。
CreateAtomWithStorageOptions
interface CreateAtomWithStorageOptions {
getOnInit?: boolean; // default: true
storage?: AtomStorage;
storageName?: string; // default: 'local'
}设计说明
为什么需要 StorageRegistry
直接在原子定义中引用 localStorage 会产生平台耦合,在 SSR、测试、React Native 等环境中无法运行。Registry 模式让 @core/state 本身不依赖任何平台 API,适配器由应用层在入口注册。
globalStore 与 JotaiProvider 的关系
Jotai 默认使用隐式内部 store,globalStore 是显式创建的同一 store。JotaiProvider 将其注入 React 树,使组件内外操作读写同一份状态。如果不挂载 JotaiProvider,组件内的 useAtom 会使用 Jotai 默认隐式 store,与 getAtomValue / setAtomValue 的 globalStore 不同步。
atomWithPartial 的 no-op 跳过
写入时逐键检查补丁字段是否与当前值 Object.is 相等,若全部相等则不执行 set,不触发订阅者。这对于频繁调用但实际值很少变化的场景(如拖拽时节流更新 UI 状态)有明显收益。
测试
# 从 monorepo 根目录运行
npx vitest run packages/@core/state/__tests__
# 在包目录内运行
cd packages/@core/state && pnpm test
# 含覆盖率报告
pnpm test --coverage测试套件覆盖以下场景:
| 模块 | 覆盖场景 |
|---|---|
atomWithPartial | 初始值、部分更新、累积合并、no-op 引用相等、updater 函数形式 |
createAtomWithStorage | 初始值回退、读取已存在值、写入同步到 storage、直传适配器、未注册名称报错、removeItem、对象值不二次反序列化、subscribe 推送外部变更、subscribe null 回退 |
globalStore | getAtomValue / setAtomValue / updateAtomValue 基本操作、连续调用累积 |
storage-registry | 注册与获取、未注册报错、同名覆盖、多名称隔离、hasStorage、unregisterStorage、__clearStorageRegistry |