Handwriting a mini version of React state management tool
Currently, there are many various state management tools in React, such as:
Each state management tool has different APIs and usage methods, and all have a certain learning cost. Moreover, these state management tools also have a certain degree of complexity and are not extremely simple. In the eyes of developers, only if it is easier to use, more people will use it. Isn’t Vue popular because it is easy to use and quick to get started?
Sometimes we only need a global state, and it is enough to place some states and functions to change the state, which also achieves the principle of simplicity.
Let’s implement the simplest state management tool together.
The core of this state management tool uses the Context API . Before understanding this article, you must first understand and be familiar with the usage of this API.
First, let’s look at how to use this status management tool. Suppose there is a counter state, and then we modify the counter through two methods, that is, addition and subtraction. In other words, we need to use a counter state and two methods to modify this state. In the React function component, we use the useState method to initialize a state. Therefore, we can easily write the following code:
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;
Now, let’s create a component to use this useCounter hook function, as follows:
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 > ) }
Then use it in the root component App, as follows:
import React from 'react' const App = () => { return ( < div className = "App" > < Counter /> < Counter /> </ div > ) }
In this way, a counter component is completed, but is it really just that?
First of all, we should know that the state of the counter component should be consistent, that is to say, our counter component should share the same state, so how to share the same state? This is when Context needs to appear. To transform the above components, we initialize the state in the root component App and pass it to the sub-components. First modify the code of the App root component as follows:
Create a new CounterContext.ts file with the following code:
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 > ) }
Then we also modify the Counter component code as follows:
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 > ) }
In this way, we can share the count state, no matter how deep it is used in sub-components, there is no problem, but this is not the end, let us continue.
Although using this method solves the problem of shared state, we found that we need to pass in an additional context name when using it, so we need to wrap it up. In the end, we only need to use it like the following:
const Counter = createModel (useCounter); export default Counter ;
const { Provider, useModel } = Counter;
Then our App component should look like this:
import React ,{ createContext } from 'react' import counter from './Counter' const App = () => { const { Provider } = counter; return ( < div className = "App" > < Provider > < Counter /> < Counter /> </ Provider > </ div > ) }
Continue to modify our Counter component as follows:
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 > ) }
Through the display of the above code, we actually understand that we are just building useContext and createContext into our encapsulated Model.
Next, we will unveil the mystery of this state management tool. First, we need to use the React-related API, so we need to import it. as follows:
// Import type import type { ReactNode , ComponentType } from 'react' ; import { createContext,useContext } from 'react' ;
Next define a unique identifier that is used to determine the incoming Context, and this is used to determine that the user is using the Context correctly.
const EMPTY :unique symbol = Symbol ();
Next we need to define the type of Provider. as follows:
export interface ModelProviderProps < State = void > { initialState?: State children: ReactNode }
Above we defined the state type of context, which is a generic type. The parameter is the type of state. It is initialized to undefined type by default, and a children type is defined. Because the child node of Provider is a React node, it is defined as ReactNode. type.
Then there is our Model type, as follows:
export interface Model < Value , State = void > { Provider: ComponentType<ModelProviderProps<State>> useModel: () => Value }
This is also easy to understand, because Model exposes two things, the first is Provider, and the second is useContext. It just changes the name. It is enough to define the types of these two.
Next is the implementation of our core function createModel function. Let’s go step by step. The first thing is to define this function. Pay attention to the type, as follows:
export const createModel = <Value,State = void >(useHook:(initialState?:State) => Value): Model<Value,State> => { //Core code }
What is difficult to understand about the above functions is the definition of the type. Our createModel function passes in a hook function. The hook function passes in a state as a parameter, and then the return value is the Model generic we defined. The parameter is the type we defined. Function generics.
Next, what we have to do is naturally create a context, as follows:
//Create a context const context = createContext<Value | typeof EMPTY >( EMPTY ) ;
Then we need to create a Provider function, which is essentially a React component, as follows:
const Provider = ( props:ModelProviderProps<State> ) => { // The main reason why ModelProvider is used here is that it cannot conflict with the defined function name const { Provider : ModelProvider } = context; const { initialState,children } = props; const value = useHook (initialState); return ( < ModelProvider value = {value} > {children} </ ModelProvider > ) }
It is also easy to understand here. In fact, we get the initial state and child nodes through the parent component, get the Provider component from the context, and then return it. Note that our value is the value wrapped by the custom hook function passed in.
In the third step, we need to define a hook function to get this custom Context, as follows:
const useModel = (): Value => { const value = useContext (context); // Here, make sure the user is using Provider correctly if (value === EMPTY ){ //Exception is thrown, the user is not wrapped in Provider Component throw new Error ( 'Component must be wrapped with <Container.Provider>' ); } //Return context return value; }
The implementation of this function is also easy to understand, which is to obtain the context, determine whether the context is used correctly, and then return.
Finally, we return these two things inside this function, that is, return the Provider and useModel functions. as follows:
return { Provider, useModel }
Combine all the above codes, and the createModel function is complete.
Finally, we merge all the code and the state management tool is completed.
// Import type 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 > => { //Create a context const context = createContext< Value | typeof EMPTY >( EMPTY ); // Define Provider function const Provider = ( props:ModelProviderProps<State> ) => { const { Provider : ModelProvider } = context; const { initialState,children } = props; const value = useHook (initialState); return ( < ModelProvider value = {value} > {children} </ ModelProvider > ) } //Define the useModel function const useModel = (): Value => { const value = useContext (context); // Here determine whether the user is using the Provider correctly if (value === EMPTY ){ //Throw an exception, the user The component is not wrapped with Provider throw new Error ( 'Component must be wrapped with <Container.Provider>' ); } //Return context return value; } return { Provider ,useModel }; }
Going a step further, we export a useModel function, as follows:
export const useModel = <Value,State = void >(model:Model<Value,State>):Value => { return model.useModel(); }
So far, our entire state management tool is complete, and it is very simple to use. In many lightweight shared state projects, we no longer need to use more complex state management tools like Redux.
Of course, this idea is not my own. The source has been indicated at the end of the article. This article analyzes the source code.