手写一个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,如下:

//创立一个contextconst 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。