preface

Before React Hooks were introduced we used Redux most often for state management solutions, centralized data management, predictability, rich peripheral tools. This is almost the correct answer to React state management. But widespread use does not mean that there are no drawbacks: relatively complex concepts, heavy template code, and, as a general best practice for large projects, specification of the writing of different members. But small projects always feel like a short sleeve jacket, the project itself does not have much logic code, hard to add a lot of Redux template code

React Hooks come, thanks to good logic encapsulation and a concise API (most notably the extremely convenient useContext), that approximate algebraic effects. As a result, a number of react-redux state management libraries have been created, including Redux-Toolkit, which also provides access to Hooks based apis

  • Extension: What are algebraic effects?

    Algebraic effects are a concept in functional programming used to separate side effects from function calls and what from how in code

    An example:

    function getTotalPicNum(user1, user2) {
      const num1 = getPicNum(user1);
      const num2 = getPicNum(user2);
    
      return picNum1 + picNum2;
    }
    Copy the code

    In this function we count the total number of images, but do not care about the implementation of getPicNum, single responsibility.

    What if getting the total number of images requires a request?

    async function getTotalPicNum(user1, user2) {
      const num1 = await getPicNum(user1);
      const num2 = await getPicNum(user2);
    
      return picNum1 + picNum2;
    }
    Copy the code

    But async/await is contagious, so you have to change the way you call getTotalPicNum as well, it goes from synchronous to asynchronous

    Perform, which suspends the code when it executes and resumes it after picNum

    function getPicNum(name) {
      const picNum = perform name;
      return picNum;
    }
    
    try {
      getTotalPicNum('xiaoming'.'xiaohong');
    } handle (who) {
        // Or execute a request
      switch (who) {
        case 'xiaoming':
          resume with 230;
        case 'xiaohong':
          resume with 122;
        default:
          resume with 0; }}Copy the code

    React Hooks Implement algebraic effects in React function programming, you do not need to care how useState, useReducer, useRef Hooks save your data. But it’s not really an algebraic effect

Why does this article exist

By chance, I came across Unstate-Next, which is light and simple and can implement basic state management capabilities with the help of useContext. What is the ideal state management solution under Hooks? Here are some of the current mainstream state management repositories

State

What is a state?

In the era of jQuery, JS code mixed with DOM manipulation was long and difficult to understand

Spaghetti code. Program code is twisted like spaghetti

$("#btn1").click(function(){$("p").append(" <b>Appended text</b>.");
    var d = Math.floor(t/1000/60/60/24) < =0 ? 0 : Math.floor(t/1000/60/60/24The $()"#t_d").html(d);
    if (d <= 0) {$("#day").hide();
    $('#second').show(); }}); $("#btn2").click(function(){$("p").hide();
  $("#test2").html("Hello world! ");
});
$("#btn3").click(function(){$("#test3").val("Dolly Duck");
    $("p").show();
});
Copy the code

With the advent of modern front-end frameworks, they all had one big idea: data-driven views

Instead of writing “imperative code” to “manipulate data”, the changing data is called State.

React State

State for Class Component is this.state

Function Component state is useState/useReducer

To avoid overly complex application code, we “split” multiple components that communicate with each other through props passing state. While adhering to “one-way data flow”

React also introduces Context to solve the problem that state communication between components causes complex transfer of props layers

Redux

Core flow chart:

