Skip to content

Latest commit

 

History

History
323 lines (252 loc) · 9.55 KB

README.zh-CN.md

File metadata and controls

323 lines (252 loc) · 9.55 KB

简体中文 | English

hookstore

NPM version Build Status Coverage Status NPM downloads size React

基于React Hooks的轻量级中心化数据管理方案。

在线查看demo

Edit

特性

  • 轻量级:基于原生Hooks API实现(且仅有5个API),易于学习上手,将Redux/dva中众多的概念(reducer、action、dispatch、effects)简化为action(异步action支持async/await写法)
  • 数据中心化管理:model(s)定义类似dva,支持多model,action和中间件内部使用mutatable方式修改state易于理解(React组件内以immutatable方式访问state,遵循React单向数据流的设计理念)
  • 高性能:useStore的设计参考了react-redux-hooks useSelector,当state变更时只会刷新使用了useStore的组件,不会引起在Fiber tree上的其他节点re-render,且组件在re-render前会经过严格的diff检查,对useContext引起的性能问题做了充分的优化
  • 内置异步action状态监听hook:按需监听异步action的执行状态(pendingerror),并及时将最新状态同步更新到DOM,简化异步编程
  • koa风格的中间件系统

安装

$ npm install hookstore -S
# or
$ yarn add hookstore

使用

更多示例请查看examples目录。

1. 中心化的model(s)定义

// src/models/count.js
export default {
  name: 'count',
  state: {
    count: 0,
  },
  actions: {
    add(n) {
      const { state } = this.ctx;

      state.count += n;
    },
    async asyncAdd(n) {
      const { state } = this.ctx;

      await new Promise(resolve => {
        setTimeout(resolve, 1000);
      });

      state.count += n;
    },
    addx(n) {
      const { state, actions } = this.ctx;

      state.count += n;

      actions.asyncAdd(n);
      // await actions.asyncAdd(n); // use async/await can access asyncAdd() response
    },
  },
}

2. model(s)绑定

import { Provider } from 'hookstore';

import countModel from './models/count';

import Counter from './src/components/Counter';
import List from './src/components/List';

ReactDOM.render(
  <Provider model={countModel}>
    <Counter />
    <Counter />
  </Provider>
  , document.getElementById('root')
);

3. 在组件中访问state和actions

// src/components/Counter.js
import { useStore } from 'hookstore';

export default () => {
  const [ count, actions ] = useStore('count', s => s.count);
  return (
    <div>
      {Math.random()}
      <div>
        <div>Count: {count}</div>
        <button onClick={() => actions.add(1)}>add 1</button>
        <button onClick={() => actions.addx(1)}>add 1 and async add 1</button>
      </div>
    </div>
  );
}

API

<Provider models>

ProviderAPI是对Context.Provider的封装,强烈建议将<Provider>作为应用的根节点,使用后面useStoreuseStatus等自定义hook的组件都必须作为<Provider>的子(孙)节点!

const Root = () => (
  <Provider models={[ model1, model2 ]}>
    ...
  </Provider>
);

ReactDOM.render(<Root />, document.getElementById('root'));

useStore(name, selector?: Function, equalityFn?: Function) => [ selectedValue, actions ]

自定义hook,以元组的形式返回store中最新的state和可安全修改state的actions方法集合。useStore整合了react-redux useSelector()useDispatch()两个hook。

  • name: model名称
  • selector:用于从store里提取所需数据的一个纯函数(不传则返回整个state对象),强烈推荐传入selector按需提取数据,这样可以保证只有被提取的state值改变时组件才会re-render
  • equalityFn:前后两次提取的state值对比函数,只有值改变才会re-render组件,默认效果和react-redux的connect一致(即浅比较),如果需要进行对象的深比较可以考虑使用三方库,如lodash.isEqual

1、直接修改返回的state是不安全的(修改不会被同步更新到组件),只有action函数和中间对state的修改是安全的!
2、actions是一个不可变对象,这意味着可以直接将actions直接传递给子组件而不会引起重新渲染、useMemo或useCallback的deps也无需将actions作为依赖项。

