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

rd-model

v0.1.0

Published

用于简化 Redux 使用的工具库

Downloads

11

Readme

rd-model

rd-model 是一个为了简化 redux 在 react 应用中使用的工具库。

核心功能在于将 reducer 切片进行统一封装,将某个 reducer 切片的初始值定义、动作派发函数、reducer 函数、数据加载,状态订阅等汇总在一起。让你在 react 中使用 redux 就像是在定义和使用一个数据模型一样简单

安装和初始化

pnpm install rd-model

需要在 react 根组件上包裹 ReduxProvider 组件,给 ReduxProvider 传入一个 store 对象来完成初始化。

import ReactDom from 'react-dom';
import { ReduxProvider, initStore } from 'rd-model';
import App from './app';

const { store, updateReducer } = initStore({});

ReactDom.render(
  <ReduxProvider store={store}>
    <App />
  </ReduxProvider>,
  document.getElementById('root')
);

基础示例

1. 定义模型

import {
  initStore,
  createModel,
  PayloadAction,
} from 'rd-model';

const { store, updateReducer } = initStore({});

const countModel = createModel({
  statePaths: ['count_demo'],
  initialState: {
    count: 0,
  },
  reducers: {
    add: (state, action: PayloadAction<number>) => {
      state.count += action.payload;
    },
    minus: (state, action: PayloadAction<number>) => {
      state.count -= action.payload;
    },
  }
});

// 将切片合并到 RootReducer 上
updateReducer({ count_demo: countModel.reducer });

2. 使用模型

这里用基于 jest 的单元测试用例举例

// 1. model.getState
expect(countModel.getState()).toBe(store.getState().count_demo); // pass:

// 2. model.actions
countModel.actions.add(2); // 触发 count_demo/add 动作

expect(countModel.getState().count).toBe(2); // pass

countModel.actions.minus(4); // 触发 count_demo/minus 动作

expect(countModel.getState().count).toBe(-2); // pass

// 3. model.useModel
const wrapper = ({ children }) => (
  <ReduxProvider store={store}>
    {children}
  </ReduxProvider>
);
const { result, waitForNextUpdate } = renderHook(
  () => countModel.useModel((state) => state.count),
  { wrapper },
)

countModel.actions.add(2)
await waitForNextUpdate();
expect(result.current).toBe(0); // pass

基础API

useStore

在函数组件中获取 redux 的 store 对象

const useStore: () => Store<any, AnyAction>

useDispatch

在函数组件中获取 redux 的 dispatch 方法。

const useDispatch: () => Dispatch<AnyAction>

useSelector

在函数组件中订阅 redux 的状态数据。

type Selector<S = unknown, P = unknown> = (state: S) => P;

type UseSelectorOptions<P> = {
  /**  同步订阅,redux 值一更新就马上执行组件的 update 操作,默认为 false */
  sync?: boolean;
  /** 对比方法: 默认浅对比 */
  eq?: IsEqual<P>;
  /** 是否与 React.Suspense 配合使用 */
  withSuspense?: boolean | FunctionLike<[P], boolean>;
}

function useSelector<S = DefaultRootState, P = any>(
  selector: Selector<S, P>, 
  options?: UseSelectorOptions<P>
): P

配置对象说明:

  • sync: 是否立即更新组件状态。默认是否,状态变化时,组件的状态是异步更新的。
  • eq: 新旧状态的对比方法,默认使用浅对比函数。
  • withSuspense: 一个判断函数,判断所选状态是否为加载中,如果是,则抛出一个 promise,直到状态数据加载完成,promise 才会被 resolve 掉。如果传入值 true,则是使用内部自带的判断函数。

核心 API

createModel

function createModel(ModelOptions):Model;

作为 rd-model 的一个核心函数,它接受初始值、reducers方法对象、持久化配置对象和异步数据获取方法对象等作为参数,并返回 Model 对象,对象内包含动作派发、状态订阅、数据加载、reducer 等方法。

