Summary of three rules for React state management

Summary of three rules for React state management

Preface

The state inside a React component is encapsulated data that persists between rendering passes. useState() is a React hook responsible for managing the state inside functional components.

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

  • Should I divide the component's state into small states, or keep it as a composite state?
  • If state management gets complicated, should I extract it from the component? What to do?
  • If the usage of useState() is so simple, then when is useReducer() needed?

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

No.1 A focus

The first rule of effective state management is:

Make the state variable responsible for a problem.

Making a state variable responsible for one concern makes it comply with the Single Responsibility Principle.

Let's look at an example of a composite state, i.e. a state that contains multiple state values.

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

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

The state consists of a plain JavaScript object with on and count properties.

The first property, state.on, contains a Boolean value, indicating a switch. Likewise, ``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 have to keep the whole state together to update only count. This is a large structure to call just to simply increment a counter: This is all because the state variable is responsible for two things: the switch and the counter.

The solution is to split 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. Again, the count variable is responsible only for the counter.

Now, let's try to update the counter:

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

The count state is just a count, and is easy to reason about, update, and read.

Don't worry about calling multiple useState() to create state variables for each concern.

But please note that if you use too many useState() variables, your component is likely to violate the "Single Responsibility Principle". Just split such components into smaller ones.

No.2 Extracting complex state logic

Extract complex state logic into custom hooks.

Does it make sense to keep complex state manipulation inside components?

The answer comes from fundamentals (as it usually happens).

React hooks were created to isolate components from complex state management and side effects. Therefore, since a component should only be concerned with which elements to render and which event listeners to attach, complex state logic should be extracted into a custom hook.

Consider a component that manages a list of products. User can add new product names. The constraint is that product names must be unique.

The first attempt was to keep the setter for the list of product names 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 names. When the Add button is clicked, the addNewProduct() event handler is called.

Inside addNewProduct(), a Set object is used to keep product names unique. Should the component be concerned with this implementation detail? unnecessary.

It's best to isolate complex state setter logic into a custom hook. Let’s get started.

New custom hook useUnique() to make 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];
};

By extracting the custom state management into a hook, the ProductsList component becomes more lightweight:

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 hooks. The component is no longer bogged down 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 custom hooks are:

  • This component no longer contains the details of state management
  • Custom hooks can be reused
  • Custom hooks can be easily tested in isolation

No.3 Extract multiple state operations

Extract multiple state operations into reducers.

Continuing with the ProductsList example, let's introduce a "delete" operation that will remove a product name from the list.

Now, you have to code two operations: adding and removing products. By handling these operations, you can create a simplifyer and free your component from state management logic.

Again, this approach fits the idea of ​​hooks: extracting complex state management from components.

Here is one implementation of a reducer that adds and removes 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() on your list of products by calling React’s useReducer() hook:

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, []) enables uniqueReducer. names is the state variable that holds the names of the products, and dispatch is the function that is called with the action object.

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

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

Interestingly, reducers are a special case of the command pattern.

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.

Likewise, if the state requires multiple actions, use a reducer to combine those actions.

Whatever rules you use, the state should be kept as simple and decoupled as possible. Components should not be bothered with the details of state updates: they should be part of custom hooks or reducers.

These 3 simple rules will make your state logic easy to understand, maintain, and test.

This concludes this article on the three rules of React state management. For more content related to React state management, please search for previous articles on 123WORDPRESS.COM or continue to browse the following related articles. I hope you will support 123WORDPRESS.COM in the future!

You may also be interested in:
  • Exploration of three underlying mechanisms of React global state management
  • Example of using Vue's state management method in React
  • Interpretation and usage of various React state managers

<<:  Encoding problems and solutions when mysql associates two tables

>>:  How to create scheduled tasks using crond tool in Linux

Recommend

Example code for implementing stacked carousel effect with HTML+CSS+JS

Effect: When the slideshow moves in one direction...

Usage of MySQL time difference functions TIMESTAMPDIFF and DATEDIFF

Usage of time difference functions TIMESTAMPDIFF ...

Detailed explanation of how to use several timers in CocosCreator

1. setTimeOut Print abc after 3 seconds. Execute ...

Perfect solution for vertical centering of form elements

Copy code The code is as follows: <!DOCTYPE ht...

Vue+Bootstrap realizes a simple student management system

I used vue and bootstrap to make a relatively sim...

How to implement multiple parameters in el-dropdown in ElementUI

Recently, due to the increase in buttons in the b...

The difference between Readonly and Disabled

To summarize: Readonly is only valid for input (te...

How to configure MySQL on Ubuntu 16.04 server and enable remote connection

background I am learning nodejs recently, and I r...

A brief introduction to the command line tool mycli for operating MySQL database

GitHub has all kinds of magic tools. Today I foun...

Docker View JVM Memory Usage

1. Enter the host machine of the docker container...

How to elegantly implement WeChat authorized login in Vue3 project

Table of contents Preface Prepare Implementation ...

Use Nginx to build a streaming media server to realize live broadcast function

Written in front In recent years, the live stream...

Detailed explanation of how to pass password to ssh/scp command in bash script

Install SSHPASS For most recent operating systems...

Case analysis of several MySQL update operations

Table of contents Case Study Update account balan...