使用70行代码配合hooks从新实现react-redux

起因

react-hooks 是 react 官方新的编写推荐,咱们很容易在官方的 useReducer 钩子上进行一层很简单的封装以达到和以往 react-redux \ redux-thunk \ redux-logger 相似的功能,而且大幅度简化了声明。html

react-hooks 的更多信息请阅读 reactjs.org/hooks;react

先看看源码

这 70 行代码是一个完整的逻辑, 客官能够先阅读,或许后续的说明文档也就不须要阅读了。git

  • 简易的实现了 react-redux, redux-thunk 和 redux-logger
  • 默认使用 reducer-in-action 的风格, 也可声明传统的 reducer 风格
import React from 'react';

function middlewareLog(lastState, nextState, action, isDev) {
  if (isDev) {
    console.log(
      `%c|------- redux: ${action.type} -------|`,
      `background: rgb(70, 70, 70); color: rgb(240, 235, 200); width:100%;`,
    );
    console.log('|--last:', lastState);
    console.log('|--next:', nextState);
  }
}

function reducerInAction(state, action) {
  if (typeof action.reducer === 'function') {
    return action.reducer(state);
  }
  return state;
}

export default function createStore(params) {
  const { isDev, reducer, initialState, middleware } = {
    isDev: false,
    reducer: reducerInAction,
    initialState: {},
    middleware: params.isDev ? [middlewareLog] : undefined,
    ...params,
  };
  const AppContext = React.createContext();
  const store = {
    isDev,
    _state: initialState,
    useContext: function() {
      return React.useContext(AppContext);
    },
    dispatch: undefined,
    getState: function() {
      return store._state;
    },
    initialState,
  };
  let isCheckedMiddleware = false;
  const middlewareReducer = function(lastState, action) {
    let nextState = reducer(lastState, action);
    if (!isCheckedMiddleware) {
      if (Object.prototype.toString.call(middleware) !== '[object Array]') {
        throw new Error("react-hooks-redux: middleware isn't Array");
      }
      isCheckedMiddleware = true;
    }
    for (let i = 0; i < middleware.length; i++) {
      const newState = middleware[i](store, lastState, nextState, action);
      if (newState) {
        nextState = newState;
      }
    }
    store._state = nextState;
    return nextState;
  };

  const Provider = props => {
    const [state, dispatch] = React.useReducer(middlewareReducer, initialState);
    if (!store.dispatch) {
      store.dispatch = async function(action) {
        if (typeof action === 'function') {
          await action(dispatch, store._state);
        } else {
          dispatch(action);
        }
      };
    }
    return <AppContext.Provider {...props} value={state} />; }; return { Provider, store }; } 复制代码

reducer-in-action 风格

reducer-in-action 是一个 reducer 函数,这 6 行代码就是 reducer-in-action 的所有:github

function reducerInAction(state, action) {
  if (typeof action.reducer === 'function') {
    return action.reducer(state);
  }
  return state;
}
复制代码

它把 reducer 给简化了,放置到了每个 action 中进行 reducer 的处理。咱们不再须要写一堆 switch,不再须要时刻关注 action 的 type 是否和 redcer 中的 type 一致。chrome

reducer-in-action 配合 thunk 风格,能够很是简单的编写 redux,随着项目的复杂,咱们只须要编写 action,会使得项目结构更清晰。redux

安装

安装 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 钩子的使用方式进行配合。

若是不但愿组件被 store.dispatch() 派发更新,仅读取数据可使用 store.getState(), 这样也能够减小一些没必要要的组件更新。

以上都是理论分析,因为此库和此文档是一个深夜的产物,并无去作性能上的基准测试,因此有人若是愿意很是欢迎帮忙作一些基准测试。

其余例子

随着工做的进展,完善了一些功能, 代码量也上升到了300行,有兴趣的能够去仓库看看:

  • subscribe 添加监听
  • 如使用 autoSave 约定进行 state 的缓存和读取
  • middlewareLog 能够打印 immutable 对象等和状态管理相关的功能

异步 action 而且缓存 state 到浏览器的例子

import React from 'react';
import ReactHookRedux, {
  reducerInAction,
  middlewareLog,
} 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: [middlewareLog], // default is [middlewareLog] 因此可省略
  actions: {}, // default is {} 因此可省略
  autoSave: {
    item: 'localSaveKey',
    keys: ['user'], // 须要缓存的字段
  },
});

// 模拟异步操做
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 from 'react-hooks-redux';
import { Map } from 'immutable';

const { Provider, store } = ReactHookRedux({
  initialState: Map({ products: ['iPhone'] }), // 请确保immutable是一个Map
  isDev: true, // 当发现对象是 immutable时,middleware会遍历属性,使用getIn作浅比较打印 diff的对象
});

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>
  );
}
复制代码

谢谢阅读。

相关文章
相关标签/搜索