type ModelOptions<STATE> {
  /**
   * 当前 reducer 切片的合并路径
   */
  statePaths: string[];
  /**
   * 初始值
   */
  initialState: STATE;
  /**
   * 数据的更新方法
   * 作用:生成 reducer 与 actionDispatcher
  */
  reducers: { [key: string]: (state, action:PayloadAction<any>) => void };
  /**
   * 异步数据的获取方法。
   * 作用:网络防抖、自动管理 pending 状态
   */
  fetch?: { [key in keyof STATE]?: (...args: unknown[]) => Promise<STATE[key]> };
  /**
   * 持久化缓存相关配置
   */
  cacheKey?: string;
  cacheStorage?: 'session'|'local'|Storage;
  cacheVersion?: string;
  /**
   * 订阅当前切片外的动作
   */
  extraReducers?: Record<string, ReducerCase<STATE>> | FunctionType<[Builder<STATE>], void>;
}

PS: 为了实现良好的类型提醒,ModelOptions 的 TS 类型比较复杂,这里为了容易阅读,对其进行了简化。

statePaths

一个字符串数组,表示当前 reducer 切片的合并路径,它可以帮助 model 对象准确地订阅和修改 redux 中的数据。但是它同时要求你在以同样的路径对 model 的 reducer 方法进行嵌套合并。

// 一级嵌套
const testModel = createModel({statePaths: ['test']});
const rootReducer = combineReducers({
  ...otherReducer,
  test: testModel.reducer
});

// 多级嵌套
const testFooModel = createModel({statePaths: ['test', 'foo']});
const testBarModel = createModel({statePaths: ['test', 'bar']});

const rootReducer = combineReducers({
  ...otherReducer,
  test: combineReducers({
    foo: testFooModel.reducer,
    bar: testBarModel.reducer,
  })
});

initialState

reducer 切片的初始值,因为 rd-model 内部使用了 immer 来实现数据不可变,所以,数据模型的初始值必须是一个引用值,而不是简单值。

如果你的 reducer 切片数据仅仅只是一个 number,string 等简单值,那大可不必使用一个数据模型来管理它。

reducers

一个 key-value 对象, 类型定义如下:

type CaseReducers = {
  [key: string]: (state, action:PayloadAction<any>) => void 
};

这实际上是对 reducer 函数 switch-case 写法的升级,每个 key 对应的方法代表对当前切片数据的一种修改。

这些方法会使用 immer 进行包装,所以你可以在函数内直接对 state 进行赋值修改,无需像以往一样通过拓展语法去返回一个新的对象。

rd-model 可以由该对象计算出最终的 reducer 方法,以及生成对应的 actionDispatcher 方法。

你需要对 action 参数通过 PayloadAction 指定类型来帮助 rd-model 进行类型推导。

fetch

指定模型对象中某个字段数据的获取方法(异步)。如果数据模型中某个字段的值是通过非分页请求获取的,那么你可以通过 fetch 字段进行配置。


/** redux 定义 **/
type BusinessData = {/*...*/}

const dataModel = createModel({
  initialState: {
    // 其他字段 。。。
    businessData: null as BusinessData,
  },
  // 其他配置 。。。
  fetch: {
    // 定义 businessData 的获取方法
    businessData: fetchBusinessData, // as (id: string) => Promise<BusinessData>
  }
});


/** 业务组件 **/
import { isPending } from 'rd-model';

function BusinessComponent(props) {
  
  useEffect(() => {
    // 直接调用 fetch.businessData
    // rd-model 内部会自动维护加载状态的更新
    dataModal.fetch.businessData(props.id);
  }, [props.id]);

  // 状态数据订阅
  const businessData = dataModal.useModel(state => state.businessData);

  // 通过 isPending 直接判定是否处于加载中状态。
  if (isPending(businessData) || !businessData) {
     return <Loading />
  }
  return <Display data={businessData} />
}

持久化缓存

