npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

raydux

v0.3.4

Published

基于Redux包装的使用Hooks开发的轻量级状态仓库,全TypeScript支持

Readme

目录

快速开始

介绍

Raydux 是一个基于 Redux 的前端状态管理工具。特点是使用类似 React Hooks 的方式构建你的状态管理仓库,并具有轻量化快速的优势。

安装

你可以从 npm 安装:

# NPM
npm install raydux
# Or, use any package manager of your choice.

创建一个 Slice

使用 createSlice 函数创建一个 Slice。第一个参数提供数据切片名称,第二个参数传入一个连续函数,其中第二层函数为 loop 函数。loop 函数会在 Slice 生命周期里被多次执行,以生成最新的数据切片状态。

至于第一层函数的作用,在 slice 初始化章节会介绍。

import { createSlice, takeCallback, takeState } from "raydux";

export const takeFoo = createSlice("foo", () => () => {
  // 定义状态
  const [count, setCount] = takeState(0);

  // 定义操作
  const increment = takeCallback(() => {
    setCount(count + 1);
  }, [count]);

  // 返回结果
  return {
    count,
    increment,
  };
});

使用这个 Slice

在组件中执行 createSlice 返回的 take 函数,状态和操作都被定义在其中,可一同引入。

function Counter() {
  // 状态和操作均可一同引入
  const { count, increment } = takeFoo();

  return (
    <div>
      <h1>count: {count}</h1>
      <button onClick={increment}>+1</button>
    </div>
  );
}

至此你已经完成了一个最简单的案例。当你点击按钮时,count 值会自动+1。

注意:Raydux 虽然基于 Redux 开发,但你无需在项目最外侧包裹 Provider 即可实现数据更新,其原理为在 take 函数内置了 React.useSyncExternalStore 的调用逻辑。

Benchmark

Benchmark 项目地址

对比了 3 款比较火的状态管理工具:

  • Redux toolkit
  • Pinia(Vue 官方推荐代替 VueX)
  • Zustand

同步更新10万次简单数值

得益于 Raydux 的帧尾结算式更新,在同一帧多次修改的示例上表现非常出色,时间复杂度仅为 O(1),性能远高于其他工具(特别是 Redux toolkit)。它们的性能由高到低排序为:

Raydux > Zustand > Pinia >>> Redux toolkit

由于 Redux toolkit 的性能差异较大,其他3款工具性能看不清楚,去掉它再看一遍:

异步更新50次大型数据(20MB+)

因为每次生成大型数据耗时过久,且很占内存,因此仅做了50次更新的测试。可以看到4款工具性能交织非常厉害,基本上不分上下:

API

takeState

声明一个可变状态,会返回一个二元组,第一个元素是值,第二个元素是变更值的函数。

当需要变更状态时执行 setState 函数,传入一个新的值或者一个状态转移函数

// 定义状态,提供初始值 0
const [count, setCount] = takeState(0);

// 传入新值
setCount(1);

// 传入状态转移函数
setCount((count) => count + 1);

无初始值

状态可以不提供初始值,但需要用泛型标出类型,这是初始值默认是 undefined

// 无初始值,用泛型标出类型,默认值是 undefined,变量类型是 number | undefined
const [count, sertCount] = takeState<number>();

状态转移函数

可以通过状态转移函数返回一个新的对象,从而确保基于状态的最新值进行变化。但这样的状态转移逻辑可能会比较繁琐,这时你可以直接修改对象,由框架负责计算变更内容。但需要注意直接修改对象的状态转移函数用法不能有返回值,否则会报错。

// 定义一个比较复杂的状态
const [state, setState] = takeState({
  a: "xxx",
  b: {
    c: [1, 2, 3, 4, 5],
    ...
  },
  ...
});

// 状态转移函数方式,使用解构方式返回新值,逻辑可能会很繁琐
// 比如将 state.b.c 数组的第 3 项修改为 9,逻辑如下:
setState((state) => {
  return {
    ...state,
    b: {
      ...state.b,
      c: [
        ...state.b.c.slice(0, 2),
        9,
        ...state.b.c.slice(3)
      ],
    },
  };
});

