Talk about two state management libraries Redux & recoil of react

Time:2021-6-10

Talk about two state management libraries Redux & recoil of react

background

React is a very excellent UI library. At the beginning, react only focused on the UI layer and did not have a good solution for global state management. Therefore, it gave birth to excellent state management tools such as flux and redux.

With the development of time, a number of new state management tools have been developed.

Some current mainstream state management tools are sorted out

  1. Redux
  2. React Context & useReducer
  3. Mobx
  4. Recoil
  5. react-sweet-state
  6. hox

These are all the things I’ve been exposed to, the current situation and the future of NPMTrend comparison

Talk about two state management libraries Redux & recoil of react

Talk about two state management libraries Redux & recoil of react

without doubt,ReactandReduxThe combination of the current mainstream.

Today, in May, a man namedRecoil.jsNew members of Redux have entered my field of vision and brought some interesting models and concepts. Today we will make a simple comparison between Redux and Redux, hoping to inspire you.

text

Let’s look at Redux:

Redux

React Redux architecture diagram:

Talk about two state management libraries Redux & recoil of react

This model is relatively simple, and we are all familiar with it.

Let’s review the whole model with a simple example

actions.js

export const UPDATE_LIST_NAME = 'UPDATE_NAME';

reducers.js

export const reducer = (state = initialState, action) => {
    const { listName, tasks } = state;
    switch (action.type) {
        case 'UPDATE_NAME': {
            // ...
        }        
    default: {
            return state;
        }
    }
};

store.js

import reducers from '../reducers';
import { createStore } from 'redux';
const store = createStore(reducers);
export const TasksProvider = ({ children }) => (
    <Provider store={store}>
       {children}
    </Provider>
);

App.js

import { TasksProvider } from './store';
import Tasks from './tasks';
const ReduxApp = () => (
    <TasksProvider>
       <Tasks />
   </TasksProvider>
);

Component

// components
import React from 'react';
import { updateListName } from './actions';
import TasksView from './TasksView';

const Tasks = (props) => {
    const { tasks } = props;
    return (
        <TasksView tasks={tasks} />
    );
};

const mapStateToProps = (state) => ({
  tasks: state.tasks
});

const mapDispatchToProps = (dispatch) => ({
    updateTasks: (tasks) => dispatch(updateTasks(tasks))
});

export default connect(mapStateToProps, mapDispatchToProps)(Tasks);

Of course, you don’t need to connect,react-reduxProvideduseDispatch, useSelectorTwo hooks, very convenient.

import { useDispatch, useSelector } from 'react-redux';
const Tasks = () => {
    const dispatch = useDispatch();
    const name = useSelector(state => state.name);
    const setName = (name) => dispatch({ type: 'updateName', payload: { name } });
    return (
        <TasksView tasks={tasks} />
    );
};

Talk about two state management libraries Redux & recoil of react

The whole model is not complicated, andreduxA toolset has also been launchedredux toolkit, using thecreateSliceMethods to simplify some operations, for example:

// Action
export const UPDATE_LIST_NAME = 'UPDATE_LIST_NAME';

// Action creator
export const updateListName = (name) => ({
    type: UPDATE_LIST_NAME,
    payload: { name }
});

// Reducer
const reducer = (state = 'My to-do list', action) => {
    switch (action.type) {
        case UPDATE_LIST_NAME: {
            const { name } = action.payload;
            return name;
        }

        default: {
            return state;
        }
    }
};

export default reducer;

usecreateSlice

// src/redux-toolkit/state/reducers/list-name
import { createSlice } from '@reduxjs/toolkit';

const listNameSlice = createSlice({
    name: 'listName',
    initialState: 'todo-list',
    reducers: {
        updateListName: (state, action) => {
            const { name } = action.payload;
            return name;
        }
    }
});

export const {
    actions: { updateListName },
} = listNameSlice;

export default listNameSlice.reducer;

adoptcreateSlice, can reduce some unnecessary code, improve the development experience.

