Why do YOU need state management

In React project development, with the increase of application complexity, component splitting is increasing, and the sharing of state between different components becomes more and more difficult to manage, mainly reflected in the following two aspects:

  1. To access parent component state, a child component needs to pass through layers:

  2. When state is shared between non-parent-child components, it is necessary to promote state to the common parent and set the update function, and then pass layer by layer:

The above approach results in very strong coupling between components, and when the component structure needs to change, the transfer logic needs to be significantly modified, resulting in very low flexibility and maintainability. In addition, since state has been raised to “global”, when state changes, all sub-components, including some that did not use these states before, will also be subject to Re-render. If there are many sub-components, performance will be greatly reduced.

The basic solution

People who have always wanted to overcome everything can not be held back by their own inventions. The best way to solve a problem is to solve it, so different state management solutions emerge, each with their own unique ideas. The two most familiar solutions are Redux and Mobx. Using the state management library, you can extract the states that need to be shared globally and then fetch them as needed by different components:

We’re in taming the React path of the project was a big step, but every time at a new height, always run into the problem of “oxygen”, brought by the highly although these state management library good solve the original problem, but their study cost and access cost always let people hesitate and stop, if the total holding just a matter of learning attitude, Body and mind will be more and more tired, back to nature is the ultimate ultimate meaning.

Use the Context API

The React Context API was then born, enabling state sharing without introducing a third party framework. For example, to implement the global sharing of name and age states, without considering any optimization, it can be written like this:

An unoptimized version of the implementation

Step 1: Implementation of the Provider component

import React, { useState, useContext, createContext } from 'react';
import ReactDOM from 'react-dom';

const ctx = createContext(null);
const Provider = (props) = > {
  const [name, setName] = useState(' ');
  const [age, setAge] = useState(0);

  return (
    <ctx.Provider
      value={{
        name.age.setName.setAge,}} >
      {props.children}
    </ctx.Provider>
  );
};
Copy the code

Part two: Child component references


const NameCmp = () = > {
  const { name } = useContext(ctx);
  console.log('name component render');
  return <div>my name is {name}</div>;
};

const AgeCmp = () = > {
  const { age } = useContext(ctx);
  console.log('age component render');
  return <div>my age is {age}</div>;
};

const ControlCmp = () = > {
  console.log('control component render');
  const { setName, setAge } = useContext(ctx);

  const onChangeNameClick = () = > {
    setName(`leoThe ${Date.now()}`);
  };

  const onChangeAgeClick = () = > {
    setAge((prev) = > prev + 1);
  };

  return (
    <div>
      <button onClick={onChangeNameClick}>change name</button>
      <button onClick={onChangeAgeClick}>add age</button>
    </div>
  );
};
Copy the code

Finally: App implementation

const App = () = > {
  return (
    <Provider>
      <NameCmp />
      <AgeCmp />
      <ControlCmp />
    </Provider>
  );
};

ReactDOM.render(<App />.document.getElementById('root1'));
Copy the code

After running, you can see that only name or age is changed, but all three components are re-render:

Optimized version of the implementation

Since there is no third-party library support, we need to manually optimize to reduce unnecessary re-render. The modified implementation is as follows:

In the Provider implementation, the original single context is split into multiple:

import React, { useState, useContext, createContext, useMemo, useCallback } from 'react';
import ReactDOM from 'react-dom';

const ctxNameState = createContext(null);
const ctxAgeState = createContext(null);
const ctxDispatch = createContext(null);
const Provider = (props) = > {
  const [name, setName] = useState(' ');
  const [age, setAge] = useState(0);
  const memoDispatch = useMemo(() = > {
    return{ setName, setAge, }; } []);return (
    <ctxDispatch.Provider value={memoDispatch}>
      <ctxNameState.Provider value={name}>
        <ctxAgeState.Provider value={age}>{props.children}</ctxAgeState.Provider>
      </ctxNameState.Provider>
    </ctxDispatch.Provider>
  );
};
Copy the code