Concept:

  • Store: A place where data is stored. You can think of it as a container. There can only be one Store for the entire application.

  • State: The Store object contains all the data. If you want to obtain the data at a certain point in time, you need to generate a snapshot of the Store. The data set at this point in time is called State.

  • Action: State changes, resulting in View changes. However, the user does not touch State, only View. Therefore, the change in State must be caused by the View. An Action is a notification from the View that the State should change.

  • Action Creator: The View will have as many actions as it wants to send. It would be cumbersome to write them all by hand, so let’s define a function to generate Action, called Action Creator.

  • Reducer: After the Store receives the Action, it must present a new State so that the View will change. This State calculation process is called Reducer. Reducer is a function that takes Action and the current State as parameters and returns a new State.

  • Dispatch: is the only way for a View to issue an Action.

    An example:

    view it online

    // action.js
    export const INCREMENT = 'INCREMENT';
    export const DECREMENT = 'DECREMENT';
    export const RESET = 'RESET';
    
    export function increaseCount() {
    return ({ type: INCREMENT});
    }
    
    export function decreaseCount() {
    return ({ type: DECREMENT});
    }
    
    export function resetCount() {
    return ({ type: RESET});
    }
    
    // reducer.js
    import {INCREMENT, DECREMENT, RESET} from '.. /actions/index'
    
    const INITIAL_STATE = {
    count: 0.history: [],}function handleChange(state, change) {
    const {count, history} = state;
    return ({
      count: count + change,
      history: [count + change, ...history]
    })
    }
    
    export default function counter(state = INITIAL_STATE, action) {
    const {count, history} = state;
    switch(action.type) {
      case INCREMENT:
        return handleChange(state, 1);
      case DECREMENT:
        return handleChange(state, -1);
      case RESET:
        return (INITIAL_STATE)
      default:
        returnstate; }}// store.js
    export const CounterStore = createStore(reducers)
    Copy the code

    Principle:

    The core code in createstore.js focuses on two functions

  • The subscribe function

    Listeners are registered to listen for events, return unsubscribe functions, and are kept on a nextListeners array

  • The dispatch function

    • Call Reducer and pass the parameters (currentState, Action).
    • Execute the Listener in sequence.
    • Return to action.

    An example of redux-Tookit simplification

    // counterSlice.js
    import { createSlice, PayloadAction } from '@reduxjs/toolkit'
    
    export interface CounterState {
      count: number
    }
    
    const initialState: CounterState = {
      count: 0,}export const counterSlice = createSlice({
      name: 'counter',
      initialState,
      reducers: {
        increment: (state) = > {
          // Redux Toolkit allows us to write "mutating" logic in reducers. It
          // doesn't actually mutate the state because it uses the Immer library,
          // which detects changes to a "draft state" and produces a brand new
          // immutable state based off those changes
          state.count += 1
        },
        decrement: (state) = > {
          state.count -= 1
        },
        incrementByAmount: (state, action: PayloadAction<number>) = > {
          state.count += action.payload
        },
      },
    })
    
    // Action creators are generated for each case reducer function
    export const { increment, decrement, incrementByAmount } = counterSlice.actions
    
    export default counterSlice.reducer
    
    // store.js
    import { configureStore } from '@reduxjs/toolkit'
    import counterReducer from '.. /features/counter/counterSlice'
    
    export const store = configureStore({
      reducer: {
        counter: counterReducer,
      },
    })
    
    // Infer the `RootState` and `AppDispatch` types from the store itself
    export type RootState = ReturnType<typeof store.getState>
    // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
    export type AppDispatch = typeof store.dispatch
    Copy the code

I want to use Context as a state management scheme

  • Suppose we now have a component Foo that has a count state inside it

    const Foo = () = > {
      const [count, setCount] = React.useState(0);
      return (
        <>
          <h1>{count}</h1>
          <button onClick={()= > setCount(v => v + 1)}>Click Me!</button>
        </>
      );
    };
    Copy the code
  • Now we want other components to have access to this count state

    const CountContext = React.createContext(null);
    const CountProvider = ({ children }) = > {
      const [value] = React.useState(0);
      return (
        <Context.Provider value={value}>
          {children}
        </Context.Provider>
      );
    };
    
    const Foo = () = > {
      const count = React.useContext(CountContext);
      return (
        <>
          <h1>{count}</h1>
        </>
      );
    };
    
    const Bar = () = > {
      const count = React.useContext(CountContext);
      return <h2>{count % 2 ? 'Even' : 'Odd'}</h2>
    };
    
    const Buz = () = > (
      <CountProvider>
          <Foo />
          <Bar />
      </CountProvider>
    );
    Copy the code
  • Further, a better approach would be to elevate the Provider to the top

    const App = () = > (
      <CountProvider>
        <Layout />
      </CountProvider>
    );
    Copy the code
  • Further, we found that although we shared the state, there was no way to modify it, right?

    Pass the methods that modify state along with the state through the Context

At this point, our code is pretty close to unstate-next

unstate-next

Unstate-next can be understood as providing a paradigm for using context as state management

The source code is very simple, less than 40 lines, and only 2 apis (****useContainer, createContainer**)

  • Example

    import React, { useState } from "react"
    import { createContainer } from "unstated-next"
    
    function useCounter(initialState = 0) {
      let [count, setCount] = useState(initialState)
      let decrement = () = > setCount(count - 1)
      let increment = () = > setCount(count + 1)
      return { count, decrement, increment }
    }
    
    let Counter = createContainer(useCounter)
    
    function CounterDisplay() {
      let counter = Counter.useContainer()
      return (
        <div>
          <button onClick={counter.decrement}>-</button>
          <span>{counter.count}</span>
          <button onClick={counter.increment}>+</button>
        </div>)}function App() {
      return (
        <Counter.Provider>
          <CounterDisplay />
          <Counter.Provider initialState={2}>
            <div>
              <div>
                <CounterDisplay />
              </div>
            </div>
          </Counter.Provider>
        </Counter.Provider>)}Copy the code

