共计 5838 个字符,预计需要花费 15 分钟才能阅读完成。
手写一个 mini 版本的 React 状态管理工具
目前在 React 中,有很多各式各样的状态管理工具,如:
- React Redux
- Mobx
- Hox
每一个状态管理工具都有着不尽相同的 API 和应用形式,而且都有肯定的学习老本,而且这些状态管理工具也有肯定的复杂度,并没有做到极致的简略。在开发者的眼中,只有用起来比较简单,那么才会有更多的人去应用它,Vue 不就是因为应用简略,上手快,而风行的吗?
有时候咱们只须要一个全局的状态,防治一些状态和更改状态的函数就足够了,这样也达到了最简化准则。
上面让咱们一起来实现一个最简略的状态管理工具吧。
这个状态管理工具的外围就应用到了 Context API, 在理解本文之前务必先理解并相熟应用这个 API 的用法。
首先咱们来看这个状态管理工具是如何应用的。假如有一个计数器状态,而后咱们通过二个办法别离去批改计数器,也就是做加法和减法,换句话说咱们须要用到一个计数器状态,二个办法来批改这个状态。在 React 函数组件中,咱们应用 useState 办法来初始化一个状态,因而,咱们能够很容易的写出如下代码:
import {useState} from 'react' | |
const useCounter = (initialCount = 0) => {const [count,setCount] = useState(initialCount); | |
const increment = () => setCount(count + 1); | |
const decrement = () => setCount(count - 1); | |
return { | |
count, | |
increment, | |
decrement | |
} | |
} | |
export default useCounter; |
当初,让咱们创立一个组件来应用这个 useCounter 钩子函数,如下:
import React from 'react' | |
import useCounter from './useCounter' | |
const Counter = () => {const { count,increment,decrement} = useCounter(); | |
return ( | |
<div className="counter"> | |
{count} | |
<button type="button" onClick={increment}>add</button> | |
<button type="button" onClick={decrement}>plus</button> | |
</div> | |
) | |
} |
而后在根组件 App 当中应用,如下:
import React from 'react' | |
const App = () => { | |
return ( | |
<div className="App"> | |
<Counter /> | |
<Counter /> | |
</div> | |
) | |
} |
这样,一个计数器组件就功败垂成了,可是真的只是这样吗?
首先,咱们应该晓得计数器组件的状态应该是统一的,也就是说咱们的计数器组件应该是共享同一个状态,那么如何共享同一个状态?这时候就须要 Context 出场了。将以上的组件革新一下,咱们将状态放在根组件 App 当中初始化,并且传到子组件中去。先批改 App 根组件的代码如下:
新建一个 CounterContext.ts 文件,代码如下:
const CounterContext = createContext(); | |
export default CounterContext; |
import React,{createContext} from 'react' | |
import CounterContext from './CounterContext' | |
const App = () => {const { count,increment,decrement} = useCounter(); | |
return ( | |
<div className="App"> | |
<CounterContext.Provider value={{count,increment,decrement}}> | |
<Counter /> | |
<Counter /> | |
</CounterContext.Provider> | |
</div> | |
) | |
} |
而后在 Counter 组件代码咱们也批改如下:
import React,{useContext} from 'react' | |
import CounterContext from './CounterContext' | |
const Counter = () => {const { count,increment,decrement} = useContext(CounterContext); | |
return ( | |
<div className="counter"> | |
{count} | |
<button type="button" onClick={increment}>add</button> | |
<button type="button" onClick={decrement}>plus</button> | |
</div> | |
) | |
} |
如此一来,咱们就能够共享 count 状态,无论是在多深的子组件当中应用都没有问题,然而这并没有完结,让咱们持续。
尽管这样应用解决了共享状态的问题,可是咱们发现,咱们在应用的时候还要额定的传入一个 context 名,所以咱们须要包装一下,到最初,咱们只须要像如下这样应用:
const Counter = createModel(useCounter); | |
export default Counter; |
const {Provider,useModel} = Counter;
而后咱们的 App 组件就应该是这样:
import React,{createContext} from 'react' | |
import counter from './Counter' | |
const App = () => {const { Provider} = counter; | |
return ( | |
<div className="App"> | |
<Provider> | |
<Counter /> | |
<Counter /> | |
</Provider> | |
</div> | |
) | |
} |
持续批改咱们的 Counter 组件,如下:
import React,{useContext} from 'react' | |
import counter from './Counter' | |
const Counter = () => {const { count,increment,decrement} = counter.useModel(); | |
return ( | |
<div className="counter"> | |
{count} | |
<button type="button" onClick={increment}>add</button> | |
<button type="button" onClick={decrement}>plus</button> | |
</div> | |
) | |
} |
通过以上代码的展现,其实咱们也就明确了,咱们无非是将 useContext 和 createContext 内置到咱们封装的 Model 外面去了。
接下来咱们就来揭开这个状态管理工具的神秘面纱,首先要用到 React 相干的 API,所以咱们须要导入进来。如下:
// 导入类型 | |
import type {ReactNode,ComponentType} from 'react'; | |
import {createContext,useContext} from 'react'; |
接下来定义一个惟一标识,用于确定传入的 Context,并且这个用来确定使用者应用 Context 时是正确应用的。
const EMPTY:unique symbol = Symbol();
接下来咱们要定义 Provider 的类型。如下:
export interface ModelProviderProps<State = void> { | |
initialState?: State | |
children: ReactNode | |
} |
以上咱们定义了 context 的状态类型,是一个泛型,参数就是状态的类型,默认初始化为 undefined 类型,并且定义了一个 children 的类型,因为 Provider 的子节点是一个 React 节点,所以也就定义成 ReactNode 类型。
而后就是咱们的 Model 类型,如下:
export interface Model<Value,State = void> { | |
Provider: ComponentType<ModelProviderProps<State>> | |
useModel: () => Value} |
这个也很好了解,因为 Model 裸露了两个货色,第一个是 Provider,第二个就是 useContext,只是换了一个名字而已,定义这两个的类型就够了。
接下来就是咱们的外围函数 createModel 函数的实现,咱们一步一步来,首先当然是定义这个函数,留神类型,如下:
export const createModel = <Value,State = void>(useHook:(initialState?:State) => Value): Model<Value,State> => {// 外围代码}
以上函数难以了解的应该是类型的定义,咱们 createModel 函数传入一个 hook 函数,hook 函数传入一个状态作为参数,而后返回值就是咱们定义好的 Model 泛型,参数为类型就是咱们定义好的这个函数的泛型。
接下来,咱们要做的天然是创立一个 context,如下:
// 创立一个 context | |
const context = createContext<Value | typeof EMPTY>(EMPTY); |
而后咱们要创立一个 Provider 函数,实质上也是一个 React 组件,如下:
const Provider = (props:ModelProviderProps<State>) => { | |
// 这里应用 ModelProvider 次要是不能和定义的函数名起抵触 | |
const {Provider:ModelProvider} = context; | |
const {initialState,children} = props; | |
const value = useHook(initialState); | |
return (<ModelProvider value={value}>{children}</ModelProvider> | |
) | |
} |
这里也很好了解,实际上就是通过父组件拿到初始状态和子节点,从 context 中拿到 Provider 组件,而后返回即可,留神咱们的 value 是通过传入的自定义 hook 函数包装后的值。
第三步,咱们就须要定义一个 hook 函数拿到这个自定义的 Context,如下:
const useModel = ():Value => {const value = useContext(context); | |
// 这里确定一下用户是否正确应用 Provider | |
if(value === EMPTY){ | |
// 抛出异样,使用者并没有用 Provider 包裹组件 | |
throw new Error('Component must be wrapped with <Container.Provider>'); | |
} | |
// 返回 context | |
return value; | |
} |
这个函数的实现也很好了解,就是获取 context,判断 context 是否正确应用,而后返回。
最初咱们在这个函数外部返回这 2 个货色,即返回 Provider 和 useModel 两个函数。如下:
return {Provider,useModel}
把以上代码全副合并起来,createModel 函数就功败垂成啦。
最初,咱们把所有代码合并起来,这个状态管理工具也就实现了。
// 导入类型 | |
import type {ReactNode,ComponentType} from 'react'; | |
import {createContext,useContext} from 'react'; | |
const EMPTY:unique symbol = Symbol(); | |
export interface ModelProviderProps<State = void> { | |
initialState?: State | |
children: ReactNode | |
} | |
export interface Model<Value,State = void> { | |
Provider: ComponentType<ModelProviderProps<State>> | |
useModel: () => Value} | |
export const createModel = <Value,State = void>(useHook:(initialState?:State) => Value): Model<Value,State> => { | |
// 创立一个 context | |
const context = createContext<Value | typeof EMPTY>(EMPTY); | |
// 定义 Provider 函数 | |
const Provider = (props:ModelProviderProps<State>) => {const { Provider:ModelProvider} = context; | |
const {initialState,children} = props; | |
const value = useHook(initialState); | |
return (<ModelProvider value={value}>{children}</ModelProvider> | |
) | |
} | |
// 定义 useModel 函数 | |
const useModel = ():Value => {const value = useContext(context); | |
// 这里确定一下用户是否正确应用 Provider | |
if(value === EMPTY){ | |
// 抛出异样,使用者并没有用 Provider 包裹组件 | |
throw new Error('Component must be wrapped with <Container.Provider>'); | |
} | |
// 返回 context | |
return value; | |
} | |
return {Provider,useModel}; | |
} |
更近一步,咱们再导出一个 useModel 函数,如下:
export const useModel = <Value,State = void>(model:Model<Value,State>):Value => {return model.useModel(); | |
} |
到目前为止,咱们的整个状态管理工具就实现啦,应用起来也很简略,很多轻量的共享状态我的项目当中咱们也就再也不须要应用像 Redux 这样的比较复杂的状态管理工具了。
当然这个想法也并不是我自己想的,文末已注明起源,本文对源码做了一遍剖析。
源码地址。
PS: 本文源码来自 unstated-next。