When the child component is used, it introduces the appropriate context:

const NameCmp = () = > {
  console.log('name component render');
  const name = useContext(ctxNameState);
  return <div>my name is {name}</div>;
};

const AgeCmp = () = > {
  console.log('age component render');
  const age = useContext(ctxAgeState);
  return <div>my age is {age}</div>;
};

const ControlCmp = () = > {
  console.log('control component render');

  const { setName, setAge } = useContext(ctxDispatch);

  const onChangeNameClick = () = > {
    setName(`leoThe ${Date.now()}`);
  };

  const onChangeAgeClick = () = > {
    setAge((prev) = > prev + 1);
  };

  return (
    <div>
      <button onClick={onChangeNameClick}>change name</button>
      <button onClick={onChangeAgeClick}>add age</button>
    </div>
  );
};
Copy the code

After executing, you can see that name only changes the name component, the other two components will not re-render, the same as age:

The obvious downside of this is that, to minimize unnecessary re-render, we need to create a separate context for each state to avoid interference with each other, but as more and more global states are created, our code will look like this:

const ctx1 = createContext(null);
const ctx2 = createContext(null);
// ...
const ctxN = createContext(null);

const Provider = (props) = > {
  const xx1 = useState(x);
  const xx2 = useState(x);
  // ...
  const xxN = useState(x);
  return (
    <ctx1.Provider value={xx1}>
      <ctx2.Provider value={xx2}>
        <ctx3.Provider value={xx3}>
          <ctxN.Provider value={xxN}>
            {props.children}
          </ctxN.Provider>
        </ctx3.Provider>
      </ctx2.Provider>
    </ctx1.Provider>
  );
};

Copy the code

Even if you can live with this, as the number of Provider packages increases, if the context is still dependent on each other, such as ctx3 state in CTx9, Provider must be wrapped around ctx3.Provider, so every time a new global state is added, we need to comb through the nesting relationship first, otherwise an exception will occur. How to flatten the Provider seems difficult to handle.

The use of Recoil

Recoil is an open source React state management library for Facebook. For compatibility and simplicity, it is best to use the React built-in state management capabilities rather than third-party global state management. However, React has a number of drawbacks in this area: We wanted to improve on this while keeping the React style as consistent as possible.

Recoil version of the implementation

So how to do this with Recoil:

First install Recoil:

npm i recoil
Copy the code

Then configure Recoil by wrapping the RecoilRoot component around the outer layer.

import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot, atom, useRecoilState } from 'recoil';

const App = () = > {
  return (
    <RecoilRoot>
      <NameCmp />
      <AgeCmp />
      <ControlCmp />
    </RecoilRoot>
  );
};

Copy the code

Defining what content needs to be shared is called Atom in Recoil:


const nameAtom = atom({
  key: 'name'.default: ' '});const ageAtom = atom({
  key: 'age'.default: 0}); ReactDOM.render(<App />.document.getElementById('root3'));
Copy the code

The child component uses atom above via the useRecoilState provided by Recoil. This hook takes atom and returns a value similar to useState:

const NameCmp = () = > {
  console.log('name component render');
  const [name] = useRecoilState(nameAtom);
  return <div>my name is {name}</div>;
};

const AgeCmp = () = > {
  console.log('age component render');
  const [age] = useRecoilState(ageAtom);
  return <div>my age is {age}</div>;
};

const ControlCmp = () = > {
  console.log('control component render');
  const [, setName] = useRecoilState(nameAtom);
  const [, setAge] = useRecoilState(ageAtom);

  const onChangeNameClick = () = > {
    setName(`leoThe ${Date.now()}`);
  };

  const onChangeAgeClick = () = > {
    setAge((prev) = > prev + 1);
  };

  return (
    <div>
      <button onClick={onChangeNameClick}>change name</button>
      <button onClick={onChangeAgeClick}>add age</button>
    </div>
  );
};

