React Hooks下的全局状态管理

React Hooks

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

动机

  1. React没有提供将可复用性行为“附加”到组件的途径,以往的解决方案是高阶组件。但是这种方法需要重新组织组件结构,造成多层次的组件嵌套,使得代码难以理解。Hook使你在无需修改组件结构的情况下复用状态逻辑

  2. 复杂组件难以维护。组件的生命周期可能包含一些副作用与事件监听,而之后在componentWillUnmount中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法组合在一起。Hook将相互关联的代码拆分成更小的函数(如useEffect)

  3. class中监听事件时常常会因为忘记绑定this造成意料之外的bug。Hook使得React在function组件中可以使用class的特性

解决方案

React Hooks + Redux

Redux

1
// create
2
import { createStore } from 'redux';
3
4
function counterReducer(state = 0, action) {
5
  switch (action.type) {
6
  case 'INCREMENT':
7
    return state + 1;
8
  case 'DECREMENT':
9
    return state - 1;
10
  default:
11
    return state;
12
  }
13
}
14
let store = createStore(counterReducer);
15
16
// use
17
// 可以手动订阅更新,也可以事件绑定到视图层。
18
store.subscribe(() =>
19
  console.log(store.getState())
20
);
21
22
// 改变内部 state 惟一方法是 dispatch 一个 action。
23
// action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行
24
store.dispatch({ type: 'INCREMENT' });
25
// 1
26
store.dispatch({ type: 'INCREMENT' });
27
// 2
28
store.dispatch({ type: 'DECREMENT' });

React-Redux

  1. 创建store,然后使用Provider组件包裹应用使得app能够访问store

    1
    import React from 'react'
    2
    import ReactDOM from 'react-dom'
    3
    4
    import { Provider } from 'react-redux'
    5
    import store from './store'
    6
    7
    import App from './App'
    8
    9
    const rootElement = document.getElementById('root')
    10
    ReactDOM.render(
    11
      <Provider store={store}>
    12
        <App />
    13
      </Provider>,
    14
      rootElement
    15
    )
  2. 在需要使用store的地方connect组件

    1
    import { connect } from 'react-redux'
    2
    import { increment, decrement, reset } from './actionCreators'
    3
    4
    // const Counter = ...
    5
    6
    const mapStateToProps = (state /*, ownProps*/) => {
    7
      return {
    8
        counter: state.counter
    9
      }
    10
    }
    11
    12
    const mapDispatchToProps = { increment, decrement, reset }
    13
    14
    export default connect(
    15
      mapStateToProps,
    16
      mapDispatchToProps
    17
    )(Counter)

React Hook Redux

React Redux自7.1版本之后引入了一系列Hook api用以支持React ook

  1. 同样需要先创建store,然后使用Provider组件包裹应用

  2. 在Function组件中只需要使用useSelector即可获取store中的值,其参数类似于connect中的mapStateToProps,使用useDispatch即可获取dispatch方法,改变状态值,使用useStore获取store的state

    1
    function Test() {
    2
      const result = useSelector((state) => ({
    3
        counter: state,
    4
      }));
    5
      const dispatch = useDispatch();
    6
      return (
    7
        <div>
    8
          {result.counter}
    9
          <button onClick={()=>dispatch({ type: "INCREMENT" })}>+</button>
    10
        </div>
    11
      );
    12
    }
    13
    14
    export default Test;
  3. useSelector第二个参数可以用于自定义比较状态方法,默认比较方法为===

Context api

useContex + useReducer

  1. 手动创建一个Context,然后使用Context.Provider包裹应用,传递的store用useReducer包裹,在Function组件中使用useContext访问store的state和dispacth
    1
    import React, { useReducer } from "react";
    2
    import Context from "./store/context";
    3
    import ContextTest from "./ContextTest";
    4
    import { counterReducer } from "./reducer";
    5
    6
    function App() {
    7
      return (
    8
        <Context.Provider value={useReducer(counterReducer,0)}>
    9
          <ContextTest></ContextTest>
    10
        </Context.Provider>
    11
      );
    12
    }
    13
    14
    export default App;
    15
    16
    17
    //ContextTest.jsx
    18
    import React, { useContext } from "react";
    19
    import Context from "./store/context";
    20
    21
    export default function ContextTest() {
    22
      const [state,dispatch] = useContext(Context);
    23
      return (
    24
        <div>
    25
          {state}
    26
          <button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
    27
        </div>
    28
      );
    29
    }

unstated

unstated

unstated是react社区中比较流行的轻量级的状态管理工具,在api设计的时候沿用react的设计思想,能够快速的理解和上手。
Unstated抛出三个对象,分别是Container、Subscribe和Provider。

  1. 创建store

    1
    import { Container } from "unstated";
    2
    3
    type CounterState = {
    4
      counter:number
    5
    }
    6
    class CounterContainer extends Container<CounterState>{
    7
      state = {
    8
        counter:0
    9
      }
    10
    11
      increment() {
    12
        this.setState({
    13
          counter:this.state.counter+1
    14
        })
    15
      }
    16
    }
    17
    18
    export default CounterContainer
  2. 将应用包裹在unstated导出的Provider中

    1
    import React from "react";
    2
    import { Provider } from "unstated";
    3
    import UnstatedTest from "./unstated/UnstatedTest";
    4
    5
    function App() {
    6
      return (
    7
        <Provider>
    8
          <UnstatedTest></UnstatedTest>
    9
        </Provider>
    10
      );
    11
    }
    12
    13
    export default App;
  3. 在需要使用全局store的子组件使用Subscribe包裹

    1
    import React from 'react'
    2
    import { Subscribe } from 'unstated'
    3
    import CounterContainer from './CounterContainer.ts'
    4
    5
    export default function UnstatedTest() {
    6
      return <Subscribe to={[CounterContainer]}>
    7
        {counter => (
    8
          <div>
    9
            {counter.state.counter}
    10
            <button onClick={()=>counter.increment()}>+</button>
    11
          </div>
    12
        )}
    13
      </Subscribe>
    14
    }

