Core Docs

@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 jotai

Peer dependenciesjotai >= 2.0.0react >= 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/utilsatomWithStorage,并在上层处理适配器解析和容错:

参数:

参数类型默认值说明
keystring存储键名
initialValueT存储中无值时的初始值
options.storageNamestring'local'从 registry 解析的存储名称,storage 存在时忽略
options.storageAtomStorage直传适配器,绕过 registry
options.getOnInitbooleantrue是否在原子创建时同步读取存储;SSR 场景可设为 false 避免 hydration 不一致

容错行为:

  • 适配器 getItem 返回 null / undefined → 使用 initialValue
  • 适配器 getItem 抛出异常 → 静默回退到 initialValue,不中断模块加载

跨标签页同步:

若适配器实现了 subscribe,外部存储变更(如 localStoragestorage 事件)会自动推送到原子:

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 回退
globalStoregetAtomValue / setAtomValue / updateAtomValue 基本操作、连续调用累积
storage-registry注册与获取、未注册报错、同名覆盖、多名称隔离、hasStorage、unregisterStorage、__clearStorageRegistry

On this page