@oldbig/redux-lite
v1.0.15
Published
A lightweight, zero-dependency, type-safe state management library for React.
Maintainers
Readme
redux-lite
一个为 React 量身打造的、零依赖、类型安全、轻量级的状态管理库。
redux-lite 提供了一个现代、简洁且高性能的状态管理方案,旨在通过 TypeScript 提供顶级的开发体验。现在,对您的 React 组件进行单元测试变得简单到超乎想象。
核心特性
- 🚀 零依赖:极其轻量,除了
react作为对等依赖外,无任何第三方运行时依赖。 - ⚡️ 高性能:通过智能的值比较,从设计上避免不必要的组件重复渲染。
- ✨ 简洁直观的 API:极简的 API,易于学习和使用。
- 🔒 完全类型安全:从 store 定义到 dispatchers,提供端到端的类型安全和卓越的自动补全体验。
- ✅ 难以置信的简单测试:灵活的 Provider 让模拟单元测试的 state 变得轻而易举。
- 🐞 DevTools 就绪:可选的、零成本的 Redux DevTools 集成,提供顶级的调试体验。
- 🔌 中间件支持:通过自定义中间件扩展功能,类似于 Redux。
安装
npm install @oldbig/redux-lite
# or
yarn add @oldbig/redux-lite
# or
pnpm add @oldbig/redux-lite快速上手
1. 定义你的初始 store
创建一个 storeDefinition 对象。这个唯一的对象是您整个 state 结构和类型的"真理之源"。
// store.ts
import { initiate, optional } from '@oldbig/redux-lite';
export const STORE_DEFINITION = {
user: {
name: 'Jhon' as string | null,
age: 30,
},
// 对可能不存在的 state 切片使用 `optional`
task: optional({
id: 1,
title: '完成 redux-lite',
}),
counter: 0,
};
export const { ReduxLiteProvider, useReduxLiteStore } =
initiate(STORE_DEFINITION);2. 使用 Provider 包装你的应用
在你的主应用文件中,用 ReduxLiteProvider 来包装你的组件树。
// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { ReduxLiteProvider } from './store';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ReduxLiteProvider>
<App />
</ReduxLiteProvider>
</React.StrictMode>,
);3. 在组件中使用 hook
使用 useReduxLiteStore hook 来访问 state 切片及其对应的 dispatchers。该 hook 返回一个扁平化的对象,包含所有 state 属性和类型安全的 dispatcher 函数。
// MyComponent.tsx
import { useReduxLiteStore } from './store';
const MyComponent = () => {
// 解构 state 和 dispatchers
const {
user,
counter,
dispatchUser,
dispatchPartialUser,
dispatchCounter
} = useReduxLiteStore();
return (
<div>
<h2>用户: {user.name}</h2>
<p>计数器: {counter}</p>
{/* 全量更新 */}
<button onClick={() => dispatchUser({ name: 'Ken', age: 31 })}>
设置用户
</button>
{/* 部分更新 */}
<button onClick={() => dispatchPartialUser({ age: 35 })}>
增加年龄
</button>
{/* 函数式更新,可访问整个 store */}
<button onClick={() => dispatchPartialUser((currentUser, store) => ({ age: currentUser.age + store.counter }))}>
根据计数器增加年龄
</button>
</div>
);
};API
initiate(storeDefinition, options?)
本库唯一的入口点。
storeDefinition: 一个定义了您 store 结构和初始值的对象。options(可选): 用于附加配置的对象。devTools(可选):boolean | { name: string }- 启用或配置 Redux DevTools。middlewares(可选):Middleware[]- 要应用的中间件数组。
- 返回: 一个包含
{ ReduxLiteProvider, useReduxLiteStore, useSelector }的对象。
useReduxLiteStore()
该 hook 返回一个扁平化的对象,其中包含所有 state 切片和 dispatchers。
Dispatchers
对于 state 的每一个切片(例如 user),都会生成两个 dispatcher:
dispatchUser(payload): 用于全量更新。dispatchPartialUser(payload): 用于部分更新。
payload 可以是一个值,也可以是一个函数。如果它是一个函数,它会接收该切片的先前状态作为第一个参数,并接收整个 store 的 state 作为第二个参数:(prevState, fullStore) => newState。
optional(initialValue?)
一个辅助函数,用于将 state 的某个切片标记为可选的。该 state 属性的类型将被推导为 T | undefined。
initialValue(可选): 该属性的初始值。如果未提供,则 state 的初始值为undefined。
useSelector(selector, equalityFn?)
一个用于选择和订阅 state 一部分的钩子,具有性能优化。它类似于 react-redux 中的 useSelector 钩子。
selector:(store: TStore) => TSelected- 一个函数,它接收整个 store 的 state 并返回所选的值。equalityFn(可选):(a: TSelected, b: TSelected) => boolean- 一个用于比较所选值的函数。默认为isEqual(一个深度相等检查)。如果 selector 函数返回的结果与上一次调用相同(通过此相等函数确定),useSelector将返回之前的结果,这有助于防止使用它的组件进行不必要的重新渲染。在大多数情况下,您不需要提供此参数。仅当selector返回的值中包含函数类型的字段时,才需要提供此函数。
何时使用 useSelector?
虽然 useReduxLiteStore 对于访问 state 和 dispatchers 很方便,但对于只需要读取一小部分 state 的性能关键组件,强烈建议使用 useSelector。当 store 的其他部分发生变化时,它可以帮助防止不必要的重新渲染。
示例:
import { useSelector } from './store';
const UserName = () => {
// 这个组件只会在 `user.name` 改变时重新渲染。
const userName = useSelector(store => store.user.name);
return <div>{userName}</div>
}
const UserAge = () => {
// 这个组件只会在 `user.age` 改变时重新渲染。
const userAge = useSelector(store => store.user.age);
return <div>{userAge}</div>
}处理异步操作
redux-lite 通过结合标准的 JavaScript async/await 语法和其 dispatch 的函数式更新形式,以一种优雅而简单的方式处理异步操作。这种模式非常直观、健壮,并且不需要学习任何新的 API。
推荐模式:
- 使用
async/await来处理你的异步逻辑(例如,获取数据)。 - 调用同步的
dispatch函数,并传入一个更新函数来应用结果。这能确保您始终基于最新的 state 工作,从而避免竞态条件。
示例:获取用户并更新 store
import { useReduxLiteStore } from './store';
import { api } from './api';
const UserComponent = () => {
const { user, dispatchUser, dispatchPartialUser } = useReduxLiteStore();
const handleFetchUser = async () => {
try {
// 1. 等待你的 API 返回数据
const fetchedUser = await api.fetchUser(123);
// 2. Dispatch 结果以全量更新 user 切片
dispatchUser(fetchedUser);
} catch (error) {
console.error("获取用户失败:", error);
}
};
const handleIncrementUserAge = async () => {
try {
// 1. 从你的 API 等待需要变更的数据
const { ageIncrement } = await api.fetchAgeIncrement(123); // 假设返回 { ageIncrement: 1 }
// 2. 使用函数式更新,只返回需要变更的部分对象。
// redux-lite 会自动将这个部分对象与现有的 user state 合并。
dispatchPartialUser(currentUser => ({
age: currentUser.age + ageIncrement,
}));
} catch (error) {
console.error("增加用户年龄失败:", error);
}
};
return (
<div>
<p>当前用户: {user.name}</p>
<button onClick={handleFetchUser}>获取用户</button>
<button onClick={handleIncrementUserAge}>增加用户年龄</button>
</div>
);
};这种模式代码清晰,易于测试,并且在不增加任何复杂性的情况下,充分利用了 redux-lite 类型安全的、函数式的 dispatchers 的全部功能。
性能
redux-lite 为高性能而设计。其内部的 reducer 使用了智能的值比较机制,当数据没有实际改变时,可以有效避免不必要的 state 更新和组件重新渲染。
在一个模拟真实世界场景、重复调用 dispatch 函数的基准测试中,redux-lite 能够实现:
- 10,000 次计数器更新约需 16.43 毫秒 (每次更新 0.0016 毫秒)
- 1,000 次数组推送操作约需 3.99 毫秒 (每次操作 0.0040 毫秒)
- 10,000 次对象属性更新约需 15.48 毫秒 (每次更新 0.0015 毫秒)
- 10,000 次部分对象更新约需 15.15 毫秒 (每次更新 0.0015 毫秒)
- 1,000 次深度嵌套更新约需 3.42 毫秒 (每次更新 0.0034 毫秒)
这证明了它即使在包含 React 渲染生命周期的开销下,依然拥有卓越的速度。
与 Redux 对比
| 特性 | Redux (使用 Redux Toolkit) | redux-lite |
| ------------ | -------------------------------------------------------- | ------------------------------------------------------------- |
| 模板代码 | 需要 createSlice, configureStore, actions, reducers。 | 几乎为零。定义一个对象,就能得到你需要的一切。 |
| API 表面 | API 较多,涉及切片、thunks、selectors 等多个概念。 | 极简。只有 initiate, optional 和返回的 hook。 |
| 类型安全 | 良好,但 thunks 和 selectors 可能需要手动指定类型。 | 端到端。所有类型都从初始 store 自动推断。 |
| 性能 | 高性能,但依赖记忆化的 selectors (reselect)。 | 内置。如果值深度相等,自动阻止更新。 |
| 依赖 | 需要 @reduxjs/toolkit 和 react-redux。 | 无。仅 react 作为对等依赖。 |
| 简洁性 | 学习曲线较陡峭。 | 极其简单。如果你了解 React hooks,你就懂得 redux-lite。 |
redux-lite 让测试使用 store 的组件变得极其简单。ReduxLiteProvider 接受一个 initStore prop,它允许你提供一个深度的部分状态(deep partial state)来覆盖测试的默认初始状态。
这意味着你不再需要派发 action 来设置你期望的测试状态。你可以直接用它所需要的确切状态来渲染你的组件。
示例
以下是如何轻松地为你的组件模拟状态:
import { render } from '@testing-library/react';
import { initiate } from '@oldbig/redux-lite';
import React from 'react';
// 假设这是你的初始 store 配置
const STORE_DEFINITION = {
user: { name: 'Guest', age: 0, profile: { theme: 'dark' } },
isAuthenticated: false,
};
const { ReduxLiteProvider, useReduxLiteStore } = initiate(STORE_DEFINITION);
// --- 你的组件 ---
const UserProfile: React.FC = () => {
const { user } = useReduxLiteStore();
return <div>欢迎, {user.name} (主题: {user.profile.theme})</div>;
};
// --- 你的测试 ---
it('应该显示已认证用户的名称,并覆盖了 profile', () => {
const { getByText } = render(
<ReduxLiteProvider initStore={{ user: { name: 'Alice', profile: { theme: 'light' } }, isAuthenticated: true }}>
<UserProfile />
</ReduxLiteProvider>
);
// 组件会使用你提供的确切状态进行渲染
expect(getByText('欢迎, Alice (主题: light)')).toBeInTheDocument();
});
it('应该浅合并 user 切片并替换嵌套对象', () => {
const { getByText } = render(
<ReduxLiteProvider initStore={{ user: { name: 'Bob' } }}>
<UserProfile />
</ReduxLiteProvider>
);
// user.name 被覆盖,user.age 保持默认,user.profile 不受影响
expect(getByText('欢迎, Bob (主题: dark)')).toBeInTheDocument();
});你可以轻松地在不同状态下测试你的组件,而无需任何复杂的设置或模拟。
redux-lite 提供了与 Redux DevTools 浏览器插件 的可选集成,为您提供顶级的调试体验,包括 action 追踪和时间旅行调试。
该功能默认禁用,在不使用时性能开销为零。
如何启用
要启用此功能,只需在 initiate 函数中传入 devTools 选项。
// 使用默认配置启用
const { ReduxLiteProvider, useReduxLiteStore } = initiate(STORE_DEFINITION, {
devTools: true
});
// 或为您的 store 实例提供一个名称和其他选项
const { ReduxLiteProvider, useReduxLiteStore } = initiate(STORE_DEFINITION, {
devTools: {
name: 'MyAppStore',
maxAge: 50, // 限制存储的 action 数量
latency: 500 // 批量处理 action 的延迟时间(毫秒)
}
});安装步骤
- 为您的浏览器安装 Redux DevTools 插件:
- 如上所示,在您的代码中启用该功能。
- 打开浏览器的开发者工具,找到 "Redux" 标签页。

