This article is reprinted by github.github.com/wang1212

How do Web applications manage state based on React. Js? The main solution in the community is React-Redux, which is essentially based on the implementation of the React Context feature. If the application is simple enough, it is actually not difficult to write a simple state management tool using the Context. However, given the sophistication of the tools and robustness of the project, better, mature community solutions are usually used. In mobile scenarios, Where React-Redux is a bit bloated, zustand, a lightweight state management tool, is a good alternative.

Lightweight state management solution

In Web optimization, the optimization of resource size is the top priority, and it is also the optimization method with the lowest cost and highest benefit, especially in the mobile terminal scenario. Every time you introduce a tool library into a project, consider whether there are lighter alternatives. Moment is known as a classic example, and I usually use DayJS as an alternative. In the choice of application status management tools, Bundlephobia can be used to first evaluate the mainstream community scheme React-Redux.

MINIFIED MINIFIED + GZIPPED
[email protected] 4.3 kB 1.6 kB
[email protected] 16.2 kB 5.4 kB
20.5 kB 7kB

The required dependencies alone are up to 7KB, and we know that Redux is not only a state management tool, it also advocates an excellent pattern that we are familiar with:

Store -> Dispatch -> Action -> Reducer -> Store

This model requires us to write a lot of template code by hand, resulting in the official solution @reduxjs/ Toolkit and the community solution @rematch/ Core, which further exacerbates the impact of resource size.

MINIFIED MINIFIED + GZIPPED
@ reduxjs/[email protected] 32.1 kB 10.5 kB
@ rematch/[email protected] 4.7 kB 1.7 kB

After analysis, we can actually see that redux’s core codebase is only 1.6 KB in size, but in order to adapt to React. Js and solve the template code problems, we need to increase the resource size by at least 7.1 KB. In other words, the core implementation of the state management tool is relatively simple, which is also the reason for the small core library, while in the mobile scenario, the project is generally simple and small, and the core requirements for the tool are only to meet the application state management. So the Zustand community project became an option for me.

MINIFIED MINIFIED + GZIPPED
[email protected] 2kB 954B
[email protected] 6.1 kB 2.5 kB

Jotai, also listed in the table above, was developed by the same group of developers as Zustand, which applies only to state management within the React. Js component, while Zustand applies to state manipulation outside of the component. Zustand is simple enough and does not require much template code, with a 954B size that meets the core requirements of application state management.

At this point, to replace the mainstream solution of React-Redux in mobile scenarios, consider the following:

  • Meet the core requirements of application state management
  • Have the advantage of resource size
  • Simple enough to use
  • Mature scheme (adopted by more people, with supporting debugging tools, etc.)
  • State can be manipulated outside of the component

The fact that state can be manipulated outside of a component is meant to be flexible in the solution, which is handy when the implementation of a business requirement may involve a scenario where state can be manipulated outside of a component.

redux vs zustand

Next, look at the source code implementations of both to see if Zustand is a better alternative. The first step is to take a look at the core implementation of both, namely the mechanism of state management.

First, we need to understand what state management does. State is data. For a native Web application, the structure and style of the page display at a given moment depends on the state, which may change due to user interaction. Web applications have many states, such as the tick button state of a form. We can think of this state as a local state. Changes in this state do not cause changes in other parts of the page. , of course, if we will be further user experience design, check the button on the synchronization state affect whether the form submit button is in a state of clickable, when a state has an effect on page two parts, for more complex Web applications, a state may affect the page dozens of parts, we need to maintain the state of the update mechanism to carry on the design, When the maintenance of state is decoupled from the page and carried out independently to the whole world, this state is called the global state. Obviously, for local state, page parts can be autonomous, while for global state, a globally centralized “database” is needed to manage them.

Now, we can see that state management needs to provide a similar centralized “database”, at the same time to provide update mechanism for the state, and the state can be dependent on multiple parts, at the same time, the dependent party can obtain the latest state in time. Isn’t this the typical publish/subscribe model in software architecture? So, take a look at the apis provided by both Redux and Zustand to get a rough idea of the core implementation model.

// Redux (https://redux.js.org/api/api-reference)
createStore(reducer, [preloadedState], [enhancer])
// Store
getState()
subscribe(listener)
dispatch(action)


// zustand (https://github.com/pmndrs/zustand)
createStore()
// Store
getState()
subscribe()
setState()Copy the code

