React Hooks useContext + useReducer实现简易Redux

context api是简化版的redux,他没有redux强大生态体系,结合各自中间件例如thunk或saga,做data fetching或处理side effect,不过单单想存一些share data避免props drilling的问题却绰绰有余。

  • context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法
  • reducer 应是纯函数,根据旧的状态和新的参数计算出最新的状态,其中新的参数来自于 dispatch(新的参数)

所以使用 context 还是 redux 要看需求、看成本、看以后拓展性、可维护性。(react-redux 从 v7.1.0开始也提供了 hooks api 以减少繁琐的高阶组件嵌套)

跟着 React 官网文档,首先去看 高级指引-Context,重点理解 React.createContext(initialValue) 方法返回的 Provider 和 Consumer 组件,这是生产者和消费者模型。

知道 context 在函数式组件中基本用法后,去看 useContext,重点去感受使用了 useContext 后,代替了之前 <MyContext.Consumer> 这种写法,令 Context 的引用变得更加方便。

接下来假设你本身已经使用过 Redux,对 Redux 那一套流程有基本的了解,Redux 本身是可以脱离 React 运行的,这是个用于全局状态管理的库,你当然可以结合 JQuery 来使用,但目前来看,之所以这么流行,还是因为结合了 React,所以 Redux 在 React 中主要依赖了 react-redux 这个库。

跟着 React 官方文档,了解 useReducer 的用法,这里也提供了 useReducer 的大概原理。

相信我,尽管可能先从其他地方开始学习 hooks,但是官方文档绝对不要错过。

我们模拟类似 redux 的功能,简单分为三步:

  1. 使用 useReducer 在根组件创建需要共享的 state 和用来更新它的 dispatch()
  2. 使用 context api 把刚才的 state 和 dispatch 同时共享下去
  3. 使用 useContext 方便底层组件读写共享的 state
import React, { useContext, useReducer } from 'react';

const BankContext = React.createContext({});

// 和 redux 一样,综合根据旧 state 和 dispatch 而来的数据,计算出最新的 state
function reducer(state, action) {
  switch (action.type) {
    case 'deposit':
      return { balance: state.balance + action.payload };
    default:
      throw new Error();
  }
}

// 纯粹为了展示 useReducer 第三个参数
function init(initialCount) {
  return { balance: initialCount };
}

// 根组件
export default function App() {
  const [state, dispatch] = useReducer(reducer, 0, init);
  return (
    <BankContext.Provider value={{
      state,
      dispatch // 把 dispatch 也作为 context 的一部分共享下去,从而在嵌套组件中调用以实现更新顶层的 state
    }}>
      <Layout>
        <Content />
      </Layout>
    </BankContext.Provider>
  );
}

// 子组件
function Layout(props) {
  return (
    <div style={{ border: '5px solid lightblue', padding: '20px' }}>
      <p>您就当我是个 Layout 组件吧!</p>
      {props.children}
    </div>
  );
}

// 孙组件
// 经过层层嵌套后,可以在孙组件中读取全局 state 并设置
function Content() {
  // 这里不要误会,useContext(BankContext) 返回值就是我们共享出来的 context,
  // 只是这里刻意把 context 设计为对象,以便同时提供 dispatch
  const { state, dispatch } = useContext(BankContext);
  return (
    <div style={{ border: '1px solid #666' }}>
      <div> 当前余额:{state.balance}</div>
      <button onClick={() => dispatch({ type: 'deposit', payload: 100 })}>存入100元</button>
    </div>
  );
}