unstated-next

随着react hook的推行,unstated也推出了hook风格的api供使用,即unstated-next

  1. 创建container容器,这里需要使用hook来编写容器状态

    1
    import { createContainer} from "unstated-next"
    2
    import { useState } from "react"
    3
    4
    function useCustomHook() {
    5
      const [counter,setCounter] = useState(0)
    6
      const increment = ()=>setCounter(counter+1)
    7
      return {
    8
        counter,
    9
        increment
    10
      }
    11
    }
    12
    13
    export default createContainer(useCustomHook)
  2. 使用创建的Container包裹应用

    1
      import React, { useReducer } from "react";
    2
      import NextText from "./unstated-next/NextText";
    3
      import CounterContainer from "./unstated-next/CounterContainer";
    4
    5
      function App() {
    6
        return (
    7
          <CounterContainer.Provider>
    8
            {/* <UnstatedTest></UnstatedTest> */}
    9
            <NextText></NextText>
    10
          </CounterContainer.Provider>
    11
        );
    12
      }
    13
    14
      export default App;
    15
    16
    3. 在需要使用的Function组件中使用hook
    17
      ```js
    18
      import React from 'react'
    19
      import { useContainer } from 'unstated-next'
    20
      import CounterContainer from './CounterContainer'
    21
    22
      export default function NextText() {
    23
        const store = useContainer(CounterContainer)
    24
        return <div>{store.counter}
    25
        <button onClick={()=>store.increment()}>+</button></div>
    26
      }

Mobx

mobx 将应用看成一个表格。对象、数组、原型、引用组成了引用的model;所有由state推出来的值(todo转换成的html,未完成的todo)都视作推导;model改变时,响应自动运行,这些函数不产生值,而是自动地执行一些任务;所有改变state的动作称为行动

Mobx

  1. 创建一个store

    1
    import { observable } from "mobx";
    2
    3
    class ObservableCounterStore {
    4
      @observable count = 0;
    5
      increment() {
    6
        this.count += 1;
    7
      }
    8
    }
    9
    10
    export default new ObservableCounterStore();
  2. 将应用包在Provider中

    1
    import React from "react";
    2
    import MobxTest from "./mobx/MobxTest";
    3
    import { inject, Provider } from "mobx-react";
    4
    import CounterStore from "./mobx/CounterStore";
    5
    6
    class App extends React.Component {
    7
      render() {
    8
        return (
    9
          <Provider counter={CounterStore}>
    10
            <MobxTest />
    11
          </Provider>
    12
        );
    13
      }
    14
    }
    15
    16
    export default App;
  3. 需要响应mobx变量响应的组件用inject注入store,observer修饰,即可

    1
    import React from 'react'
    2
    import { observer, inject } from 'mobx-react'
    3
    4
    5
    function MobxTest(props){
    6
        const {counter} = props
    7
        return <div>{counter.count}
    8
          <button onClick={() => counter.increment()}>+</button>
    9
        </div>
    10
    }
    11
    12
    export default inject('counter')(observer(MobxTest))

mobx-react-lite

mobx-react-lite提供了一种无需修饰即可引用mobx store的方法

  1. 创建store同上不再贴代码

  2. 使用React Context将创建的store包裹应用

    1
    import React, { useReducer } from "react";
    2
    import storeContext from "./mobx-hook/storeContext"
    3
    import CounterStore from './mobx-hook/CounterStore'
    4
    import MobxHook from "./mobx-hook/MobxHook";
    5
    6
    class App extends React.Component {
    7
      render() {
    8
        return (
    9
          <storeContext.Provider value={CounterStore}>
    10
            <MobxHook></MobxHook>
    11
          </storeContext.Provider>
    12
        );
    13
      }
    14
    }
    15
    16
    export default App;
  3. 在需要使用store的子组件中用useContext获取store,用useObserver来监听observable变化

    1
    import React, { useContext } from "react";
    2
    import { useObserver } from "mobx-react-lite";
    3
    import storeContext from "./storeContext";
    4
    5
    export default function MobxHook() {
    6
      const store = useContext(storeContext);
    7
    8
      return useObserver(() => (
    9
        <div>
    10
          <p>count: {store.count}</p>
    11
          <button onClick={()=>{store.count = store.count +1}}>+</button>
    12
        </div>
    13
      ))
    14
    }

总结

  1. 大型项目Redux是第一选择,有较好的生态以及middleware,提供可预测的状态管理,但需要编写大量模版代码,且包体积在状态管理根据中较大,适合大型项目使用

  2. context+hook提供了全局状态管理的基本功能,配合useReducer可以实现可预测的状态管理,且不用安装多余的包,适合在简单的项目中使用。但是用Context做Provider有一个问题,就是会导致所有使用到store的子组件产生不必要的渲染

  3. unstated将状态和操作封装在container中,优点是在风格上比redux更贴近react的操作方式,也更容易上手,仅有200b,但是相比context+useReducer的方式没什么优势,除非原来就使用了unstated的项目,否则并不推荐。

  4. mobx使用observable来管理状态,采用代理的方式,支持直接操作对象,相比redux来说更为简单灵活,但是太过灵活可能使得流程不那么清晰,适合中小项目使用