🧑🏻‍🏫

Reactのステート管理 5 つの原則

2024/03/24に公開

TL;DR

5 つの原則

  1. 常に一緒に変わる state は、一つにまとめる
  2. 矛盾しないように、関連する state は管理する
  3. 既存の state や props から計算できる値は state にしない
  4. 重複を避ける
  5. 深いネストを避け、フラットにする

はじめに

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.
ステートをうまく構造化することで、修正やデバッグがしやすいコンポーネントと、バグの絶えないコンポーネントの違いが生まれます。(DeepL 翻訳)
Choosing the State Structure

最近、React アプリケーションの state の重複と、それに伴う useEffect() の不適切な使用のリファクタリングを行う機会がありました。
その際に、Choosing the State Structure の記事で書かれている 5 つの原則を事前に意識できていれば、このような負債はある程度防げたはずであると思いました。
この記事では、その 5 つの原則を広めることを目的としています。
元記事の重要なポイントを基にしていますが、よりアクセスしやすいよう筆者なりに整理しました。したがって、筆者の解釈に基づく表現が含まれていることをご了承ください。

5 つの原則

1. 常に一緒に変わる state は、一つにまとめる

Group related state より

以下の座標の例のように、いつも一緒に変化する値は、1 つの state で管理しましょう。
そうすることで、値の同期に同期に悩まされることがなくなります。

  // 👎  `x` と `y` は常に一緒に変化するが、別々の state で管理されている
- const [x, setX] = useState(0)- const [y, setY] = useState(0)
  // 👍 1 つの state にまとめる
+ const [position, setPosition]=useState({ x: 0, y: 0 })

2. 矛盾しないように、関連する state は管理する

Avoid contradictions in state より

状態が矛盾しないよう、どのような state にどんな値を保持するか考えましょう。
矛盾が起きてしまう状況を防ぐことで、バグのリスクを減らすことができます。

export default function FeedbackForm() {
  // 👎 `isSending` と `isSent` が同時に `true` になることはないが、そのあり得ない状態が可能となっている
- const [isSending, setIsSending] = useState(false);
- const [isSent, setIsSent] = useState(false);

  // 👍 有効な値のうちの 1 つを持つ、1 つの state に置き換える('typing'(初期状態)、'sending'、'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');
  }
  ...

3. 既存の state や props から計算できる値は state にしない

Avoid redundant state より

不要な state 管理を減らすことで、コードがシンプルになります。
計算できる値はレンダリング時にその都度計算することで、同期が取れなくなる心配がなくなり、複雑度もバグのリスクも減らせます。

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

  // 👎 `fullName` は `firstName` と `lastName` から計算できるが、state としている
- const [fullName, setFullName] = useState('');
  // 👍 レンダリング時に `fullName` を計算する
+ 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);
  }
  ...
}
このような冗長な state は、useEffect() の誤った使い方を誘発する例でもあります😣
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');
  ...
  // 👎 `fullName` state を `useEffect()` を使って更新する
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  ...

You might not need an effect より

useEffect() の誤った利用は、余分な再レンダリングによりアプリケーションが遅くなり、必要以上に複雑化し、同期ミスによるバグを生む可能性が生みます。

4. 重複を避ける

Avoid redundant state より

重複している state を減らすことで、管理が楽になります。必要な情報だけを 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);
   // 👎 `items` state が持つ値の 1 つと同じオブジェクトが、`selectedItem` に保持されているので重複が起こっている
   //    `items = [{ id: 0, title: 'pretzels'}, ...]`
   //    `selectedItem = { id:0, title: 'pretzels'}`
-  const [selectedItem, setSelectedItem] = useState(items[0]);
   // 👍 `selectedId` を state に保持することで、重複を避け必要な状態だけを保持している
   //    `items = [{ id: 0, title:'pretzels'}, ...];
   //    `selectedId = 0`
+  const [selectedId, setSelectedId] = useState(0);

   // 👍 items配列からそのIDを持つitemを検索してselectedItemを取得
+  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;
      }
    }));
    // 👎 `selectedItem` の更新を行わないとバグになる
-   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>
    </>
  );
}

5. 深いネストを避け、フラットにする

Avoid deeply nested state より

複雑なネストは管理が難しくなりがちです。可能であれば、データ構造を見直し、フラットな構造にしましょう。

places.js
  // 👎 ネストされた構造
- export const initialTravelPlan = {
-   id: 0,
-   title: '(Root)',
-   childPlaces: [{
-     // 惑星レベル
-     id: 1,
-     title: 'Earth',
-     childPlaces: [{
-       // 大陸レベル
-       id: 2,
-       title: 'Africa',
-       childPlaces: [{
-         // 国レベル
-         id: 3,
-         title: 'Botswana',
-         childPlaces: []
-       }, { 
-         ...
-       }], 
-     }, {
-       // 大陸レベル
-       ...
-     }],
-   }, {
-     // 惑星レベル
-     ...
-   }],
- };
  // 👍 フラット化
+ 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: []
+   },
+   ...
+ };

まとめ

5 つの原則

  1. 常に一緒に変わる state は、一つにまとめる
  2. 矛盾しないように、関連する state は管理する
  3. 既存の state や props から計算できる値は state にしない
  4. 重複を避ける
  5. 深いネストを避け、フラットにする

元記事の最後の課題セクションでは、これらの原則が体系的に理解できるので、チャレンジしてみることをお勧めします!

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

Happy coding! 🚀


If you want to read this article in English, here is the link: https://dev.to/takuyakikuchi/five-principles-of-react-state-management-3l1n

GitHubで編集を提案

Discussion