😺

React Docs BETAを読む 【状態の管理】

2022/07/06に公開

最近はフロントエンドの実装が多く、Reactについて再学習をしているところです。
後輩に教える機会なども増えてきているので改めてしっかりと知識をつけたいと思いました。
そこで英語なので避けてきたReact Doc Betaを読んでいるので、併せてまとめていきます。

Managing State

状態の管理という章を今回は読んでいきます。
useStateなど一番身近な項目なのでしっかりと復習していきます。

Redundant or duplicate state is a common source of bugs.

とある通り、状態が重複していたり、冗長であることはバグにつながるのでしっかり勉強したいところです。
今のプロジェクトに入りたてのときに経験した痛い思いが蘇ります...

Reacting to input with state

直訳は"状態で入力に反映する"とかですかね。
Reactではコードから直接UIを変更しません。
入力に対してStateを変化させ、その状態に応じてコンポーネントの制御を行います。
以下のようなボタンタグの場合, statusというStateに応じて活性か非活性化を切り替えます。
このstatusがsubmitされた際に変化し、終了後にまた変化することでUIが変化するわけですね。

<button disabled={
  status === 'submitting'
}>
  Submit
</button>

Choosing the state structure

Stateの構造の話の節ですね。
Stateをうまく構造化できると、保守性の高いコンポーネントを作ることができます。
逆にうまく構造化できなければ、バグの原因となるコンポーネントになりやすくなります。

The most important principle is that state shouldn’t contain redundant or duplicated information.

上記の通り、冗長な情報や重複した情報は含めてはいけません。
例えば以下のコンポーネントはfullNameというStateを持っています。
これはfirstName + lastNameなのでそれぞれのコンポーネントと重複しており無駄になります。

import { useState } from 'react';

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

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

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

fullNameを表示したければ、単純に

 const fullName = firstName + ' ' + lastName;

と変数を持ってあげれば良いだけですね。
これも別の章に書いてあったと思うんですが、計算量の少ない処理であること前提で、このように処理してあげるだけで、わざわざStateで持たなくてもいいケースは結構ありますね。

This might seem like a small change, but many bugs in React apps are fixed this way.

と書いてあって、Reactアプリのバグの多くは上記の方法で修正されているようです。
結構やりがちなミスなので気をつけないといけないですね....

Sharing state between components

Stateをコンポーネント間で共有する方法を解説した節です。
前回の記事で"Lifting State Up"について書きました。
この節はその話のようですね。
Stateを近接する親コンポーネントに押し上げて、状態を共有するという話でした。

Preserving and resetting state

状態の保持とリセットについての節です。
Reactのデフォルトでは以前にレンダリングされたツリーと一致するツリーのを保持します。
このデフォルトの動作がうまく機能しないときは意図的に状態をリセットすることがあります。
Docsの例だと、コンポーネントのkeyに別の値を渡すことで強制的に0からコンポーネントを作成しています。
こういうリセットは、リセットするための処理をuseEffectの中に書いてしまったことが過去にありましたが、こういうコントロールの仕方もあったんですね。

Extracting state logic into a reducer

StateのロジックをReducerに抽出するという節ですね。
なんのことだろうと思ったんですが、Stateの更新を行うハンドラが複数ある場合に、それReducerにまとめてしまうという話でした。
イベントハンドラはアクションを指定して、Stateに更新をかけるだけなので管理しやすいし、読みやすいですね。

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Visit Kafka Museum', done: true },
  { id: 1, text: 'Watch a puppet show', done: false },
  { id: 2, text: 'Lennon Wall pic', done: false }
];

結構大きめのコンポーネントだとStateがとっ散らかるケースが多いので、場合に応じて抽出してあげると良いですね。
リファクタリングできそうなコードがいくつか思い当たります。

Passing data deeply with context

useContextを使ってコンテキストオブジェクトを生成することで、親コンポーネント配下で明示的なpropsなしで情報を利用できるようになります。
何故か私の関わっているプロジェクトでは見かけないHooksです。
利用シーンは思い当たりますし、使い方さえ間違えなければ便利そうなんですが、なぜか見かけないですね。
私の関わっているアプリケーションの場合は見かけませんが、こういう使い方をしているとかあれば教えていただけると嬉しいです。

Scaling up with reducer and context

ReducerとContextを使ったスケールアップという節です。
上記で使い方教えてくれといったものの、この節で非常に腹落ちした感じがします。
Reducerで抽出した更新ロジックとコンテキストを組み合わせることで、複雑な状態を制御できます。
Docsの例では親コンポーネントの状態をReducerで管理し、コンテキストオブジェクトにしています。
こうすることで、ツリーの深いコンポーネントからでもStateを更新するためのアクションをディスパッチできるというメリットがあります。

まとめ

既に使っているHooksの話も多かったものの、改めてかなり参考になりました。
特にReducerとContextの使い方がかなり参考になりました。
複雑な画面の実装をすることはありましたが、ファットになるのが悩みだったんですよね...
うまく状態を管理すればかなりスマートに書けそうなので、チャレンジしてみたいところです。
この調子で今週はReact Docs Betaを引き続き読んでいきたいと思います。

GitHubで編集を提案

Discussion