We pass in custom Hooks that encapsulate state and modify state, and return container with Provider and useContainer

The problem

  • The provider does not support displayName

    Optimize the DevTools display

  • Special attention needs to be paid to performance issues caused by repeated rendering of the context

    Context repeat render problem – CodeSandbox

    Context components will be rendered by the parent component’s render

    • The child component is promoted to props. Children

    • “W Memo”, useMemo for context value, and React. Memo wrap for child components

    But if we put all the values in one context, we don’t care whether the component uses it or not, we rerender it, we split it

    • Split contexts

      • Split Contexts. Split Contexts with different Contexts

      • Separate the mutable and unchanging Contexts so that the unchanging Contexts are in the outer layer and the mutable Contexts are in the inner layer

    One thing that won’t change with Split Contexts is the single dependent repeat render problem for state and setState

    As you add more providers to your app, it will look like this. What a beautiful graph

    const App = () = > (
      <Context1Provider>
        <Context2Provider>
          <Context3Provider>
            <Context4Provider>
              <Context5Provider>
                <Context6Provider>
                  <Context7Provider>
                    <Context8Provider>
                      <Context9Provider>
                        <Context10Provider>
                          <Layout />
                        </Context10Provider>
                      </Context9Provider>
                    </Context8Provider>
                  </Context7Provider>
                </Context6Provider>
              </Context5Provider>
            </Context4Provider>
          </Context3Provider>
        </Context2Provider>
      </Context1Provider>
    );
    Copy the code

constate

Constate is used in a very similar way to unstate-next, with several improvements over unstate-next

  • Provides the displayName

  • Selectors apis are provided to use context values more carefully

  • Example

    import React, { useState, useCallback } from "react";
    import constate from "constate";
    
    function useCounter() {
      const [count, setCount] = useState(0);
      // increment's reference identity will never change
      const increment = useCallback(() = > setCount(prev= > prev + 1), []);
      return { count, increment };
    }
    
    const [Provider, useCount, useIncrement] = constate(
      useCounter,
      value= > value.count, // becomes useCount
      value= > value.increment // becomes useIncrement
    );
    
    function Button() {
      // since increment never changes, this will never trigger a re-render
      const increment = useIncrement();
      return <button onClick={increment}>+</button>;
    }
    
    function Count() {
      const count = useCount();
      return <span>{count}</span>;
    }
    Copy the code

The core code

diegohaz/constate

Create a context for each based on the incoming selectors

if (selectors.length) {
  selectors.forEach((selector) = > createContext(selector.name));
} else {
  createContext(useValue.name);
}
Copy the code

Automatically generate nested providers based on the contexts created

const Provider: React.FC<Props> = ({ children, ... props }) = > {
    const value = useValue(props as Props);
    let element = children as React.ReactElement;
    for (let i = 0; i < contexts.length; i += 1) {
      const context = contexts[i];
      const selector = selectors[i] || ((v) = > v);
      element = (
        <context.Provider value={selector(value)}>{element}</context.Provider>
      );
    }
    return element;
  };
Copy the code

zustand

  • 2021 most 🔥 state management library

    – the Source Data

Zustand uses a global state management solution. Based on the observer model. The API is clear and simple. There is no need to deal with repeated rendering of the Context, no Provider is required, and component updates are implemented through forceUpdate instead. It is not in the React state, and supports non-React environments.

  • Example

    import create from 'zustand'
    
    const useStore = create(set= > ({
      bears: 0.increasePopulation: () = > set(state= > ({ bears: state.bears + 1 })),
      removeAllBears: () = > set({ bears: 0}})))function BearCounter() {
      const bears = useStore(state= > state.bears)
      return <h1>{bears} around here ...</h1>
    }
    
    function Controls() {
      const increasePopulation = useStore(state= > state.increasePopulation)
      return <button onClick={increasePopulation}>one up</button>
    }
    Copy the code

