Dune Tools Dune Tools Collection

Store 工具

基于 valtio 的状态管理工具,通过分离 actions 和 state 提供更规范的使用方式和更好的类型支持

Store 工具

一个基于 valtio 的状态管理工具,通过分离 actions 和 state 提供更规范的使用方式和更好的类型支持。

基本用法

创建 Store

使用 createStore 函数来创建一个 store,它接收一个配置对象,包含 namestateactionsCreator 三个字段:

import { createStore } from '@dune2/tools/store';

const store = createStore({
  name: 'counter',
  state: {
    count: 0,
    user: { name: 'John' },
  },
  actionsCreator: (state) => ({
    increment: (amount: number) => {
      state.count += amount;
    },
    setUserName: (name: string) => {
      state.user.name = name;
    },
  }),
});

配置说明

  • name: store 的名称,用于调试
  • state: 定义初始状态,必须是一个对象
  • actionsCreator: 定义修改状态的方法,接收 state 参数并返回 actions 对象

状态管理

获取状态

// 获取完整状态
const state = store.getState();

// 在 React 组件中使用
const count = store.useSnapshot();

修改状态

// 使用 actions
store.actions.increment(1);
store.actions.setUserName('Jane');

// 直接修改状态(推荐使用 actions)
store.state.count += 1;

监听状态变化

// 监听状态变化
const unsubscribe = store.subscribe((state) => {
  console.log('状态改变:', state);
});

// 停止监听
unsubscribe();

React 集成

useSnapshot

useSnapshot 用于在组件中订阅状态变化,基于 valtio 的 useSnapshot。它通过 Proxy 自动追踪组件渲染时访问的属性,只有被访问的路径发生变化时才触发重渲染:

function Counter() {
  const state = store.useSnapshot();
  return <div>{state.count}</div>;
}

useShallowSnapshot

useShallowSnapshot 通过 selector 精确选择需要的状态切片,使用 isEqual 深比较来避免不必要的重渲染。返回普通值而非 Proxy 对象:

function Counter() {
  const { count, user } = store.useShallowSnapshot((s) => ({
    count: s.count,
    user: s.user,
  }));
  return <div>{count} - {user.name}</div>;
}

useSnapshot vs useShallowSnapshot

useSnapshotuseShallowSnapshot
追踪方式基于 proxy-compare,自动追踪渲染中访问的属性通过 selector 手动声明需要的状态
返回值Proxy 对象普通值
Hook 开销7 个 hook(含 useMemo、useCallback、useLayoutEffect 等)3 个 hook(useRef + useSyncExternalStore + useDebugValue)
渲染时开销每次属性访问经过 Proxy trap无额外开销
第三方库兼容未知✅ 普通值,无兼容问题
调试体验Proxy 对象在 DevTools 中不直观普通值,调试友好

Babel 插件自动优化

@dune2/babel 提供了编译时插件,自动将 useSnapshot 转换为 useShallowSnapshot。插件会分析组件中实际访问的属性,自动生成最优的 selector:

// 编译前:开发者正常使用 useSnapshot
function Normal() {
  const state = store.useSnapshot();
  return <div>{state.a} -- {state.c.name}</div>;
}

// 编译后:自动转换为 useShallowSnapshot + 生成 selector
function _selector_(state) {
  return { a: state.a, 'c.name': state.c.name };
}
function Normal() {
  const state = store.useShallowSnapshot(_selector_);
  return <div>{state.a} -- {state['c.name']}</div>;
}

解构写法同样支持:

// 编译前
const {
  a,
  c: { name },
} = store.useSnapshot();

// 编译后
const {
  a,
  c: { name },
} = store.useShallowSnapshot(({ a, c: { name } }) => ({
  a,
  c: { name },
}));

插件会保留以下场景不做转换:

  • 状态作为整体传递(如 <Child state={state} />),因为无法静态分析哪些属性被访问
  • 已经手动传入 selector 的调用

调试支持

浏览器调试

Store 工具会在浏览器的 window 对象上注册 __stores2 属性,方便调试:

// 在浏览器控制台中访问所有 store 的状态
console.log(window.__stores2);

// 访问原始的 store 对象
console.log(window.__stores2.__raw);

类型支持

store 的类型会自动推导:

const store = createStore({
  name: 'counter',
  state: { count: 0 },
  actionsCreator: (state) => ({
    increment: (amount: number) => {
      state.count += amount;
    },
  }),
});

// ✅ 类型安全
store.actions.increment(1);

// ❌ 类型错误
store.actions.increment('1'); // Type 'string' is not assignable to type 'number'

On this page