3
头图

手写一个mini版本的React状态管理工具

目前在React中,有很多各式各样的状态管理工具,如:

每一个状态管理工具都有着不尽相同的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

夕水
5.2k 声望5.7k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。