使用 React Hooks + Context 打造一个类vuex语法的简单数据管理。

2020-04-10 17:33:42 浏览数 (1)

React Hooks 是目前社区非常火热的一个新的特性,vue 3.0也引入了hooks,这个特性 在 React16.8 版本正式发布。

这篇文章不过多介绍hooks的基础用法,相关的文章一大堆,个人非常推荐把精读周刊里关于hooks的文章全部看一遍。 前端精读周刊

最近公司做了一个新项目,是后台管理系统,我们没有引入redux,但是其实在某些比较复杂的页面级模块中,组件拆分的层级非常深,所以我想到了可以利用React的Context这个api进行跨层级的数据传递,利用useReducer去做一个简单的store来统一操作模块的数据。

基础用法 Context配合useReducer

先贴一个利用Context配合useReducer的简单示例

定义Store

代码语言:javascript复制
const CountContext = React.createContext();

const initialState = 0;
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment': return state   1;
    case 'decrement': return state - 1;
    case 'set': return action.count;
    default: throw new Error('Unexpected action');
  }
};

const CountProvider = ({ children }) => {
  const contextValue = useReducer(reducer, initialState);
  return (
    <CountContext.Provider value={contextValue}>
      {children}
    </CountContext.Provider>
  );
};

const useCount = () => {
  const contextValue = useContext(CountContext);
  return contextValue;
};
复制代码

组件中使用方法:

代码语言:javascript复制
const Counter = () => {
  const [count, dispatch] = useCount();
  return (
    <div>
      {count}
      <button onClick={() => dispatch({ type: 'increment' })}> 1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>
    </div>
  );
};

const Page = () => (
  <>
    <CountProvider>
      <Counter />
      <Counter />
    </CountProvider>
    <CountProvider>
      <Counter />
      <Counter />
    </CountProvider>
  </>
);
复制代码

很好,很方便,但是useReducer更适用于小型的模块,我们肯定不会每个模块每次使用store都去写这么一段重复的Provider定义代码,所以我们要找出这个模式的痛点,然后进行一些封装~

基础用法的不足

  • 每次引入都要走 createContext -> 定义Provider -> 找一个合适的地方把Provider放上去 这一系列流程。
  • reducer的写法 switch case不是很友好,可读性相对较差。
  • 没有支持异步处理
  • 不支持自动计算依赖state变化的值。

这些缺点是在项目开发中真实体验到的,所以还是有必要去做封装的。

期望的使用方式

编写 store

代码语言:javascript复制
// store.js
import initStore from 'react-hook-store'

const store = {
  // 初始状态
  initState: {
    count: 0,
  },
  // 同步操作 必须返回state的拷贝值
  mutations: {
    // 浅拷贝state
    add(payload, state) {
      return Object.assign({}, state, { count: state.count   1 })
    },
  },
  // 异步操作,拥有dispatch的执行权
  actions: {
    async asyncAdd(payload, { dispatch, state, getters }) {
      await wait(1000)
      dispatch({ type: 'add' })
      // 返回的值会被包裹的promise resolve
      return true
    },
  },
  // 计算属性 根据state里的值动态计算
  // 在页面中根据state值的变化而动态变化
  getters: {
    countPlusOne(state) {
      return state.count   1
    },
  },
}

export const { connect, useStore } = initStore(store)

在页面引用

代码语言:javascript复制
// page.js
import React, { useMemo } from 'react'
import { Spin } from 'antd'
import { connect, useStore } from './store.js'

function Count() {
  const { state, getters, dispatch } = useStore()
  const { countPlusOne } = getters
  const { loadingMap, count } = state
  // loadingMap是内部提供的变量 会监听异步action的起始和结束
  // 便于页面显示loading状态
  // 需要传入对应action的key值
  // 数组内可以写多项同时监听多个action
  // 灵感来源于dva
  const loading = loadingMap.any(['asyncAdd'])

  // 同步的add
  const add = () => dispatch({ type: 'add' })

  // 异步的add
  const asyncAdd = () => dispatch.action({ type: 'asyncAdd' })
  return (
    <Spin spinning={loading}>
      <span>count is {count}</span>
      <span>countPlusOne is {countPlusOne}</span>
      <button onClick={add}>add</button>
      <button onClick={asyncAdd}>async add</button>

      {/** 性能优化的做法 * */}
      {useMemo(
        () => (
          <span>只有count变化会重新渲染 {count}</span>
        ),
        [count]
      )}
    </Spin>
  )
}

// 必须用connect包裹 内部会保证Context的Provider在包裹Count的外层
export default connect(Count)
复制代码

适用场景

比较适用于单个比较复杂的小模块,个人认为这也是 react 官方推荐 useReducer 和 context 配合使用的场景。 由于所有使用了 useContext 的组件都会在 state 发生变化的时候进行更新(context 的弊端),推荐渲染复杂场景的时候配合 useMemo 来做性能优化。

预览地址

codesandbox.io/s/react-hoo…

源码地址

github.com/sl1673495/r…

总结

这是一次简单的封装尝试,虽然已经在生产环境跑起来了,但是覆盖的场景还是比较少,如果有优化的建议和吐槽都欢迎提出来~ 如果有小伙伴对实现的过程感兴趣的话,也可以留言,后续可以增加源码的相关解析。

0 人点赞