redux-lite 支持与 Redux 几乎完全相同的中间件 API,允许您扩展 store 的能力,用于日志记录、处理异步 action 等。
如何使用中间件
在调用 initiate 时,在 options 对象中传递一个中间件数组。
import { initiate, Middleware } from '@oldbig/redux-lite';
const logger: Middleware<any> = (api) => (next) => (action) => {
console.log('dispatching', action);
const result = next(action);
console.log('next state', api.getState());
return result;
};
const { ReduxLiteProvider, useReduxLiteStore } = initiate(STORE_DEFINITION, {
middlewares: [logger]
});编写自定义中间件
中间件是一个高阶函数,其签名如下:
type Middleware<S> = (api: MiddlewareAPI<S>) => (next: (action: Action<S>) => Action<S>) => (action: Action<S>) => Action<S>;api: 一个包含两个方法的对象:getState(): 返回当前 state。dispatch(action): 派发一个 action。这会将 action 发送到中间件链的开始。
next: 一个将 action 传递给链中的下一个中间件的函数。您必须在某个时刻调用next(action),以确保 action 最终能到达 reducer。action: 正在派发的 action。
重要的中间件最佳实践
- 避免无限循环:在中间件中调用
api.dispatch(action)会将 action 重新发送到中间件链的开头。为了避免无限循环,必须将api.dispatch调用放在适当的条件块中:
const conditionalDispatchMiddleware: Middleware<any> = (api) => (next) => (action) => {
// 错误的做法 - 这会导致无限循环
// api.dispatch({ type: 'someAction', payload: 'data', isPartial: false });
// 正确的做法 - 将 dispatch 放在条件块中
if (action.type === 'user_login') {
api.dispatch({ type: 'notifications_show', payload: '欢迎!', isPartial: false });
}
return next(action);
};错误处理:将中间件逻辑包装在 try-catch 块中,以防止一个有问题的中间件破坏整个链条。
性能:尽量减少中间件中的重量级计算,因为它们是同步运行的,可能会阻塞 UI 线程。
示例
支持本项目
如果您觉得 redux-lite 对您有帮助,并希望支持本项目的开发,请考虑:
非常感谢您的支持!
许可证
本项目采用 MIT 许可证。
