简体中文 | English
基于React Hooks的轻量级中心化数据管理方案。
- 轻量级:基于原生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的执行状态(
pending
和error
),并及时将最新状态同步更新到DOM,简化异步编程 - koa风格的中间件系统
$ npm install hookstore -S
# or
$ yarn add hookstore
更多示例请查看examples目录。
// 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
},
},
}
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')
);
// 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>
);
}
Provider
API是对Context.Provider的封装,强烈建议将<Provider>
作为应用的根节点,使用后面useStore
或useStatus
等自定义hook的组件都必须作为<Provider>
的子(孙)节点!
const Root = () => (
<Provider models={[ model1, model2 ]}>
...
</Provider>
);
ReactDOM.render(<Root />, document.getElementById('root'));
自定义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
hook,用于监听(异步)action的执行状态,返回pending
和error
两个状态,当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
的参数和返回类型和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
// ...
}
}
为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
是普通的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
对象可以在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即可。
MIT