Still, Redux has some natural featuresdefect

  1. There are many concepts and mental burden.
  2. The properties should be picked one by one, and the calculation of properties should rely on reselect. There are also a series of problems such as magic strings, which are very troublesome to use, easy to make mistakes, and low development efficiency.
  3. The efficiency of triggering updates is also relatively poor. For the components that connect to the store, they must traverse one by one, and then compare the components to intercept unnecessary updates. This is undoubtedly a disaster in performance oriented or large-scale applications.

For this case, react itself also provides a solution, which we are familiar withContext.

Talk about two state management libraries Redux & recoil of react

<MyContext.Provider value={/* some value */}>

<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>

Add a provider to the parent node and a consumer to the child node. However, for each additional item, there must be more providers

Talk about two state management libraries Redux & recoil of react

What’s more, usingContextThere are also many problems.

For the use ofuseContextThe most prominent problem is thatre-render.

However, there are also corresponding optimization schemesReact-tracked.

For example:

// store.js
import React, { useReducer } from 'react';
import { createContainer } from 'react-tracked';
import { reducers } from './reducers';

const useValue = ({ reducers, initialState }) => useReducer(reducer, initialState);
const { Provider, useTracked, useTrackedState, useUpdate } = createContainer(useValue);

export const TasksProvider = ({ children, initialState }) => (
    <Provider reducer={reducer} initialState={initialState}>
        {children}
    </Provider>
);

export { useTracked, useTrackedState, useUpdate };

Correspondingly, there are alsohooksedition:

const [state, dispatch] = useTracked();
const dispatch = useUpdate();
const state = useTrackedState();

// ...

Recoil

Recoil.js provides another idea. Its model is as follows:

Talk about two state management libraries Redux & recoil of react

Create another orthogonal tree on react tree and extract the state of each item.

Each component has its own state. When the data is updated, the corresponding component will also be updated.

Recoil calls each piece of data atom, which is a variable state unit that can be subscribed to.

This may be a bit abstract. Let’s take a simple example

// index.js

import React from "react";
import ReactDOM from "react-dom";
import { RecoilRoot } from "recoil";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";

ReactDOM.render(
  <React.StrictMode>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </React.StrictMode>,
  document.getElementById("root")
);

Recoil Root

Provides the context in which atoms have values. Must be an ancestor of any component that uses any Recoil hooks. Multiple roots may co-exist; atoms will have distinct values within each root. If they are nested, the innermost root will completely mask any outer roots.

Recoilroot can be regarded as a top-level provider

Atoms

Suppose you want to implement a counter:

Talk about two state management libraries Redux & recoil of react

First, use usestate

import React, { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <button onClick={() => setCount(count - 1)}>Decrease</button>
      <div>Count is {count}</div>
    </div>
  );
};

export default App;

Rewrite it with atom

import React from "react";
import { atom, useRecoilState } from "recoil";

const countState = atom({
  key: "counter",
  default: 0,
});

const App = () => {
  const [count, setCount] = useRecoilState(countState);
  
  return (
    <div className="app">
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <button onClick={() => setCount(count - 1)}>Decrease</button>
      <div>Count is {count}</div>
    </div>
  );
};

export default App;

Seeing this, you may have a preliminary understanding of atom.

What is the concept of atom?

Atom

Simply understand, atom is a collection of data, which can be shared and modified.

A component can subscribe to an atom, one or more. When the atom changes, it triggers rendering again.

const someState = atom({
    key: 'uniqueString',
    default: [],
});

Each atom has two parameters:

  • key: string used to internally identify atom. be relative toEntire applicationThe string should beThe only
  • default: the initial value of atom.

Atom is the smallest unit of storage state. A reasonable design is to keep atom as small as possible and keep maximum flexibility.

Author of recoil, inReactEurope videoIn this paper, we also introduce a new method to encapsulate atom

export const itemWithId =
    memoize(id => atom({
        key: `item${id}`,
        default: {...},
    }));

Selectors

Official description:

“A selector is a pure function that accepts atoms or other selectors as input. When these upstream atoms or selectors are updated, the selector function will be re-evaluated.”

Selector is a pure function with atom as parameter. When atom changes, it will trigger recalculation.

