react-hooks 是 react 官方新的编写推荐,我们很容易在官方的 useReducer 钩子上进行一层很简单的封装以达到和以往 react-redux \ redux-thunk \ redux-logger 类似的功能,并且大幅度简化了声明。
安装
安装 react-hooks-redux, 需要 react 版本 >= 16.7
yarn add react-hooks-redux
使用
我们用了不到 35 行代码就声明了一个完整的 react-redux 的例子, 拥抱 hooks。
import React from 'react';
import ReactHookRedux from 'react-hooks-redux';
// 通过 ReactHookRedux 获得 Provider 组件和一个 sotre 对象
const { Provider, store } = ReactHookRedux({
isDev: true, // 打印日志
initialState: { name: 'dog', age: 0 },
});
function actionOfAdd() {
return {
type: 'add the count',
reducer(state) {
return { ...state, age: state.age + 1 }; // 每次需要返回一个新的 state
},
};
}
function Button() {
function handleAdd() {
store.dispatch(actionOfAdd()); //dispatch
}
return <button onClick={handleAdd}>add</button>;
}
function Page() {
const state = store.useContext();
return (
<div>
{state.age} <Button />{' '}
</div>
);
}
export default function App() {
return (
<Provider>
<Page />
</Provider>
);
}
总结一下:
准备工作
使用 ReactHookRedux 创建 Provider 组件 和 store 对象
使用 Provide r 包裹根组件
使用
在需要使用状态的地方 使用 store.useContext() 获取 store 中的 state
使用 store.dispatch(action()) 派发更新
我们阅读这个小例子会发现,没有对组件进行 connect, 没有编写 reducer 函数, 这么简化设计是为了迎合 hooks, hooks 极大的简化了我们编写千篇一律的类模板,但是如果我们还是需要对组件进行 connect, 我们又回到了编写模板代码的老路。
middleware 的编写
绝大部分情况,你不需要编写 middleware, 不过它也极其简单。middleware 是一个一维数组,数组中每个对象都是一个函数, 传入了参数并且如果返回的对象存在, 就会替换成 nextState 并且继续执行下一个 middleware。
我们可以使用 middleware 进行打印日志、编写 chrome 插件或者二次处理 state 等操作。
我们看看 middleware 的源码:
let nextState = reducer(lastState, action);
for (let i = 0; i < middleware.length; i++) {
const newState = middleware[i](lastState, nextState, action, isDev);
if (newState) {
nextState = newState;
}
}
return nextState;
性能和注意的事项
性能(和实现上)上最大的区别是,react-hooks-redux 使用 useContext 钩子代替 connect 高阶组件进行 dispatch 的派发。
在传统的 react-redux 中,如果一个组件被 connect 高阶函数进行处理,那么当 dispatch 时,这个组件相关的 mapStateToProps 函数就会被执行,并且返回新的 props 以激活组件更新。
而在 hooks 风格中,当一个组件被声明了 useContext() 时,context 相关联的对象被变更了,这个组件会进行更新。
理论上性能和 react-redux 是一致的,由于 hooks 相对于 class 有着更少的声明,所以应该会更快一些。
所以,我们有节制的使用 useContext 可以减少一些组件被 dispatch 派发更新。
如果我们需要手动控制减少更新 可以参考 useMemo 钩子的使用方式进行配合。
以上都是理论分析,由于此库和此文档是一个深夜的产物,并没有去做性能上的基准测试,所以有人如果愿意非常欢迎帮忙做一些基准测试。
其他例子
异步 action 的例子
import React from 'react';
import ReactHookRedux, { reducerInAction, devLog } from 'react-hooks-redux';
// 通过 ReactHookRedux 获得 Provider 组件和一个 sotre 对象
const { Provider, store } = ReactHookRedux({
isDev: true, // default is false
initialState: { count: 0, asyncCount: 0 }, // default is {}
reducer: reducerInAction, // default is reducerInAction 所以可省略
middleware: [devLog], // default is [devLog] 所以可省略
actions: {}, // default is {} 所以可省略
});
// 模拟异步操作
function timeOutAdd(a) {
return new Promise(cb => setTimeout(() => cb(a + 1), 500));
}
const actions = {
// 如果返回的是一个function,我们会把它当成类似 react-thunk 的处理方式,并且额外增加一个ownState的对象方便获取state
asyncAdd: () => async (dispatch, ownState) => {
const asyncCount = await timeOutAdd(ownState.asyncCount);
dispatch({
type: 'asyncAdd',
// if use reducerInAction, we can add reducer Function repeat reducer
reducer(state) {
return {
...state,
asyncCount,
};
},
});
},
};
function Item() {
const state = store.useContext();
return <div>async-count: {state.asyncCount}</div>;
}
function Button() {
async function handleAdd() {
// 使用 async dispatch
await store.dispatch(actions.asyncAdd());
}
return <button onClick={handleAdd}>add</button>;
}
export default function App() {
return (
<Provider>
<Item />
<Button />
</Provider>
);
}
使用 immutableJS 配合 hooks 减少重渲染的例子
import React, { useCallback } from 'react';
import ReactHookRedux, { createDevLogFromImmutable } from 'react-hooks-redux';
import { Map } from 'immutable';
const { Provider, store } = ReactHookRedux({
isDev: true, // 打印日志
initialState: Map({ products: ['iPhone'] }),
// createDevLogFromImmutable,提前声明getIn对象,可以有效的规避toJS的性能开销
// 例如 createDevLogFromImmutable('user', ['data', 'local'], 'ui', 'products');
middleware: [createDevLogFromImmutable('products')],
});
function actionAddProduct(product) {
return {
type: 'add the product',
reducer(state) {
return state.update('products', p => {
p.push(product);
return [...p];
});
},
};
}
let num = 0;
function Button() {
function handleAdd() {
num += 1;
store.dispatch(actionAddProduct('iPhone' + num)); //dispatch
}
return <button onClick={handleAdd}>add-product</button>;
}
function Page() {
const state = store.useContext();
// 从immutable获取对象,如果products未改变,会从堆中获取而不是重新生成新的数组
const products = state.get('products');
return useCallback(
<div>
<Button />
{products.map(v => (
<div>{v}</div>
))}
</div>,
[products], // 如果products未发生改变,不会进行进行重渲染
);
}
export default function App() {
return (
<Provider>
<Page />
</Provider>
);
}