/ / zustand (github.com/pmndrs/zust... createStore(a) // Store getState(a) subscribe(a) setState(a)

Thus, the core apis provided by the two are very similar, and from the API naming point of view, the core implementation is definitely based on the publish/subscribe model.

Both have a createStore() API to create a centralized datastore, and both createStore instances that expose the active state getState() API, subscribe() API, And state update APIS dispatch() and setState(). Of course, Redux also introduces a Reducer concept and API.

Both core libraries are only 1KB in size, while Zustand is smaller because zustand’s implementation is simpler. The differences are mainly in the status update mechanism, followed by the status subscription mechanism.

subscribe()

In the implementation of the Subscribe () API for state subscriptions, Zustand simply adds the subscribe function directly to the list of subscriptions and provides a selector mechanism to filter state:

/ / see https://github.com/pmndrs/zustand/blob/v3.6.5/src/vanilla.ts#L126
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>)
}


/ / see https://github.com/pmndrs/zustand/blob/v3.6.5/src/vanilla.ts#L107
const subscribeWithSelector = <StateSlice>(
listener: StateSliceListener<StateSlice>,
selector: StateSelector<TState, StateSlice> = getState as any,
equalityFn: EqualityChecker<StateSlice> = Object.is
) = > {
console.warn('[DEPRECATED] Please use subscribeWithSelector middleware')
let currentSlice: StateSlice = selector(state)
function listenerToAdd() {
const nextSlice = selector(state)
if(! equalityFn(currentSlice, nextSlice)) {const previousSlice = currentSlice
listener((currentSlice = nextSlice), previousSlice)
}
}
listeners.add(listenerToAdd)
// Unsubscribe
return () = > listeners.delete(listenerToAdd)
}Copy the code

// see https://github.com/pmndrs/zustand/blob/v3.6.5/src/vanilla.ts#L107 const subscribeWithSelector = <StateSlice>( listener: StateSliceListener<StateSlice>, selector: StateSelector<TState, StateSlice> = getState as any, equalityFn: EqualityChecker<StateSlice> = Object.is ) => { console.warn('[DEPRECATED] Please use subscribeWithSelector middleware') let currentSlice: StateSlice = selector(state) function listenerToAdd() { const nextSlice = selector(state) if(! equalityFn(currentSlice, nextSlice)) { const previousSlice = currentSlice listener((currentSlice = nextSlice), previousSlice) } } listeners.add(listenerToAdd)// Unsubscribe return () => listeners.delete(listenerToAdd) }

As you can see from the listenerToAdd() function above, when a selector is provided when a state is subscribed, the state is first filtered and notified to the subscriber when the state is updated.

/ / see https://github.com/pmndrs/zustand/blob/v3.6.5/src/vanilla.ts#L89
const setState: SetState<TState> = (partial, replace) = > {
    // ...
    listeners.forEach((listener) = > listener(state, previousState))
    // ...
}Copy the code

When the state is updated by setState(), all subscription functions are called, passing the new state and the old state to the subscription function.

Next, take a look at the implementation of Redux, which makes some special judgments and special handling when adding subscription functions:

/ / see https://github.com/reduxjs/redux/blob/v4.1.2/src/createStore.js#L128
function subscribe(listener) {
  // ...
  if (isDispatching) {
    throw new Error('... ')}let isSubscribed = true




ensureCanMutateNextListeners()
nextListeners.push(listener)




return function unsubscribe() {
if(! isSubscribed) {return
}



<span class="hljs-keyword">if</span> (isDispatching) {
  throw new Error(<span class="hljs-string">&#x27;.&#x27;</span>)
}

isSubscribed = false

ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, <span class="hljs-number">1</span>)
currentListeners = null}}/ / see https://github.com/reduxjs/redux/blob/v4.1.2/src/createStore.js#L82
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}Copy the code

// see https://github.com/reduxjs/redux/blob/v4.1.2/src/createStore.js#L82 function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } }

According to the implementation, redux through isDispatching flag bit to avoid during the status updates to add subscription function, and through ensureCanMutateNextListeners subscription function () function will list made a shallow copy and then to add and delete operations, It’s all about avoiding potential problems.

/ / see https://github.com/reduxjs/redux/blob/v4.1.2/src/createStore.js#L197
function dispatch(action) {
  // ...


const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}




return action
}Copy the code

return action }

When redux updates its state with dispatch(), it notifies all subscribers indiscriminately and does not pass the old and new state to the subscription function, since the selector mechanism is not provided by default at subscription time. As you can see in the official sample code, The official recommendation is to actively get the new state and perform the selector operation in the subscription function via getState(). It can be said that due to the different design philosophies of Redux and Zustand, subscriptions are implemented in a slightly different way, with the former having more control and flexibility, while zustand keeps it simple without sacrificing flexibility.

setState() && dispatch()

The status update mechanism is the biggest difference between the two implementations. Zustand provides a setState() function to update the status:

/ / see https://github.com/pmndrs/zustand/blob/v3.6.5/src/vanilla.ts#L89
const setState: SetState<TState> = (partial, replace) = > {
  // TODO: Remove type assertion once https://github.com/microsoft/TypeScript/issues/37663 is resolved
  // https://github.com/microsoft/TypeScript/issues/37663#issuecomment-759728342
  const nextState =
    typeof partial === 'function'
      ? (partial as (state: TState) => TState)(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

According to the source code implementation, Zustand merges the updated state with the object. assign function, and provides the replace flag to completely replace the old state.

The state update of Redux is more complicated, mainly because the officially recommended programming mode divides the state update into multiple steps. The dispatch() function triggers an Action, and the specific Action processing and state merging operations are completed by the Reducer function, which is a pure function. As for the reason of this design, the official explanation is that pure functions are predictable for state changes and easy to test, which is the basis for implementing functions like time travel.

/ / see https://github.com/reduxjs/redux/blob/v4.1.2/src/createStore.js#L197
function dispatch(action) {
  if(! isPlainObject(action)) {throw new Error(
      `Actions must be plain objects. Instead, the actual type was: '${kindOf( action )}'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.`)}if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.')}if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')}try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}




const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}




return action
}Copy the code

return action }

According to the source code implementation, isDispatching flag is shown here, which is mainly used to restrict the state update operation cannot be initiated again in the state update process, so as to avoid errors.

However, it is worth mentioning that redux does not support asynchronous state updates by default and requires the support of the Redux-Thunk library. Zustand itself supports asynchronous status updates.

According to the above analysis, in fact, the core implementation is similar, and Zustand, as a latecomer, has borrowed from and simplified from Redux. It has no problem meeting the core simple requirements of state management and can be used as an alternative to Redux.

The React. Js adaptation

If the core libraries differ slightly and the package sizes are similar, the biggest difference is in the adaptation of the React.js library.

Since Zustand emerged late, Hook API has become the mainstream of the React. Js community, so Zustand ADAPTS to it in the way of Hook API, without providing adaptation of class components.

/ / see https://github.com/pmndrs/zustand/blob/v3.6.5/src/index.ts#L64
function create<
  TState extends State.CustomSetState.CustomGetState.CustomStoreApi extends StoreApi<TState> > (createState: | StateCreator
       
         | CustomStoreApi
       ,>) :UseBoundStore<TState.CustomStoreApi> {
  // ...
  const useStore: any = <StateSlice>(
    selector: StateSelector<TState, StateSlice> = api.getState as any,
    equalityFn: EqualityChecker<StateSlice> = Object.is
  ) = > {
    const [, forceUpdate] = useReducer((c) = > c + 1.0) as [never.() = > void]
    // ...
    const stateBeforeSubscriptionRef = useRef(state)
    useIsomorphicLayoutEffect(() = > {
      const listener = () = > {
        try {
          const nextState = api.getState()
          const nextStateSlice = selectorRef.current(nextState)
          if (
            !equalityFnRef.current(
              currentSliceRef.current as 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
    }, [])

<span class="hljs-keyword">const</span> sliceToReturn = hasNewStateSlice
  ? (newStateSlice as StateSlice)
  : currentSliceRef.current
useDebugValue(sliceToReturn)
return sliceToReturn




}




// ...
return useStore
}Copy the code

// ... return useStore }

Zustand implements the return value of createStore as a custom hook. In order for the React. Js component to be aware of status updates, useEffect is used to complete the subscription operation. ForceUpdate () forces the component to rerender to get the latest state.

Here, take a look at how to use Zustand in function components:

import create from 'zustand // Store const useStore = create(set => ({ bears: 0, increasePopulation: () => set(state => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }) })) // Component function BearCounter() { const bears = useStore(state => state.bears) return 

{bears} around here ...

}
Copy the code

// Component function BearCounter() { const bears = useStore(state= >state.bears)

return <h1>{bears} around here ... </h1> }

In fact, the usage is very similar to react-redux, but the useStore API is used to fetch and update the state.

However, the react-Redux implementation is much more complicated. Because of its early appearance, it ADAPTS both class components and function components. The implementation of React-Redux is not detailed here, but the biggest difference from Zustand is that the state is stored in the Context, so you need to use the Provider to wrap the root component of the page. Redux’s useSelector() Hook API is also very similar to the implementation logic of useStore() mentioned above in Zustand.

Modal tools

A core tool library very not good, not only can solve business problems, but also can provide a good development experience, redux became the React. Js community generally used state management scheme, not only lies in its implementation of elegant, advocated by the excellent mode, more lies in its form a complete set of debugging tools, middleware is very nice, too. So, zustand, as a latecomer, is not reinventing the wheel, but is reusing the redux community’s open source solutions as much as possible, which is also good. At least the migration from Redux to Zustand is not too difficult, and the development experience is good.

conclusion

At this point, zustand’s exploration of a lightweight state management solution is complete. At least in terms of meeting the core needs of simple state management, simplicity of use, and good tuning tools, it’s worth a try as a lightweight alternative to Redux.

reference

  • redux
  • react-redux
  • zustand
  • Let’s build our own Redux