The core code

  • Vanilla.ts

    The implementation of observer mode provides setState, SUBSCRIBE, getState, destroy methods. And the first two provide the selector and equalityFn arguments

    • SetState code

      const setState: SetState<TState> = (partial, replace) = > {
        const nextState = typeof partial === 'function' ? partial(state) : partial
        if(nextState ! == state) {const previousState = state
          state = replace ? (nextState as TState) : Object.assign({}, state, nextState)
          listeners.forEach((listener) = > listener(state, previousState))
        }
      }
      Copy the code
    • GetState code

      const getState: GetState<TState> = () = > state
      Copy the code
    • The subscribe code

      const subscribe: Subscribe<TState> = <StateSlice>(listener: StateListener
                 
                   | StateSliceListener
                  
                   , selector? : StateSelector
                   
                    , equalityFn? : EqualityChecker
                    
                   ,>
                  
                 ) = > {
        if (selector || equalityFn) {
          return subscribeWithSelector(
            listener as StateSliceListener<StateSlice>,
            selector,
            equalityFn
          )
        }
        listeners.add(listener as StateListener<TState>)
        // Unsubscribe
        return () = > listeners.delete(listener as StateListener<TState>)
      }
      Copy the code
    • createStore

      function createStore(createState) {
        let state: TState
        const setState = / * *... * /
        const getState = / * *... * /
        / * *... * /
        const api = { setState, getState, subscribe, destroy }
        state = createState(setState, getState, api)
        return api
      }
      Copy the code
  • react.ts

    Implement state registration and updating based on the React Hooks API interface, and rerender components via forceUpdate

    • How components are rerendered

      const [, forceUpdate] = useReducer((c) = > c + 1.0) as [never, () = > void]
      Copy the code
    • Check whether the status is updated

      conststate = api.getState() newStateSlice = selector(state) hasNewStateSlice = ! equalityFn( currentSliceRef.currentas StateSlice,
        newStateSlice
      )
      useIsomorphicLayoutEffect(() = > {
        if (hasNewStateSlice) {
          currentSliceRef.current = newStateSlice as StateSlice
        }
        stateRef.current = state
        selectorRef.current = selector
        equalityFnRef.current = equalityFn
        erroredRef.current = false
      })
      Copy the code
    • Status updates notify components to rerender

      useIsomorphicLayoutEffect(() = > {
        const listener = () = > {
          try {
            const nextState = api.getState()
            const nextStateSlice = selectorRef.current(nextState)
            if(! equalityFnRef.current(currentSliceRef.currentas StateSlice, nextStateSlice)) {
              stateRef.current = nextState
              currentSliceRef.current = nextStateSlice
              forceUpdate()
            }
          } catch (error) {
            erroredRef.current = true
            forceUpdate()
          }
        }
        const unsubscribe = api.subscribe(listener)
        if(api.getState() ! == stateBeforeSubscriptionRef.current) { listener()// state has changed before subscription
        }
        return unsubscribe
      }, [])
      Copy the code
  • context.ts

    React Context distributes stores to create multiple store instances that do not interfere with each other

    const useStore: UseContextStore<TState> = <StateSlice>(selector? : StateSelector<TState, StateSlice>, equalityFn =Object.is
    ) = > {
      const useProviderStore = useContext(ZustandContext)
      return useProviderStore(
        selector as StateSelector<TState, StateSlice>,
        equalityFn
      )
    }
    Copy the code
  • middleware.ts

    The nature of middleware is that functions are called in a nested order. In Zustand’s case, middleware wraps the setState function, and Zustand naturally uses this functional design

    Take the built-in persistence state middleware as an example:

    // Use an example
    export const useStore = create(persist(
      (set, get) = > ({
        fishes: 0.addAFish: () = > set({ fishes: get().fishes + 1})}, {name: "food-storage".// unique name
        getStorage: () = > sessionStorage, // (optional) by default, 'localStorage' is used}))// Middleware.ts implements thumbnail code
    // Modify the set function passed in options
    const configResult = config(
        ((. args) = >{ set(... args)void setItem()
        }) as CustomSetState,
        get,
        api
    )
    Copy the code

jotai

Same global state management scheme, same author as Zustand. Borrowed from Recoil’s concept of atomization

The API is also very simple, Atom and useAtom. To create an atomized data source from Atom, call useAtom and pass in atom to return the corresponding state value and modification method.

import { atom } from 'jotai'

const countAtom = atom(0)
const countryAtom = atom('Japan')
const citiesAtom = atom(['Tokyo'.'Kyoto'.'Osaka'])
const mangaAtom = atom({ 'Dragon Ball': 1984.'One Piece': 1997.Naruto: 1999 })

function Counter() {
  const [count, setCount] = useAtom(countAtom)
  return (
    <h1>
      {count}
      <button onClick={()= > setCount(c => c + 1)}>one up</button>
        </h1>)}Copy the code

Atomized data is very derivable

const doubledCountAtom = atom((get) = > get(countAtom) * 2)

function DoubleCounter() {
  const [doubledCount] = useAtom(doubledCountAtom)
  return <h2>{doubledCount}</h2>
}
Copy the code

Both get and set can be used to modify the value of the atomic state and the modification method

const decrementCountAtom = atom(
  (get) = > get(countAtom),
  (get, set, _arg) = > set(countAtom, get(countAtom) - 1),function Counter() {
  const [count, decrement] = useAtom(decrementCountAtom)
  return (
    <h1>
      {count}
      <button onClick={decrement}>Decrease</button>
        </h1>)}Copy the code
  • Although it is a global status management solution, it also supports the use of providers

Easy to view the understanding of the source code:

  • The atom analogy key

  • The state of analogy store

  • AtomState analog value

  • Principle: The simplest version given by official documentation

    import { useState, useEffect } from "react";
    
    The atom function returns a configuration object containing the initial value
    export const atom = (initialValue) = > ({ init: initialValue });
    
    // Track the state of atoms
    // Use WeakMap to store atomState
    // Store atom as key and atomState as value
    const atomStateMap = new WeakMap(a);const getAtomState = (atom) = > {
      let atomState = atomStateMap.get(atom);
      if(! atomState) { atomState = {value: atom.init, listeners: new Set() };
        atomStateMap.set(atom, atomState);
      }
      return atomState;
    };
    
    // the useAtom hook is similar to useState
    export const useAtom = (atom) = > {
      const atomState = getAtomState(atom);
      const [value, setValue] = useState(atomState.value);
      useEffect(() = > {
        const callback = () = > setValue(atomState.value);
    
            // Add the atom update callback, which updates the Value of the current Hooks
        atomState.listeners.add(callback);
        callback();
        return () = > atomState.listeners.delete(callback);
      }, [atomState]);
    
      const setAtom = (nextValue) = > {
        atomState.value = nextValue;
    
        // All listeners are executed during update
        atomState.listeners.forEach((l) = > l());
      };
    
      return [value, setAtom];
    };
    Copy the code
  • Support for deriving Atom

    const atomState = {
      value: atom.init,
      listeners: new Set(),
      dependents: new Set()
    };
    Copy the code

valtio

Proxy state with the Proxy & Reflect API

import { proxy, useSnapshot } from 'valtio'

const state = proxy({ count: 0.text: 'hello' })

// This will re-render on `state.count` change but not on `state.text` change
function Counter() {
  const snap = useSnapshot(state)
  return (
    <div>
      {snap.count}
      <button onClick={()= > ++state.count}>+1</button>
    </div>)}Copy the code
  • The core code

    const handler = {
      get(target: T, prop: string | symbol, receiver: any) {
        // Omit some code
        return Reflect.get(target, prop, receiver)
      },
      deleteProperty(target: T, prop: string | symbol) {
        const prevValue = Reflect.get(target, prop)
        constchildListeners = prevValue? .[LISTENERS]if (childListeners) {
          childListeners.delete(popPropListener(prop))
        }
        const deleted = Reflect.deleteProperty(target, prop)
        if (deleted) {
          notifyUpdate(['delete', [prop], prevValue])
        }
        return deleted
      },
      is: Object.is,
      canProxy,
      set(target: T, prop: string | symbol, value: any, receiver: any) {
        const prevValue = Reflect.get(target, prop, receiver)
        if (this.is(prevValue, value)) {
          return true
        }
        constchildListeners = prevValue? .[LISTENERS]if (childListeners) {
          childListeners.delete(popPropListener(prop))
        }
        if (isObject(value)) {
          value = getUntracked(value) || value
        }
        let nextValue: any
        if (Object.getOwnPropertyDescriptor(target, prop)? .set) { nextValue = value }// Omit some code
        Reflect.set(target, prop, nextValue, receiver)
        notifyUpdate(['set', [prop], value, prevValue])
        return true}},const proxyObject = new Proxy(baseObject, handler)
    Copy the code

conclusion

For a state management scheme we know that there must be two key concepts:

  • The data source
  • Data consumption

For data sources:

Value transfer is implemented based on the Context API, and since providers can be placed anywhere in the application, they can also be called local state management solutions, corresponding to the global state management solutions

(The Context API has its own mechanism for rerendering state changes, so global state management solutions need to deal with how to track state changes and rerender components.)

For data consumption:

Similar to immutable data consumption based on context and redux above

Mobx and Valtio are mutable data

The way data is consumed also determines how they track state changes. Immutable data is publish-subscribe/watch-mode, while mutable data is Proxy mode

Ref

  • GitHub – PMNDRS /zustand: 🐻 Bear Necessities for State management in React

  • zhuanlan.zhihu.com/p/147010079

  • 2021 JavaScript Star Project

  • Mp.weixin.qq.com/s/ddZTSSHT8…

  • Simple algebraic effects

  • Algebraic effects and React