// 可以直接修改状态,但不可再返回新状态,否则会报错
// 实现和上面相同的逻辑,但会更简洁
setState((state) => {
  state.b.c[2] = 9;
});

Pure 更新

如果某个变量对性能的需求比较高,可以考虑使用 Pure 更新,它再修改变量的时候不会变异 lastState,而是采用纯粹的 reducer 写法,结尾需要返回一个新状态。

只需要在 setState 的第二个参数传递一个 true 即可开启 Pure 更新.

const [state, setState] = takeState({
  foo: 1,
});

// 第二个参数传递 true 以开启 Pure 更新。这时不可直接修改 state,需要返回新的 state
setState((state) => {
  return {
    ...state,
    foo: state.foo + 1,
  };
}, true);

takeCallback

声明一个函数。当且仅当依赖列表有变化时生成一个新的函数引用。

const [count, setCount] = takeState(0);

// 定义函数,依赖 count。当 count 变化时函数引用会变
const plus = takeCallback(
  (num: number) => {
    // 更新状态,因为有依赖,可直接使用外部变量
    setCount(count + num);
  },
  [count]
);

// 结尾可将函数和状态一同抛出去
return {
  count,
  plus,
};

通过 takeCallback 实现 getter 函数

getter 函数指类似 getSkuById 这种查询函数,可以让数据使用者传入参数以查询 state 中的某个结果。需要将所有依赖项都放在依赖列表中,以保证任意依赖项变化时,函数引用也会变更。这样外部可以根据函数引用是否变更来判断是否需要重新渲染。

const [skuMap, setSkuMap] = takeState({
  "1": {
    skuId: "1",
    skuName: "Sku-1",
  },
  "2": {
    skuId: "2",
    skuName: "Sku-2",
  },
});

// getter 函数依赖 skuMap
const getSkuById = takeCallback(
  (skuId: string) => {
    return skuMap[skuId];
  },
  [skuMap]
);

通过 takeCallback 实现异步函数

实现异步函数同样简单,你可以在任何时刻执行 setState 更新状态。

const [response, setResponse] = takeState<FooResponse>();

const request = takeCallback(async () => {
  // 执行一个异步操作,然后更新状态
  const { data } = await axios.get<FooResponse>("/foo");
  setResponse(data);
}, []);

takeMemo

声明一个 Selector,根据依赖列表计算一个新值。当且仅当依赖列表有变化时会重新计算。

const [count, setCount] = takeState(0);

// 定义 Selector,根据 count 计算出一个新的值
const countText = takeMemo(() => {
  return `the count is ${count}`;
}, [count]);

通过 takeMemo 实现 getter 函数

takeMemo 也可以用来实现与 takeCallback 一样的 getter 函数。它们的区别是 takeMemo 可以对 getter 函数进行包装,例如实现一个包装了防抖功能的 getter 函数。

const [skuMap, setSkuMap] = takeState({
  "1": {
    skuId: "1",
    skuName: "Sku-1",
  },
  "2": {
    skuId: "2",
    skuName: "Sku-2",
  },
});

// getter 函数依赖 skuMap
const getSkuById = takeMemo(() => {
  return debounce((skuId: string) => {
    return skuMap[skuId];
  }, 100);
}, [skuMap]);

takeEffect

执行一个可能函数副作用的函数,结尾可返回一个回收函数,用于回收本次执行遗留的副作用。

当且仅当依赖列表有变化时重新执行函数,重新执行前会先回收上一次的副作用。

const [count, setCount] = takeState(0);

// 执行一个副作用操作,每当 count 变化时会重新执行
takeEffect(() => {
  console.log(`当前的 count 是:${count}`);

  // 延迟 3 秒后自动加 1
  const timeoutId = setTimeout(() => {
    setCount(count + 1);
  }, 3000);

  // 由于延时操作有副作用,需要回收
  return () => {
    clearTimeout(timeoutId);
  };
}, [count]);