The selector has the following parameters:

  • key: string used to internally identify atom. be relative toEntire applicationThe string should beThe only.
  • get: function passed as an object{ get }Of whichgetIs a function that retrieves a value from another atom or selector. Any atom or selector passed to this function is implicitly added to the selector’s dependency list.
  • set?: an optional function that returns the new writable state. It acts as an object{ get, set }And a new value.getIs a function that retrieves values from other atom or selector.setIs a function that sets the atomic value, where the first parameter is the atomic name and the second parameter is the new value.

Let’s take a concrete example

import React from "react";
import { atom, selector, useRecoilState, useRecoilValue } from "recoil";

const countState = atom({
  key: "myCount",
  default: 0,
});

const doubleCountState = selector({
  key: "myDoubleCount",
  get: ({ get }) => get(countState) * 2,
});

const inputState = selector({
  key: "inputCount",
  get: ({ get }) => get(doubleCountState),
  set: ({ set }, newValue) => set(countState, newValue),
});

const App = () => {
  const [count, setCount] = useRecoilState(countState);
  const doubleCount = useRecoilValue(doubleCountState);
  const [input, setInput] = useRecoilState(inputState);
  
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <button onClick={() => setCount(count - 1)}>Decrease</button>
      <input type="number" value={input} onChange={(e) => setInput(Number(e.target.value))} />
      <div>Count is {count}</div>
      <div>Double count is {doubleCount}</div>
    </div>
  );
};

export default App;

It’s easy to understand,useRecoilStateuseRecoilValueThese basic concepts can be referred toOfficial documents

In addition, the selector can be asynchronous, such as:

  get: async ({ get }) => {
    const countStateValue = get(countState);
    const response = await new Promise(
      (resolve) => setTimeout(() => resolve(countStateValue * 2)),
      1000
    );
    return response;
  }

However, for asynchronous selectors, you need toRecoilRootAdd a layerSuspense:

ReactDOM.render(
  <React.StrictMode>
    <RecoilRoot>
      <React.Suspense fallback={<div>Loading...</div>}>
          <App />
      </React.Suspense>
    </RecoilRoot>
  </React.StrictMode>,
  document.getElementById("root")
);

Redux vs Recoil

Model comparison:

Talk about two state management libraries Redux & recoil of react

Recoil recommends that atom should be small enough so that each leaf component can subscribe to it separately, and when the data changes, it can achieve o (1) level update

Recoil authorDave McCabestayIn a commentmention:

Well, I know that on one tool we saw a 20x or so speedup compared to using Redux. This is because Redux is O(n) in that it has to ask each connected component whether it needs to re-render, whereas we can be O(1).

useReducer is equivalent to useState in that it works on a particular component and all of its descendants, rather than being orthogonal to the React tree.

Rocil can update o (1) because when the data of the atom changes, only the components that subscribe to the atom need re render.

However, in Redux, we can use selector to achieve the same effect:

// selector
const taskSelector = (id) => state.tasks[id];

// component code
const task = useSelector(taskSelector(id));

However, a small problem here is that when the state changes, the task selector will also recalculate, but we can use thecreateSelectorTo optimize, for example:

import { createSelector } from 'reselect';

const shopItemsSelector = state => state.shop.items;

const subtotalSelector = createSelector(
  shopItemsSelector,
  items => items.reduce((acc, item) => acc + item.value, 0)
)

Write here, do you want to say, this is it? After all this talk, what rocoil can do and Redux can do, what’s your use?

Ha ha, this is really a bit embarrassing.

But I think it’s a pattern change. Recoil encourages each state to be small enough, arbitrarily combined and updated in a minimum range.

In Redux, our habit is to connect the container components to the store. As for the sub components, it doesn’t matter if they are passed down one layer.

I think that recoil’s design may pay attention to performance issues and optimize the performance of super large applications.

At present, recoil is still in its infancyToy stageThere are still a lot of issues to deal with, but it is worthy of further attention.

last

Interested friends can have a look, do a ToDo list experience.

I hope this article can help you.

If there are any mistakes in this article, please correct them.

reference material

  1. http://react.html.cn/docs/context.html#reactcreatecontext
  2. https://recoiljs.org/docs/basic-tutorial/atoms
  3. https://www.emgoto.com/react-state-management/
  4. https://medium.com/better-programming/recoil-a-new-state-management-library-moving-beyond-redux-and-the-context-api-63794c11b3a5