const Component = () => {
  const [ name, actions ] = useStore('foo', s => s.name);
  const [ nested, actions ] = useStore('bar', s => s.nested, _.isEqual);
  // ...
};

useStatus(name/action) => { pending: boolean, error: Error }

useStatus hook,用于监听(异步)action的执行状态,返回pendingerror两个状态,当action正在执行时pending=true,当执行出错时error为具体错误对象,当执行状态发生变化时会同步更新到DOM。

// src/components/CounterWithLoading.js
import { useStore, useStatus } from 'hookstore';

const CounterWithLoading = () => {
  const [ { count }, actions ] = useStore('count', s => c.count);
  const { pending, error } = useStatus('count/asyncAdd');
  const asyncAdd = () => {
    if (pending) return console.log('pls wait...');
    actions.asyncAdd(5);
  };

  return (
    <div>
      {Math.random()}
      <div>
        { pending && <div>loading...<div> }
        { error && <div>{error.message}<div> }
        <div>count: {count}</div>
        <button onClick={asyncAdd}>async add 5</button>
      </div>
    </div>
  );
};

getStore(name[, selector = state => state]) => [ selectedState, actions ]

getStore的参数和返回类型和useStore一致,区别是getStore不是React Hook,因此调用不受Hook Rules的限制(可以在组件外部调用),但要注意useStore没有监听功能,state改变不会引起re-render。

// models/foo.js
import { getStore } from 'hookstore';

export default {
  name: 'foo',
  actions: {
    const [ , barActions ] = getStore('bar'); // access actions from `bar` model
    // ...
  }
}

applyMiddlewares([middleware1, middleware2, ...])

为action添加中间件,写法类似koa中间件

import { Provider, applyMiddlewares } from 'hookstore';
import errorMiddleware from 'hookstore-error';
import loggerMiddleware from 'hookstore-logger';

import countModel from './models/count';
import listModel from './models/list';

import Counter from './src/components/Counter';
import List from './src/components/List';

function Root() {
  useEffect(() => {
    // if (/localhost|\btest\b/.test(location.hostname)) {
    applyMiddlewares([ errorMiddleware(), loggerMiddleware({ showDiff: true }) ]);
    // }
  }, []);

  return (
    <Provider models={[ countModel, listModel ]}>
      <h2>Counter</h2>
      <Counter />
      <Counter />
      <h2>List</h2>
      <List />
    </Provider>
  );
}

ReactDOM.render(<Root />, document.getElementById('root'));

中间件定义:

// middlewares/errHandler.js
export default async (ctx, next) => {
  try {
    await next();
  } catch(e) {
    console.error(`${ctx.name}/${ctx.action}`, e);
  }
}

// use middleware
import errHandler from 'errHandler';

function Root() {
  applyMiddlewares([errHandler]);

  return (
    <Privider model={model}>
      // ...
    </Privider>
  );
}

model定义

model是普通的javascript对象,类型申明:

interface Model {
  readonly name: string, // name of model
  state?: {}, // model state
  actions: {
    [action: string]: ({this: {ctx: Context}}) => any | Promise<any>
  },
}

定义一个model:

// src/models/foo.js
export default {
  name: 'foo', // model name
  actions: {
    setName(newName) {
      this.ctx.state.name = newName;
    },
    async asyncSetName(newName) {
      await new Promise(resolve => setTimeout(resolve, 1000));
      this.ctx.state.name = newName;
    }
  },
}

ctx对象

ctx对象可以在action和middleware中访问,存储store的一些中间状态和方法。

类型申明:

interface Actions {
  [ action: string ]: (...args: any[]) => Promise<any>;
}

interface Context<S = {}> {
  // access current store's name
  readonly name: string,
  // access current action's name
  readonly action: string,
  // access the lastest state in current store
  state: S,
  // access the bound action collection of current store
  actions: Actions,
  // access the lastest state and actions of some other store
  getStore: (name?: string, selector?: StateSelector<S>) => [ any, Actions ],
}

本地运行示例

examples文件夹包含所有可用代码示例,可以通过以下命令运行示例代码:

$ cd examples/[folder] && npm run install && npm start

然后用浏览器打开http://localhost:3000即可。

License

MIT