Copy the code

As you can see from the examples above, Recoil is very easy to get into and use, and very React-style.

What is the selector

Another important concept in Recoil is selector, which has a similar relationship with Atom to selector = f(atomA, atomB…). Automatically triggers updates whenever the selector dependent Atom value changes:

import { atom, selector } from 'recoil';

const ageAtom = atom({
  key: 'age'.default: 0});export const ageLabelSelector = selector({
	key: 'ageLabelSelector'.get({get}) {
		// This selector relies on ageAtom, which is automatically updated whenever the ageAtom value changes
		const ageState = get(ageAtom);
		return `${ageState}At the age of `}})Copy the code

By using selectors, you can keep Atom as primitive as possible.

Encapsulate custom hooks

For those of you familiar with Redux, how do you do an action-like operation? The answer is to wrap custom hooks, using a Todo-app as an example:

Step 1: Define global state to hold all todo items

const todoListAtom = atom({
  key: "todoList".default: [],});Copy the code

Part two: Encapsulation hook


function useTodo() {
  const [list, setList] = useRecoilState(todoListAtom);

  const dispatch = ({ type, payload }) = > {
    switch (type) {
      case 'ADD':
        setList((prev) = > {
          return prev.concat({ id: Date.now().toString(), content: payload.content });
        });
      case 'DELETE':
        setList((prev) = > prev.filter((l) = > l.id !== payload.id));
    }
  };
  return {
    dispatch,
    list,
  };
}
Copy the code

Final step: Use

const TODO = () = > {
  const { dispatch, list } = useTodo();
  const [input, setInput] = useState(' ');

  const onInputChange = (ev) = > {
    setInput(ev.target.value);
  };

  return (
    <>
      <input type="text" value={input} onChange={onInputChange} />
      <button
        onClick={()= >{ dispatch({ type: 'ADD', payload: { content: input, }, }); }} > add</button>

      {list.map((i) => {
        return (
          <div key={i.id}>
            <span>{i.content}</span>
            <button
              onClick={()= >{ dispatch({ type: 'DELETE', payload: { id: i.id, }, }); }} > delete</button>
          </div>
        );
      })}
    </>
  );
};

const App = () = > {
  return (
    <RecoilRoot>
      <TODO />
    </RecoilRoot>
  );
};

ReactDOM.render(<App />.document.getElementById('root'));
Copy the code

Three similar apis/Hooks

In addition to useRecoilState similar to useState, the API provided by Recoil also provides useRecoilValue and useSetRecoil, whose return values are as follows:

const [name, setName] = useRecoilState(nameAtom)
const name = useRecoilValue(nameAtom)
const setName = useSetRecoil(nameAtom)
Copy the code

Why provide the latter two hooks? As you can see in the react Context demo, both the set function and the state function use the context separately. If the set function and the state function are placed together, then when the state is updated, Other components that use set but do not use state will also be updated, resulting in unnecessary re-render, so Recoil provides these two hooks to “decouple” and use the right hooks in the right places to reduce re-render to improve page performance.

Recoil summary

In addition to the basic capabilities mentioned above, Recoil also supports asynchronous Atom/Seletor, Suspense, Atom Effect, and more to make state management very useful and fun. At present Recoil is still experimental, some API is still unstable, but does not involve major functions, can be put into practice in some small and medium-sized projects, in addition to atom debugging tool is still under development officially. There is no visual tool like Redux’s DevTools to track and debug state changes, but apis are available for simple implementation.

In short, we use Recoil the same way we use React hooks, the only difference is that the former state lives in the current component and the latter “live forever” anywhere outside the component. As Recoil continues to evolve, it will one day become a mainstream status management library that everyone knows about.

reference

  • Reactjs.org/docs/contex…
  • www.youtube.com/watch?v=_IS…