任何时候都应该书写副作用回收函数,即使依赖列表是空数组。因为项目是持续迭代的,未来这部分逻辑可能会被其他人修改。为了避免未来依赖列表加入新依赖时忘记回复副作用函数,应该做到有副作用就回收,而不区分是否必要。

const [count, setCount] = takeState(0);

takeEffect(() => {
  const intervalId = setInterval(() => {
    setCount((count) => count + 1);
  }, 3000);

  // 依赖列表为空时,副作用回收函数不会被执行。但为了避免未来可能的问题,还是要书写副作用回收函数
  return () => {
    clearInterval(intervallId);
  };
}, []);

最佳实践 & 典型案例分析

slice 初始化

有的 slice 需要初始化过程,例如请求后端以获取初始状态,从而避免状态具有 undefined 类型而需要频繁进行非空判断。因为界面会在所有 slice 都初始化完毕后才开始渲染

这时可将连续函数的第一层用起来,如下扩展为一个异步函数:

export const takeFoo = createSlice("foo", async () => {
  // 向后端请求初始数据
  const { data } = await axios.get<FooResponse>("/foo");

  // 第一层函数返回 loop 函数
  return () => {
    // 这样定义的 foo 不会有空值,界面无需对其进行非空判断
    const [foo, setFoo] = takeState(data);

    return {
      foo,
    };
  };
});

预加载 Preloading

由于 slice 存在异步初始化的情况,如果界面在 slice 初始化完毕前显示,有可能会碰到空引用错误,所以需要在所有 slice 初始化完毕前阻止界面展示。方式是在你的项目最外侧包裹一个 StartUp 组件,并在其中进行判断,如下:

import { StartUp } from "raydux";

// 将 StartUp 的内容修改为一个函数,其入参中包含 ready 属性,用以判断数据仓库是否准备完毕
createRoot(document.getElementById("app")).render(
  <StartUp>
    {(ctx) => {
      return ctx.ready ? <div>正式逻辑</div> : <div>加载中,请稍候...</div>;
    }}
  </StartUp>
);

懒加载

你的界面可能会采用懒加载方式,在用户访问到时才加载和初始化,这样的界面所依赖的 slice 也会被懒加载。

slice 懒加载发生时,界面会被暂时隐藏,并重新显示 Preloading 界面,等到懒加载的 slice 初始化完毕后重新显示界面。

在开发方面是透明的,无需额外关注 slice 是否被懒加载。

多个 slice 相互依赖

实际情景下经常会有一个 slice 依赖另外一个 slice 的情况。

  • 直接在 loop 函数中使用另外一个 slice 即可,就像在界面中使用一样
// foo-slice.ts
export const takeFoo = createSlice("foo", () => () => {
  const [count, setCount] = takeState(0);

  return {
    count,
    setCount,
  };
});
// another-slice.ts
import { takeFoo } from "./foo-slice";

export const takeAnother = createSlice("another", () => () => {
  // 直接执行 take 函数获取其他 slice 的状态和操作
  const { count } = takeFoo();

  ...
});

执行异步操作后获取最新状态

你可以在任何时候通过执行 take 函数 获取到 slice 的最新状态,包括异步函数结尾:

// foo-slice.ts
export const takeFoo = createSlice("foo", () => () => {
  const [foo, setFoo] = takeState<FooResponse>();

  const fetchFoo = takeCallback(async () => {
    const { data } = await axios.get<FooResponse>("/foo");
    setFoo(data);
  }, []);

  return {
    foo,
    fetchFoo,
  };
});
// ui.tsx
import { takeFoo } from "./foo-slice";

function UI() {
  const { foo, fetchFoo } = takeFoo();

  useEffect(() => {
    // 执行异步操作
    fetchFoo().then(() => {
      // 重新执行 takeFoo 以获取最新状态
      const { foo } = takeFoo();
      console.log("The latest foo state is", foo);
    });
  }, []);
}