Summary of three rules about React state management

  • 2021-11-10 08:28:12
  • OfStack

Directory Preface No.1 1 Focus
No. 2 Extracting Complex State Logic
No. 3 Extract multiple state operations
Summarize

Preface

The state inside the React component is encapsulated data that remains the same between rendering processes. useState () is React hook and is responsible for managing the internal state of the functional components.

I like useState (), which really makes state handling very easy. But I often encounter similar problems:

Should I divide the state of the component into small states or keep the composite state? If state management becomes complicated, should I extract it from components? What should I do? If the use of useState () is so simple, when do you need useReducer ()?

This article introduces three simple rules that can answer the above questions and help you design the state of components.

No.1 1 Focus

The first rule for effective state management is:

Make the state variable responsible for 1 problem.

Make the state variable responsible for one problem and make it conform to the principle of single one responsibility.

Let's look at an example of a composite state, that is, a state with multiple state values.


const [state, setState] = useState({
    on: true,
    count: 0
});

state.on    // => true
state.count // => 0

The state consists of a normal JavaScript object with on and count attributes.

The first attribute, state. on, contains a Boolean value for the switch. Similarly, ` ` state. count ` contains a number representing a counter, for example, the number of times the user clicked a button.

Then, suppose you want to increase the counter by 1:


// Updating compound state
setUser({
    ...state,
    count: state.count + 1
});

You must put the whole status on 1 to update only count. This is a large structure called to simply add a counter: this is all because state variables are responsible for two things: switches and counters.

The solution is to divide the composite state into two atomic states, on and count:


const [on, setOnOff] = useState(true);
const [count, setCount] = useState(0);

The state variable on is only responsible for storing the switch state. Similarly, the count variable is only responsible for counters.

Now, let's try to update the counter:


setCount(count + 1);
// or using a callback
setCount(count => count + 1);

count state is only responsible for counting, which is easy to infer, update and read.

There is no need to worry about calling multiple useState () to create state variables for each concern.

Note, however, that if you use too many useState () variables, your component is likely to violate the "single 1 duty principle". Simply split such components into smaller components.

No. 2 Extracting Complex State Logic

Extract complex state logic into custom hook.

Does it make sense to keep complex state operations within components?

The answer comes from fundamentals (which usually happens).

React hook was created to isolate components from complex state management and side effects. Therefore, since the component should only focus on the elements to be rendered and some event listeners to be attached, the complex state logic should be extracted into the custom hook.

Consider a component that manages the list of products. Users can add new product names. The constraint is that the product name must be only 1.

The first attempt is to keep the setup program for the product name list directly inside the component:


function ProductsList() {
    const [names, setNames] = useState([]);  
    const [newName, setNewName] = useState('');

    const map = name => <div>{name}</div>;

    const handleChange = event => setNewName(event.target.value);
    const handleAdd = () => {    
        const s = new Set([...names, newName]);    
        setNames([...s]);  };
    return (
        <div className="products">
            {names.map(map)}
            <input type="text" onChange={handleChange} />
            <button onClick={handleAdd}>Add</button>
        </div>
    );
}

The names state variable holds the product name. When the Add button is clicked, the addNewProduct () event handler is called.

Inside addNewProduct (), the Set object is used to keep the product name only 1. Should components pay attention to this implementation detail? No need.

It is best to isolate the complex state setter logic into a custom hook. Let's start doing it.

The new custom hook useUnique () keeps each item unique:


// useUnique.js
export function useUnique(initial) {
    const [items, setItems] = useState(initial);
    const add = newItem => {
        const uniqueItems = [...new Set([...items, newItem])];
        setItems(uniqueItems);
    };
    return [items, add];
};

After extracting custom state management into one hook, the ProductsList component becomes even lighter:


import { useUnique } from './useUnique';

function ProductsList() {
  const [names, add] = useUnique([]);  const [newName, setNewName] = useState('');

  const map = name => <div>{name}</div>;

  const handleChange = event => setNewName(e.target.value);
  const handleAdd = () => add(newName);
  return (
    <div className="products">
      {names.map(map)}
      <input type="text" onChange={handleChange} />
      <button onClick={handleAdd}>Add</button>
    </div>
  );
}

const [names, addName] = useUnique ([]) Enables custom hook. This component is no longer troubled by complex state management.

If you want to add a new name to the list, just call add ('New Product Name').

Most importantly, the benefits of extracting complex state management into a custom hooks are:

This component no longer contains state management details Custom hook can be reused Custom hook for easy isolation testing

No. 3 Extract multiple state operations

Extract multiple state operations into the simplifier.

Continuing with the ProductsList example, let's introduce the "delete" action, which removes the product name from the list.

Now, you must code two operations: add and delete products. By handling these operations, you can create a simplifier and get rid of the state management logic of components.

Again, this approach follows the idea of hook: extracting complex state management from components.

The following is one implementation of reducer for adding and removing products:


function uniqueReducer(state, action) {
    switch (action.type) {
        case 'add':
            return [...new Set([...state, action.name])];
        case 'delete':
            return state.filter(name => name === action.name);
        default:
            throw new Error();
    }
}

You can then use uniqueReducer () in the product list by calling useReducer () hook of React:


function ProductsList() {
    const [names, dispatch] = useReducer(uniqueReducer, []);
    const [newName, setNewName] = useState('');

    const handleChange = event => setNewName(event.target.value);

    const handleAdd = () => dispatch({ type: 'add', name: newName });
    const map = name => {
        const delete = () => dispatch({ type: 'delete', name });    
        return (
            <div>
                {name}
                <button onClick={delete}>Delete</button>
            </div>
        );
    }

    return (
        <div className="products">
            {names.map(map)}
            <input type="text" onChange={handleChange} />
            <button onClick={handleAdd}>Add</button>
        </div>
    );
}

const [names, dispatch] = useReducer (uniqueReducer, []) Enable uniqueReducer. names is a state variable that holds the product name, while dispatch is a function called using an action object.

When the Add button is clicked, the handler calls dispatch ({type: 'add', name: newName}). Scheduling an add action causes reducer uniqueReducer to add a new product name to the status.

In the same way, when the Delete button is clicked, the handler calls dispatch ({type: 'delete', name}). The remove operation removes the product name from the name state.

Interestingly, reducer is a special case of command mode.

Summarize

State variables should focus on only one point.

If the state has complex update logic, extract that logic from the component into a custom hook.

Similarly, if the state requires multiple operations, merge these operations with reducer.

No matter what rules you use, the state should be as simple and separate as possible. Components should not be bothered by the details of status updates: They should be part of a custom hook or Simplifier 1.

These three simple rules can make your state logic easy to understand, maintain and test.


Related articles: