background

In order to be a comfortable wheel to use, I recently studied the React state management library, but only at the usage level, which is to choose which state management library to use from a comfortable perspective. The selection was based on a GitHub look at the popularity and usage of the state management library within the React community. Let me know which one we chose at the end of the post.

The principles for selecting libraries are as follows:

  • To fully embrace TypeScript, so chooses libraries that are friendly to TypeScript support
  • React introduced hooks with 16.8 and then started the age of functional programming, so don’t have classes
  • Must be simple to use, not too complex, use is very light, the focus is comfortable
  • Supports modularity, can be centrally managed, low atomicity
  • Support for EsModule, as migration to Vite will be considered later, although for now it is still Webpack

So far, I’ve looked at the number of stars and used by for some of the most popular status management libraries on GitHub, as well as the weekly downloads on NPM. This can be seen in some ways, given the popularity of the framework, but there’s a small chance that it’s not accurate. However, to a large extent, the frame selection is helpful.

The library github star github used NPM weekly downloads
mobx 23.9 k. 83.9 k. 671787
redux-toolkit 5.9 k. 83.2 k. 755564
recoil 13.5 k. 83.9 k. 95245
zustand 9.4 k. 7.2 k. 104682
rematch 7.3 k. 2.5 k. 33810
concent 950 65 1263

In the table above, we are going to choose which one we like, depending on the posture and which one is more comfortable to use.

mobx

Mobx is a great React status management library, no doubt, and it’s the # 1 most-used one on GitHub. The examples given on the official website are very simple, and the same is true for most official websites. However, I don’t need a simple example, I need a complete project example, so I refer to the project ANTD-PRO-MOBX on GitHub. Mobx needs to be used with the mobx-react connection library.

As recommended by NPM, the class + observe function is wrapped in the latest version V6:

import React from "react" import ReactDOM from "react-dom" import { makeAutoObservable } from "mobx" import { observer }  from "mobx-react" // Model the application state. class Timer { secondsPassed = 0 constructor() { makeAutoObservable(this) } increase() { this.secondsPassed += 1 } reset() { this.secondsPassed = 0 } } const myTimer = new Timer() // Build a "user interface" that uses the observable state. const TimerView = observer(({ timer }) => ( <button onClick={() => timer.reset()}>Seconds passed: {timer.secondsPassed}</button> )) ReactDOM.render(<TimerView timer={myTimer} />, document.body) // Update the 'Seconds passed: X' text every second. setInterval(() => { myTimer.increase() }, 1000)

When a new project starts from scratch, it should not choose the old version of the library to use, but generally choose the stable new version to use. As for TypeScript, we can see that the source code is already written in TypeScript, but we don’t see any signs of TypeScript on the official website and NPM. Maybe it hasn’t been published yet.

Let’s take a look at mobx by comparing our principles:

  • Support for TypeScript — NO
  • Using Functional Programming — No
  • Comfortable to use — OK
  • Low atomicity problem — OK
  • Support Esmodule — OK

About the mobx part of the temporary here, said the wrong place welcome to inform, really is a lack of talent and knowledge, how to use such a popular state management library.

reduxjs/toolkit

NPX create-react-app –template redux(NPX create-react-app –template redux) comes with a state management library CRA template Redux -ts(NPX create-react-app –template Redux -TypeScript) comes with its own state management library. It is possible that these two templates also contribute to the high number of downloads and usage of the Toolkit. Also because it is the official library of Redux, we need to use it with React – Redux. Let’s leave these aside, let’s see how to use, pro test ha, if there is improper use, you can point out.

// index.ts import React from 'React '; import { useSelector, useDispatch } from 'react-redux'; import { RootStore } from 'modules/store'; import { fetchInfo } from 'modules/counter'; function App(props: any) { const count = useSelector((state: RootStore) => state.counter.value); const dispatch = useDispatch(); return ( <div> hello home <hr/> <button aria-label="Decrement value" onClick={() => dispatch(fetchInfo(2234))} > fetchInfo </button> <div> <span>{count}</span> </div> </div> ); }; ReactDOM.render( <App/>, document.getElementById('root'), );

Above is the main entry file code, you can see that this is a fairly common use, in line with the use of Redux.

// modules/store.ts
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import counter from './counter';
import { TypedUseSelectorHook, useSelector, useDispatch } from 'react-redux';

const reducer = combineReducers({
  counter,
});

const store = configureStore({
  reducer,
});

export type RootStore = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootStore> = useSelector;
export default store;

The above is the code for the main store file, which is actually the official fair use method.

// modules/counter.ts import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; const namespace: string = 'counter'; export interface ICounter { value: number; } const initialState: ICounter = { value: 1, }; Export const fetchInfo = createAsyncThunk(' ${namespace}/fetchInfo ', async (value: number) => { await sleep(1000); return { value: 9000 + value, }; }); // create a map-reducer const counterSlice = createSlice({name: namespace, initialState, map-reducer: {}, extrareDucers: (builder) => { builder .addCase(fetchInfo.pending, (state, action) => { state.value = 1000; }) .addCase(fetchInfo.fulfilled, (state, {payload}) => { state.value = payload.value; }) .addCase(fetchInfo.rejected, (state, {payload}) => { state.value = 500; }); }}); const { reducer } = counterSlice; export default reducer;

The only thing that is not elegant is that Extraducers are called in concatenation, but can also be passed in the form of objects. However, it is not friendly enough to support in TS, as follows:

const counterSlice = createSlice({ name: namespace, initialState, reducers: {}, extraReducers: { [fetchInfo.pending.type]: (state: Draft<ICounter>, action: PayloadAction<ICounter>) => { state.value = 1000; }, [fetchInfo.pending.type]: (state: Draft<ICounter>, { payload }: PayloadAction<ICounter>) => { state.value = payload.value; }, [fetchInfo.pending.type]: (state: Draft<ICounter>, action: PayloadAction<ICounter>) => { state.value = 500; ,}}});

You can see that it’s an object, but you have to write your own type declaration inside the function. In a serial way, TypeScript automatically deduces the types of arguments to functions.

Let’s take a look at the Toolkit by comparing our principles:

  • TypeScript support — OK
  • Using functional programming — OK
  • Comfortable to use — OK, except for the chain-like use of Builder
  • Low atomicity problem — OK
  • Support Esmodule — OK

recoil

The React state management library comes with React17. The official website is recoiljs.org, and we can see that it almost completely follows the use of React hooks. It does not require any connecters. However, this also leads to strong atomicity, the unified state management needs to carry on the second encapsulation, and the workload is not small. On the TypeScript side, 0.3.0 is now supported, with the latest version currently being 0.3.1. : Let me just look at the official example

import React from 'react'; import { RecoilRoot, atom, selector, useRecoilState, useRecoilValue, } from 'recoil'; function App() { return ( <RecoilRoot> <CharacterCounter /> </RecoilRoot> ); } const textState = atom({ key: 'textState', // unique ID (with respect to other atoms/selectors) default: '', // default value (aka initial value) }); function CharacterCounter() { return ( <div> <TextInput /> <CharacterCount /> </div> ); } function TextInput() { const [text, setText] = useRecoilState(textState); const onChange = (event) => { setText(event.target.value); }; return ( <div> <input type="text" value={text} onChange={onChange} /> <br /> Echo: {text} </div> ); } const charCountState = selector({ key: 'charCountState', // unique ID (with respect to other atoms/selectors) get: ({get}) => { const text = get(textState); return text.length; }}); function CharacterCount() { const count = useRecoilValue(charCountState); return <>Character Count: {count}</>; }

From the above, we can simply compare and contrast our principles:

  • TypeScript support — OK, although not very strong
  • Using functional programming — OK
  • Comfortable to use — NO
  • Low atomicity problem — NO
  • Support Esmodule — OK

zustand

Zustand is, to be honest, a new library to see, but looking at the NPM example above, it is very useful and comfortable to use. It does not provide a lot of APIs, but it is compact enough to meet the requirements. There is no separate website, but the readme is detailed enough to be an address, NPM zustand. Let’s take a look at the examples provided on the website:

import React from "react";
import create from "zustand";
import PrismCode from "react-prism";
import "prismjs";
import "prismjs/components/prism-jsx.min";
import "prismjs/themes/prism-okaidia.css";

const sleep = (time = 1000) => new Promise((r) => setTimeout(r, time));
const code = `import create from 'zustand'

const useStore = create(set => ({
  count: 1,
  inc: () => set(state => ({ count: state.count + 1 })),
}))

function Controls() {
  const inc = useStore(state => state.inc)
  return <button onClick={inc}>one up</button>
)

function Counter() {
  const count = useStore(state => state.count)
  return <h1>{count}</h1>  
}`;

const useStore = create((set) => ({
  count: 1,
  inc: () => set((state) => ({ count: state.count + 1 })),
  sleep: async () => {
    await sleep(2000);
    set((state) => ({ count: state.count + 30 }));
  }
}));

function Counter() {
  const { count, inc, sleep } = useStore();
  return (
    <div class="counter">
      <span>{count}</span>
      <button onClick={inc}>one up</button>
      <button onClick={sleep}>30 up</button>
    </div>
  );
}

export default function App() {
  return (
    <div class="main">
      <div class="code">
        <div class="code-container">
          <PrismCode className="language-jsx" children={code} />
          <Counter />
        </div>
      </div>
    </div>
  );
}

You can see that all the data is wrapped in CreateStore, which can define any type. It can be a stats type like count, or it can use functions (including asynchronous functions), which is the simplest. In addition, Zustand also provides some other utility functions and middleware. For more information on how to use middleware and utility functions, please go to NPM.

From the above, we can simply compare and contrast our principles:

  • TypeScript is supported – OK, but there are few examples in the description
  • Using functional programming — OK
  • Comfortable to use — OK
  • Low atomicity problem – medium, medium, may need to use middleware to package and extend the use
  • Support Esmodule — OK

rematch

Rematch, because some projects are using this library, so briefly look at the use. Rematchjs.org. v1 does not support TypeScript, but there are two ways to use it (for Effects).

Effects: {fetchInfo: async () = BB0 {const res = await requestInfo(); this.setState({ ... res; Return {fetchInfo: async () => {const res = await requestInfo(); return {fetchInfo: async (); dispatch.info.setInfo(res); }}}

In V2, TypeScript support was added, but the use of mode 1 above was removed, leaving only the second one in place. See Rematch TypeScript for examples. This is actually used in a slightly similar way to the Redex-Toolkit above, but it seems that downloads of Rematch have dropped quite a bit recently.

Rematch does a great job of modularizing and encapsulating all of the states, and then breaking them up by Models makes it more comfortable to use.

From the examples above and on our website, we can simply compare our principles:

  • TypeScript support — OK
  • Using functional programming — OK
  • Comfortable to use — OK
  • Low atomicity problem — OK
  • Support Esmodule — OK

concent

Concent, another implementation of a state manager, is used in much the same way as Vue3’s setup. Why do we talk about concent? Because there are several projects that use concent, and they do very well. The official website concentjs.concent has very powerful functions, and various dark magic. If you want to use concent well, there will be a relatively large learning cost, that is, it may not be very simple to use.

import { StrictMode } from "react"; import ReactDOM from "react-dom"; import { run, useConcent } from "concent"; const sleep = (time = 1000) => new Promise((r) => setTimeout(r, time)); run({ counter: { state: { value: "" }, computed: {}, lifecycle: {}, reducer: { decrement: async (payload, moduleState) => { await sleep(1000); return { value: moduleState.value - 1 }; }}}}); const setup = (ctx) => { ctx.effect(() => { ctx.setState({ value: 1000 }); } []); const increment = () => { console.log(1233); ctx.setState({ value: ctx.state.value + 1 }); }; return { increment }; }; export default function App() { const { state, settings, moduleReducer } = useConcent({ module: "counter", setup }); return ( <div className="App"> <h1>Hello Counter : {state.value}</h1> <div> <button onClick={settings.increment}>click increment</button> <button onClick={moduleReducer.decrement}>click decrement</button> </div> </div> ); } ReactDOM.render( <StrictMode> <App /> </StrictMode>, document.getElementById("root") );

The complete example and API can be viewed on the website. It supports both the class decorator mode and the setUp function, as well as the non-setUp method. The problem is that there are too many APIs and learning costs are high.

Based on our experience and the examples above, we can simply recompare our principles:

  • TypeScript support — OK
  • Using functional programming — OK
  • Use comfortable – OK, that is, the higher learning cost
  • Low atomicity issues — OK, support modularization, unified state management
  • Support Esmodule — OK

conclusion

At this point, the use of these several state management library and principle comparison has been almost completed. You are welcome to point out any improper use or incorrect explanation.

Finally, we considered the following points:

  • Support for TypeScript, using functional programming
  • Support modularization, state unified management
  • The learning cost is low, and the state management library is more popular
  • Consider everyone’s opinions, which is objective, and choose a library that everyone wants to use

The final choice was Redux’s official Toolkit for unified state management.

The end of this article, thanks for reading, welcome to discuss and exchange. Thank you for pointing out if there is any impropriety.

Focus on

Welcome everyone to pay attention to my public number [Delai asked the front], the article first in the public number above.

In addition to the daily collection of selected articles in the community, technical articles will also be shared from time to time.

I hope we can learn together and make progress together.