DEV Community

TK
TK

Posted on

Five principles of React state management

If you want to read this article in Japanese here is the link: https://zenn.dev/takuyakikuchi/articles/225bc3d88d8538

TL;DR

5 principles

  1. Always combine states that change together into one
  2. Manage related states to avoid conflicts
  3. Do not assign values to states that can be calculated from existing states or props
  4. Avoid duplication
  5. Avoid deep nesting, make it flat

Introduction

Structuring state well can make a difference between a component that is pleasant to modify and debug, and one that is a constant source of bugs.
Choosing the State Structure

Recently, I had the opportunity to refactor a React application's duplication of states and the associated improper use of useEffect().
I thought that if we had been aware of the five principles described in the article Choosing the State Structure, we could have prevented some of these technical debts.
The purpose of this article is to disseminate those five principles.
It is based on the key points of the original article, but organized in the author's own way to make it more accessible. Therefore, please note that it contains expressions based on the author's interpretation.

Five principles

1. Always combine states that change together into one

From Group related state

As in the coordinate example below, values that always change together should be managed in a single state.
This way, you don't have to worry about synchronization of values.

  // πŸ‘Ž `x` and `y` always change together, but are managed in separate states
- const [x, setX] = useState(0);
- const [y, setY] = useState(0);

  // πŸ‘ combine into one state.
+ const [position, setPosition]=useState({ x: 0, y: 0 }
Enter fullscreen mode Exit fullscreen mode

2. Manage related states to avoid conflicts

From Avoid contradictions in state

To avoid contradictions in state, consider what values to keep in what states.
Avoiding contradictions reduces the risk of bugs.

export default function FeedbackForm() {
  // πŸ‘Ž `isSending` and `isSent` can never be `true` at the same time, but that improbable state is possible
- const [isSending, setIsSending] = useState(false);
- const [isSent, setIsSent] = useState(false);

  // πŸ‘ replace with a single state with one of the valid values ('typing' (initial state), 'sending', or 'sent')
+ const [status, setStatus] = useState('typing');

  async function handleSubmit(e) {
    e.preventDefault();
- setIsSending(true);
+ setStatus('sending');
    await sendMessage(text);
- setIsSending(false);
- setIsSent(true); + setStatus('sent'); await
+ setStatus('sent'); }
  }
  ...
Enter fullscreen mode Exit fullscreen mode

3. Do not assign values to states that can be calculated from existing states or props

From Avoid redundant state

Reducing unnecessary state management simplifies the code.
Computing computable values each time at render time eliminates the risk of out-of-sync, reducing complexity and the risk of bugs.

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // πŸ‘Ž `fullName` can be computed from `firstName` and `lastName`, but we use state
- const [fullName, setFullName] = useState('');
  // πŸ‘ compute `fullName` at render time.
+ const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
- setFullName(e.target.value + ' ' + lastName); }
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value); }
- setFullName(firstName + ' ' + e.target.value); }
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

Such a verbose state is also an example of inducing misuse of useEffect() 😣

  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');
  ...
  // πŸ‘Ž Update the `fullName` state using `useEffect()`.
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  ...
Enter fullscreen mode Exit fullscreen mode

From You might not need an effect

Misuse of useEffect() can slow down your application due to extra re-rendering, make them more complex than necessary, and create the potential for bugs due to synchronization errors.

4. Avoid redundancy

From Avoid redundant state

Reducing the number of duplicate states makes management easier.
Keep only the necessary information in state.

const initialItems = [
  { title: 'pretzels', id: 0 }, }
  { title: 'crispy seaweed', id: 1 }, }
  { title: 'granola bar', id: 2 }
];; }

export default function Menu() {
  const [items, setItems] = useState(initialItems);
   // πŸ‘Ž Duplication occurs because the same object as one of the values of the `items` state is held in the `selectedItem`.
   // `items = [{ id: 0, title: 'pretzels'}, ...] `
   // `selectedItem = { id: 0, title: 'pretzels'}`
- const [selectedItem, setSelectedItem] = useState(items[0]);.
   // πŸ‘ By keeping the `selectedId` in state, we avoid duplicates and keep only the state we need.
   // `items = [{ id: 0, title:'pretzels'}, ...] ;
   // `selectedId = 0`
+ const [selectedId, setSelectedId] = useState(0);

   // πŸ‘ get selectedItem by searching the items array for the item with that ID.
+ const selectedItem = items.find(item =>
+ item.id === selectedId
+ ); 

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ... .item,.
          title: e.target.value,.
        };
      } else {
        return item; }
      }
    }));
    // πŸ‘Ž update `selectedItem` or it will be a bug
- setSelectedItem(...) ;
  }

  return (
    <>
      <h2>What's your travel snack?</h2> 
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}. </p>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

5. Avoid deep nesting, make it flat

From Avoid deeply nested state

Complex nesting tends to be difficult to manage. If possible, rethink your data structure and make it flat.

  // πŸ‘Ž Deep nest
- export const initialTravelPlan = {
-   id: 0,
-   title: '(Root)',
-   childPlaces: [{
-     // ζƒ‘ζ˜Ÿγƒ¬γƒ™γƒ«
-     id: 1,
-     title: 'Earth',
-     childPlaces: [{
-       // 倧陸レベル
-       id: 2,
-       title: 'Africa',
-       childPlaces: [{
-         // 国レベル
-         id: 3,
-         title: 'Botswana',
-         childPlaces: []
-       }, { 
-         ...
-       }], 
-     }, {
-       // 倧陸レベル
-       ...
-     }],
-   }, {
-     // ζƒ‘ζ˜Ÿγƒ¬γƒ™γƒ«
-     ...
-   }],
- };
  // πŸ‘ Make it flat
+ export const initialTravelPlan = {
+   0: {
+     id: 0,
+     title: '(Root)',
+     childIds: [1, 42, 46],
+   },
+   1: {
+     id: 1,
+     title: 'Earth',
+     childIds: [2, 10, 19, 26, 34]
+   },
+   2: {
+     id: 2,
+     title: 'Africa',
+     childIds: [3, 4, 5, 6 , 7, 8, 9]
+   }, 
+   3: {
+     id: 3,
+     title: 'Botswana',
+     childIds: []
+   },
+   ...
+ };
Enter fullscreen mode Exit fullscreen mode

Summary

5 principles

  1. Always combine states that change together into one
  2. Manage related states to avoid conflicts
  3. Do not assign values to states that can be calculated from existing states or props
  4. Avoid duplication
  5. Avoid deep nesting, make it flat

The Challenges section at the end of the original article provides a systematic understanding of these principles, and I encourage you to take the challenge!

https://react.dev/learn/choosing-the-state-structure

Happy coding!

Top comments (0)