React官方提供的Context API,提供了一种全局访问状态的方式,结合hooks,可以实现一个使用友好的状态管理工具,适合一般的使用场景。
基础使用
useContext本身的功能是让包裹的子组件都能够全局访问到Context中的值,但“值”在react中有无数可能,完全可以提供一个包含完整state以及对应操作state方法的大对象。通过操作这个对象便可以完成想要的效果了。
首先准备这个状态对象,这里完全可以写成hooks的形式,暴露出对应的状态和操作,里边useState/useReducer等都可以使用:
const useCount = () => {
const [count, setCount] = useState(0)
const increase = () => {
setCount(count + 1)
}
return {
count,
increase
}
}
紧接着需要把这个状态对象放到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的类型使用,在这种工具库里边体现的很好,值得学习。