React官方提供的Context API,提供了一种全局访问状态的方式,结合hooks,可以实现一个使用友好的状态管理工具,适合一般的使用场景。

基础使用

useContext本身的功能是让包裹的子组件都能够全局访问到Context中的值,但“值”在react中有无数可能,完全可以提供一个包含完整state以及对应操作state方法的大对象。通过操作这个对象便可以完成想要的效果了。

首先准备这个状态对象,这里完全可以写成hooks的形式,暴露出对应的状态和操作,里边useState/useReducer等都可以使用:

  1. const useCount = () => {
  2. const [count, setCount] = useState(0)
  3. const increase = () => {
  4. setCount(count + 1)
  5. }
  6. return {
  7. count,
  8. increase
  9. }
  10. }

紧接着需要把这个状态对象放到context中,通过调用准备好的hook就行:

const CountContext = createContext()

const App = () => {
  return (
    <div>
      <CountContext value={useCount()}>
        <SomeComponent />
      </CountContext>
    </div>
  )
}

最后,在<SomeComponent />中使用:

const SomeComponent = () => {
  const { count, increase } = useContext(CountContext);
  return (
    <div>
      <div>{count}</div>
      <button onClick={increase}>+</button>
    </div>
  );
};

整体使用还是非常简单的。

不足之处

上述的实现能实现基本的功能,但是还是有两个不太满意的地方:

  • 得创建两个东西(状态hook和Context对象)还不能忘了把执行hook的值赋给Context.Provider的value prop
  • 如果使用TypeScript的话,类型还要自己写,这就更灾难了

有没有一种方式可以解决上边这个问题呢?
还真有!且看下文。

精妙封装

来自:https://github.com/jamiebuilds/unstated-next
其实我们真正要写的就是那个hook,Context的创建只是不得不做的事情。因此可以自己封装一个createContainer,接收这个hook,返回需要的东西。
需要两个东西:

  • Context.Provider,而且要赋值好状态对象
  • 通过Context获取到状态对象

那么来着手实现一个初级版本:

import React from "react";

function createStore<Value>(hook: () => Value) {
  const Context = React.createContext<Value>();

  function Provider({ children }: { children: React.ReactNode }) {
    return <Context.Provider value={hook()}>{children}</Context.Provider>;
  }

  function useContext() {
    return React.useContext(Context);
  }

  return {
    Provider,
    useContext
  };
}

export default createStore;

可以看出暴露出来两个方法,一个是对Context.Provider做了一层封装,把value设置进去,另一个是把useContext封装了一下,解决了上边提到的两个问题。
使用方法如下:

import { useState } from "react";
import createStore from "./createStore";

const useCount = () => {
  const [count, setCount] = useState(0);
  const increase = () => {
    setCount(count + 1);
  };

  return {
    count,
    increase
  };
};

const CountStore = createStore(useCount);

const SomeComponent = () => {
  const { count, increase } = CountStore.useContext(); // 获取状态
  return (
    <div>
      <div>{count}</div>
      <button onClick={increase}>+</button>
    </div>
  );
};

const Counter2 = () => {
  // 设置状态
  return (
    <CountStore.Provider>
      <SomeComponent />
    </CountStore.Provider>
  );
};

export default Counter2;

最终版本

虽然可以用,但是有些不太友好,需要再加点东西:

  • 初始值
  • TS类型
  • 错误提示

直接看代码吧:

import React from "react";

const EMPTY = Symbol();

interface StoreProviderProps<InitialValue> {
  children: React.ReactNode;
  initialValues: InitialValue;
}

function createStore<Value, InitialValue>(
  hook: (initialValues: InitialValue) => Value
) {
  const Context = React.createContext<Value | typeof EMPTY>(EMPTY);

  function Provider(props: StoreProviderProps<InitialValue>) {
    return (
      <Context.Provider value={hook(props.initialValues)}>
        {props.children}
      </Context.Provider>
    );
  }

  function useContext() {
    const value = React.useContext(Context);
    if (value === EMPTY) {
      throw Error("Component must be wrapped with Store.Provider");
    }
    return value;
  }

  return {
    Provider,
    useContext
  };
}

export default createStore;

TS的类型使用,在这种工具库里边体现的很好,值得学习。