当你不希望某些 Redux 数据随着页面刷新就丢失时,你就可以将它们下沉到 localStoragesessionStorage 中,以达到持久化的目的。createModel 提供了三个用于持久化缓存的配置字段:

  • cacheStorage?: 'session'|'local'|Storage;:配置存储对象。
  • cacheKey?: string;: 存储用的 key 值,需要维护其唯一性。
  • cacheVersion?: string;:缓存的版本号,一般用于避免代码版本的升级导致数据结构的冲突。

extraReducers

订阅当前切片外的动作。

const dataModel = createModel({
  initialState,
  // other config ...
  extraReducers: {
    "root-reset": (state, action) => {
      return initialState;
    }
  }
});

Model

Model 对象包含对当前 reducer 切片状态数据的所有订阅和更新方法。它拥有完善的类型提醒,使用也非常简单。

type Selector<S, s> = (state: S) => s;
type UseModelOption = {
  withSuspense?: boolean | ((subState: any) => boolean);
  eq?: (a, b) => boolean;
};
type Model<STATE> = {
  /** 获取切片状态数据 */
  getState: () => STATE;
  /** 组件内订阅切片状态数据 */
  useModel: <T>(selector: Selector<STATE, T>, options?: UseModelOption) => T;
  /** actions dispatchers */
  actions: Record<string, Function>;
  /** data fetchers */
  fetch: Record<string, PromiseFn>;
  /** pure reducer functions */
  reducer: Reducer<STATE>;
}

getState

获取当前 reducer 切片的状态数据。

useModel

在函数组件内订阅当前 reducer 切片的状态数据。在所订阅的状态发生变化时更新当前组件。默认使用浅对比判断状态是否变化。

useModeluseSelector 方法的用法是一致的。区别在于它们订阅的数据范围不同,useModel 是当前切片状态,useSelector是全局状态。

const dataModel = createModel({
  initialState: {
    // other...
    businessData: null as BusinessData,
  },
  // other config ...
  fetch: {
    businessData: fetchBusinessData, // () => Promise<BusinessData>
  }
});

function Children() {
  // 订阅切片状态数据
  const businessData = dataModel.useModel(
    state => state.businessData,
    { withSuspense: true }
  );
  return (<Display data={businessData} />)
}

function Parent() {
  useLayoutEffect(() => {
    dataModel.fetch.businessData()
  }, []);
  return (
    <Suspense fellback={<Loading/>}>
      <Children />
    </Suspense>
  )
}

当用 model.fetch.xxx 去获取异步数据时,withSuspense 配置能减少不少判定逻辑。

因为使用 withSuspense 配置后,当数据处于加载中状态时它会抛出一个 Promise 异常,这个 Promise 会等待加载中状态结束时进行 resolve。 配合 React.Suspense 就可以像获取同步数据一样获取异步数据。

actions

动作派发方法集合对象,调用这些方法就可以直接派发动作,不需要额外调用 bindActionCreators。该对象由 reduces 配置推导而出:

// reducers 定义
type Reducers = {
  setNum: (state, action: PayloadAction<number>) => void;
  reset: (state) => void;
};
// 推导出
type Actions = {
  setNum: (p: number) => void;
  reset: () => void;
}

fetch

状态数据加载方法集合对象,拥有与 fetch 配置对象一样的类型签名,用于获取异步数据。它除了会自动维护数据的加载状态外,还处理了数据竞争的问题。

数据竞争说明: 用户不断变更筛选条件,导致发起多次筛选请求,但这些请求最终都是作用于同一个数据,这个时候,数据的最终结果会变得不可控,网络抖动会导致请求的返回顺序与发起顺序不一致,最终导致界面会展示最慢返回的请求数据,这与用户的期待是不一致的。

虽然使用截流函数能减少此类问题的概率,但是并不能完全杜绝出现的可能性。

reducer

一个纯的 reducer 函数,由 reducers 配置生成,为了让 model 对象能够正常使用,需要将它合并到正确的位置上,详见 statePaths 配置


完整 API 文档地址