You and 120% Cleaner React

はじめに

react.dev を参照しながら、React に関する 14 のアンチパターンとその改善案を紹介します。

1. useState の多用

問題

React フックスの useState を多用しすぎて、可読性が落ちていると言う問題です。各 state が何の値なのか、いつ変更されるのかがわかりづらくなっています。

import { useState } from "react"

const ExampleTodo1 = () => {
  const [error, setError] = useState(null)
  const [status, setStatus] = useState("")
  const [todo, setTodo] = useState("")
  const [deadline, setDeadline] = useState("")
  const [description, setDescription] = useState("")
  const [isLoading, setIsLoading] = useState(false)
  const [todoList, setTodoList] = useState("")
  const [createTodoModalIsOpen, setCreateTodModalIsOpen] = useState(false)
  // (省略)その他、数多くのstate

  return (
    <div>
      // ...略...
    <div>
  )
}

改善案

フォームの値など、関連性のある state を一つの構造化データにまとめることで見やすくなります。

import { useState } from "react";

export default function Form() {
  const [person, setPerson] = useState({
    firstName: "Barbara",
    lastName: "Hepworth",
    email: "bhepworth@sculpture.com",
  });

  function handleChange(e) {
    setPerson({
      ...person,
      [e.target.name]: e.target.value,
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          name="firstName"
          value={person.firstName}
          onChange={handleChange}
        />
      </label>
      <label>
        Last name:
        <input
          name="lastName"
          value={person.lastName}
          onChange={handleChange}
        />
      </label>
      <label>
        Email:
        <input name="email" value={person.email} onChange={handleChange} />
      </label>
      <p>
        {person.firstName} {person.lastName} ({person.email})
      </p>
    </>
  );
}

2. 巨大コンポーネント

問題

コンポーネントの分割を適切に行わず、巨大なコンポーネントを作ってしまうというアンチパターンです。巨大コンポーネントは認知コストも増え、変更容易性の天敵です。

また、作業の競合が起きやすくなったり、パフォーマンスの観点でも再レンダリングされる描画範囲が増えるため好ましくありません。

改善案

単一責任の原則

コンポーネントを分割する際にも、新しい関数やオブジェクトを作るべきかどうかを判断する時と同じようなテクニックが使えます。そのようなテクニックの 1 つが単一責任の原則です。つまり、コンポーネントは理想的には 1 つのことだけを行うべきです。もしコンポーネントが大きく膨らみそうなら、より小さなサブコンポーネントに分解する必要があります。

例えば、ToDo アプリのケースでは、表示、編集、作成等にコンポーネントを分割します。

3. 膨大なロジック

問題

巨大コンポーネントの類似の問題で、コンポーネント内部に膨大なロジックを孕んでいるアンチパターンです。

import { useState, useEffect } from "react";

const ExampleTodo = () => {
  const [query, setQuery] = useState({
    isLoading: false,
    error: null,
    todoList: undefined,
  });

  const [filterStatus, setFilterStatus] = useState("DOING");
  const handleSelectFilterStatus = (e) => {
    setFilterStatus(e.target.value);
  };
  const handleSubmitStatusFilter = async (e) => {
    e.preventDefault();
    try {
      const response = await fetch(`/api/todos&${filterStatus}`, {
        method: "GET",
      });
      const todoList = await response.json();
      setQuery((prev) => ({
        ...prev,
        isLoading: false,
        todoList,
      }));
    } catch (error) {
      setQuery((prev) => ({
        ...prev,
        isLoading: false,
        error: error,
      }));
    }
  };

  useEffect(() => {
    setQuery((prev) => ({
      ...prev,
      isLoading: true,
    }));
    const fetchFn = async () => {
      try {
        const response = await fetch("/api/todos", { method: "GET" });
        const todoList = await response.json();
        setQuery((prev) => ({
          ...prev,
          isLoading: false,
          todoList,
        }));
      } catch (error) {
        setQuery((prev) => ({
          ...prev,
          isLoading: false,
          error: error,
        }));
      }
    };
    fetchFn();
  }, []);

  if (query.isLoading || !query.todoList) {
    return <p>now on loading...</p>;
  }

  return (
    <div>
      <form className="todoFilter" onSubmit={handleSubmitStatusFilter}>
        <select value={filterStatus} onChange={handleSelectFilterStatus}>
          <option value="DOING">今日やること</option>
          <option value="PARKING">いつかやること</option>
          <option value="DONE">できたこと</option>
        </select>
        <button type="submit">絞り込み</button>
      </form>
      <ul className="todoList">
        {query.todoList.map((todo) => (
          <li key={todo.id}>
            <h2 className="todoTitle">{todo.title}</h2>
            <p className="todoDescription">{todo.description}</p>
            <p className="totoDeadline">{todo.deadline}</p>
            <button className="primaryButton">
              <i className="fa-edit" />
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};

問題点は、コンポーネントが詳細なロジックについて知りすぎているところです。コンポーネントは「実行方法ではなく、実行したいことを記述している」状態にすベきです。コンポーネントは、あくまで結果しか知らなくてよく、データを取得する際に、どのように過程を経てデータを取得するのか、その詳細について知るべきではありません。

そうすることでテストも書きやすくなるというメリットもあります。

改善案

以下のようにロジックを別のファイルにカスタムフックとして定義し、コンポーネントの側で呼び出すことで改善します。

import { useState, useEffect } from "react";

export const useFetchTodos = () => {
  const [query, setQuery] = useState({
    isLoading: false,
    error: null,
    todoList: undefined,
  });

  const [filterStatus, setFilterStatus] = useState("DOING");

  useEffect(() => {
    setQuery((prev) => ({
      ...prev,
      isLoading: true,
    }));
    const fetchFn = async () => {
      try {
        const response = await fetch(`/api/todos&filter=${filterStatus}`);
        const todoList = await response.json();
        setQuery((prev) => ({
          ...prev,
          isLoading: false,
          todoList,
        }));
      } catch (error) {
        setQuery((prev) => ({
          ...prev,
          isLoading: false,
          error: error,
        }));
      }
    };
    fetchFn();
  }, [filterStatus]);

  const { error, todoList, isLoading } = query;
  return {
    error,
    todoList,
    isLoading,
    onFilter: setFilterStatus,
  };
};

4. 非同期処理に useEffect を使う

問題

useEffect は本来、React コンポーネントのルールの外側に出るためのエスケープハッチであり、useEffect 内で非同期処理を行うには、大変な実装が付き纏います。また、Race Condition 問題などエッジケースも発生し、自分で実装して対処する必要が出てきます。

例えば、インクリメンタルサーチで API に問い合わせる際、文字列がカタカナを含んでいるときに API のレスポンスが遅くなるケースがあるとします。

  1. (イベント)ユーザーが"カタカナ"を入力する
  2. (イベント)ユーザーが"漢字"を入力する
  3. (出力結果)"漢字"の検索結果が返る
  4. (出力結果)"カタカナ"の検索結果が返る

この場合、ユーザーは漢字を入力しているのでカタカナの検索結果が返ってしまいます。

改善案

TanStack Query を使用して、非同期処理で API を叩くための最適化されたライブラリを使用します。

5. state の逆流 (useEffect)

問題

子コンポーネントで取得したデータを、親コンポーネントに渡してるアンチパターンです。

import { useState, useEffect } from "react";

const TodoListParent = () => {
  const [todoListData, setTodoListData] = useState();

  return (
    <>
      <h1>{todoListData.length}件のTODO</h1>
      <TodoListChild onFetched={setTodoListData} />
    </>
  );
};

const TodoListChild = ({ onFetched }) => {
  const [todoList, setTodoList] = useState();

  useEffect(() => {
    const fetchFn = async () => {
      const response = await fetch("/api/todos");
      const todoListResponse = await response.json();
      setTodoList(todoListResponse);
      onFetched(todoListResponse);
    };
    fetchFn();
  }, []);

  return; // 省略
};

React では、データは親コンポーネントから子コンポーネントに流れます。
画面に何か問題がある場合は、コンポーネントのチェーンを上へたどっていきながら、おかしな情報がどこから来たのかを追跡できます。
そうすると、どのコンポーネントが間違った props を渡しているか、または間違った state を持っているかがわかります。
しかし、子コンポーネントが Effect 内で親コンポーネントの状態を更新すると、そのデータフローをたどるのが非常に難しくなります。

子コンポーネント内の useEffect で親の state を更新するのは、データフローの逆走にあたり、デバックを極めて困難にし、開発の大きな障壁になります。

改善案

state の更新を "Lifting Up (持ち上げること)" です。

import { useState, useEffect } from "react";

const TodoListParent = () => {
  const [todoList, setTodoList] = useState();

  useEffect(() => {
    const fetchFn = async () => {
      const response = await fetch("/api/todos");
      const todoList = await response.json();
      setTodoList(todoList);
    };
    fetchFn();
  }, []);

  return (
    <>
      <h1>{todoList.length}件のTODO</h1>
      <TodoListChild todoList={todoList} />
    </>
  );
};

const TodoListChild = ({ todoList }) => {
  return; // 省略
};

6. 無駄な useState

問題

同じ情報を state で二重管理してしまっているアンチパターンです。無駄に state を乱立させてしまうと、状態を追うのもデバッグするのも困難になりますし、可読性も下がります。

Redundant State

例えば、fullName は firstName と lastName から計算できます。

import { useState } from "react";

const RedundantStateExample = () => {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [fullName, setFullName] = useState("");

  const handleFirstNameChange = (e) => {
    setFirstName(e.target.value);
    setFullName(e.target.value + " " + lastName);
  };

  const handleLastNameChange = (e) => {
    setLastName(e.target.value);
    setFullName(firstName + " " + e.target.value);
  };

  return (
    <>
      <input value={firstName} onChange={handleFirstNameChange} />
      <input value={lastName} onChange={handleLastNameChange} />
      <p>{fullName}</p>
    </>
  );
};

Duplication in State

例えば、以下のコードでは selectedCake は id だけを保持するべきで、無駄に title など他の情報まで保持すると、ユーザーが title を変更できるようになった場合に、バグを生む危険も生じます。

import { useState } from "react";

const initialCakes = [
  { title: "ショートケーキ", id: 0 },
  { title: "フルーツケーキ", id: 1 },
  { title: "チョコレートケーキ", id: 2 },
];

const DuplicationInStateExample = () => {
  const [cakes, setCakes] = useState(initialCakes);
  const [selectedCake, setSelectedCake] = useState();

  return (
    <>
      <ul>
        {cakes.map((cake) => (
          <li key={cake.id}>
            {cake.title}
            <button onClick={() => setSelectedCake(cake)}>
              このケーキを選ぶ!
            </button>
          </li>
        ))}
      </ul>
      {selectedCake && <p>{selectedCakeId.title}</p>}
    </>
  );
};

改善案

Single Source Of Truth に従うことで、単一の state で管理します。

state ごとに、それを「所有」するコンポーネントを選択します。
この原則は、「Single Source Of Truth(信頼できる唯一の情報源)」を持つこととも呼ばれています。
これは、すべての state が単一の場所に存在することを意味するのものではありません。
ある state に対して、その情報を保持する単一のコンポーネントが存在することを意味します

7. useEffect の乱用

問題

パフォーマンスの観点で見ても、エラーの発生しやすさの観点で見ても非常に有害だからです。

不要な Effect を削除すると、コードが理解しやすくなり、実行が速くなり、エラーが発生しにくくなります。

useEffect とは、React におけるエスケープハッチ、すなわち、脱出口です。
ここでいう「脱出口」とは、React コンポーネントの中から React コンポーネントの外へ脱出し、外側の出来事と接続するための脱出口です。
例えば、React のコンポーネントが預かり知らぬところで発生した事象に反応して、DOM を操作したい場合です。コンポーネントの外の出来事をコンポーネントと同期させるために effect を使う必要があります。

const Component = () => {
  const [h, setH] = useState(0);

  useEffect(() => {
    const handler = () => {
      setH((h) => (h + 1) % 360);
    };
    window.addEventListener("pointermove", handler);
    return () => {
      window.removeEventListener("pointermove", handler);
    };
  }, []);

  return (
    <div
      style={{
        backgroundColor: `hsl(${h}, 100%, 50%)`,
        width: "100px",
        height: "100px",
      }}
    />
  );
};

改善案

計算処理の最適化に使っているパターン

「useEffect は依存配列の値に変更があったときにだけ実行されるから、useEffect を使った方がパフォーマンスが良いはずだ」として使われているパターンです。これは二回レンダリングされてしまうため、むしろパフォーマンスが悪くなります。

import { useState } from "react";

const HarmfulUseEffect = () => {
  const [options, setOptions] = useState([]);
  const [defaultSelectOption, setDefaultSelectOption] = useState();

  useEffect(() => {
    const newDefaultSelectOption = options.find(
      (option) => option.isDefault
    ).code;
    setDefaultSelectOption(newDefaultSelectOption);
  }, [options]);

  return; // 省略
};

useMemo を利用します。

import { useState, useMemo } from "react";

const EffectiveUseMemo = () => {
  const [options, setOptions] = useState([]);
  const defaultSelectOption = useMemo(
    () => options.find((option) => option.isDefault).code,
    [options]
  );

  return; // 省略
};

無駄なレンダリングしてるパターン

import { useEffect } from "react";
import { redirect } from "react-router-dom";
import { useUserInfoContext } from "./containers/useUserInfoContext";

const AdminSuperSecretPage = () => {
  const { isAdmin } = useUserInfoContext();

  useEffect(() => {
    if (!isAdmin) {
      redirect("/login");
    }
  }, [isAdmin]);

  return <p>これは本当に本当に内緒の話なんだけどさ、▲▲▲って✖️✖️✖️らしいよ</p>;
};

無駄な情報を表示したあとにリダイレクトしています。

import { useEffect } from "react";
import { redirect } from "react-router-dom";
import { useUserInfoContext } from "./containers/useUserInfoContext";

const AdminSuperSecretPage = () => {
  const { isAdmin } = useUserInfoContext();

  if (!isAdmin) {
    return redirect("/login");
  }

  return <p>これは本当に本当に内緒の話なんだけどさ、▲▲▲って✖️✖️✖️らしいよ</p>;
};

8. 不変じゃない key

問題

必要な場合に key を渡さなかったり、誤った key を渡してしまうアンチパターンです。

import { v4 as uuid } from "uuid";

const WrongKeyExample = () => {
  const cakes = [
    { title: "ショートケーキ", id: uuid() },
    { title: "フルーツケーキ", id: uuid() },
    { title: "チョコレートケーキ", id: uuid() },
  ];

  return (
    <ul>
      {cakes.map((cake) => (
        <li key={cake.id}>{cake.title}</li>
      ))}
    </ul>
  );
};

この場合、レンダリングの度に key が毎回変わるため、たとえ同じ内容であっても React はコンポーネントを再生成します。

改善案

どんなアップデートが起きたかを React が正しく認識できるように、key が変化しないことと key がユニークであることの2つの条件を満たす必要があります。

ローカルで WebAPI の crypto.randomUUID() を使用して UUID を生成したり、API からもデータベースに格納されている ID を利用したりします。

9. State の Closure

問題

「state はレンダリングのスナップショットである」という事実を無視して、アプリケーションにバグを仕込んでしまうケースがあります。
例えば、以下の例ではゲームスタートした時点で bomber が "太郎" で決定してしまっている点です。

import { useState } from "react";

const BombGame = () => {
  const [bomber, setBomber] = useState("太郎");
  const [selectedMember, setSelectedMembers] = useState("太郎");

  const onGameStart = () => {
    setTimeout(() => {
      alert(`Boom💣🔥 at ${bomber}!!!`);
    }, 30000);
  };

  return (
    <div>
      <button onClick={onGameStart}>ゲームスタート</button>
      <select
        value={selectedMember}
        onChange={(e) => setSelectedMembers(e.target.value)}
      >
        <option value="太郎">太郎</option>
        <option value="秋子">秋子</option>
        <option value="花子">花子</option>
      </select>
      <button onClick={() => setBomber(selectedMember)}>ボムを渡す</button>
      <p>今ボールを持っている人 {bomber}</p>
    </div>
  );
};

改善案

例えば、以下のコードでは ThreeUp 関数では、leftMarioCount の値は変わらないので結果として 1 つしかアップしません。

import { useState } from "react";

const LeftMarioCounter = () => {
  const [leftMarioCount, setLeftMarioCount] = useState(0);

  const onClickOneUPMushroom = () => {
    setLeftMarioCount(leftMarioCount + 1);
  };

  const onClickThreeUpMoon = () => {
    setLeftMarioCount(leftMarioCount + 1);
    setLeftMarioCount(leftMarioCount + 1);
    setLeftMarioCount(leftMarioCount + 1);
  };

  return (
    <>
      <h1>{leftMarioCount}</h1>
      <button onClick={onClickOneUPMushroom}>🍄1UPキノコ</button>
      <button onClick={onClickThreeUpMoon}>🌛3UPムーン</button>
    </>
  );
};

updater function を使用することで解決できます。

冗長な構文よりも一貫性を重視するのであれば、新しい state が更新前の state から計算されるときには常に updater を書くのは理にかなっています。

更新が以前の state に依存している場合は、できるだけ updater function を使うように意識していくのが良さそうです。

他の手段として useReducer を使用すると、updater function の冗長性を軽減できます。
useReducer とは、端的にいえば、「action から state の update を切り離す」ためのフックスです。

setSomething(something => ...)と書いていることに気づいたら、代わりに reducer を使うことを検討する良い機会です。
Reducer を使うと、コンポーネントで起こった”Action”に関する記述を、それに反応して state がどのようにアップデートされるかという部分からを切り離すことができます。

import { useReducer } from "react";

const reducer = (state, action) => {
  switch (action) {
    case "clickMushroom":
      return state + 1;
    case "clickMoon":
      return state + 3;
    default:
      throw new Error("不正なactionの値です");
  }
};

const LeftMarioCounter = () => {
  const [leftMarioCount, dispatch] = useReducer(reducer, 0);

  return (
    <>
      <h1>{leftMarioCount}</h1>
      <button onClick={() => dispatch("clickMushroom")}>🍄1UPキノコ</button>
      <button onClick={() => dispatch("clickMoon")}>🌛3UPムーン</button>
    </>
  );
};

10. useEffect の依存配列が不適切

問題

useEffect の依存配列を適当に設定してしまうアンチパターンです。例えば以下の例では、依存配列に leftSeconds が渡されていないため、正常に動きません。

import { useState, useEffect } from "react";

const initialSeconds = 60 * 3;

const ThreeMinutes = () => {
  const [leftSeconds, setLeftSeconds] = useState(initialSeconds);

  useEffect(() => {
    const id = setInterval(() => {
      setLeftSeconds(leftSeconds - 1);
    }, 1000);

    return () => clearInterval(id);
  }, []);

  return (
    <div>
      <h1>{leftSeconds}</h1>
      {leftSeconds > 0 ? (
        <span>You are fixing ramen...</span>
      ) : (
        <span>3分経過しました!</span>
      )}
    </div>
  );
};

改善案

React 用の lint である react-hooks-exhaustive-deps を設定します。

無限ループが発生してしまうケース

しかし、無限ループしまうケースで強制的に disable lint してしまうことがあります。

const [count, setCount] = useState(0);
useEffect(() => {
  setCount(count + 1);
}, [count]);

このような場合は  updater function を使うことで回避できます。

const [count, setCount] = useState(0);
useEffect(() => {
  setCount((c) => c + 1);
}, []);

または、このようなケースにあたった場合はそもそも useEffect を使わなくて良いケースの可能性があります。

const [count, setCount] = useState(1);

11. useEffect のクリーンアップ関数が書かれていない

問題

useEffect のクリーンアップ関数が書かれていないアンチパターンです。
useEffect の return の後に書かれた処理がクリーンアップ関数です。このケースでは こんぽコンポーネントがマウントされる度に Interval 関数が増殖してしまいます。

useEffect(() => {
  const id = setInterval(() => {
    setLeftSeconds((s) => s - 1);
  }, 1000);

  // ↓↓これがクリーンアップ関数!!
  return () => clearInterval(id);
}, []);

改善案

「setInterval」は「clearInterval」、「setTimeout」は「clearTimeout」
また、「addEventListener」は「removeEventListener」をクリーンアップ関数に設定します。

また、クリーンアップ関数の書き忘れ防ぐために Strict Mode を使用します。StrictMode をオンにすると、React は様々な処理を、開発環境においてのみ二回実行するようになります。そして React は冪等性がない処理に対して警告を出します。

+ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(
+ <StrictMode>
    <App />
+ </StrictMode>,
);

12. コンポーネントの中にコンポーネントを書く

問題

コンポーネントの中にコンポーネントを書いてしまうアンチパターンです。親コンポーネントがレンダリングされると、子コンポーネントも初めから作り直します。

そのため、パフォーマンス面で良くありません。

const Library = () => {
  const Book = () => {
    return (
      <section>
        <h1>本のタイトル</h1>
        <p>内容のサマリー</p>
      </section>
    );
  };

  return (
    <div>
      <Book />
    </div>
  );
};

また、参照が不明瞭になり、バグを引き起こしやすいです。「どの変数がどこを参照しているのか」のかが読み解くのが大変になり、可読性が低いです。

import { useState, useEffect } from "react";
import { Dialog } from "./ui/Dialog";

const ExampleTodo = () => {
  const todoList = [];
  const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState(false);

  const TodoList = () => {
    return (
      <ul className="todoList">
        {todoList.map((todo) => (
          <li key={todo.id}>
            <h2 className="todoTitle">{todo.title}</h2>
            <button
              className="primaryButton"
              onClick={() => setIsEditDialogOpen(true)}
            >
              <i className="fa-edit" />
            </button>
          </li>
        ))}
      </ul>
    );
  };

  const UpdateTodoDialog = () => {
    return (
      <Dialog
        open={isUpdateDialogOpen}
        onClose={setIsUpdateDialogOpen}
        header="新規TODOの追加"
      >
        <form classNme="todoForm">
          <select id="status">
            <option value="DONE">やったこと</option>
            <option value="TODO">今日やること</option>
            <option value="PARKING">いつかやること</option>
          </select>
          <div className="dialogActionButtonWrapper">
            <button className="dialogActionNegativeButton">キャンセル</button>
            <button className="dialogActionPositiveButton" type="submit">
              更新
            </button>
          </div>
        </form>
      </Dialog>
    );
  };

  return (
    <div>
      <button
        className="primaryButton"
        onClick={() => setIsUpdateDialogOpen(true)}
      >
        <i className="fa-edit" />
        新規作成
      </button>
      <TodoList />
      <UpdateTodoDialog />
    </div>
  );
};

改善案

コンポーネントをネストしないように書くようにします。props によって何に依存しているかを一瞬で判断できます。

13. エラーフィードバックがされていない

問題

ErrorBoundary を使わなかったことによって、本来はユーザーになされるべきであったフィードバックがなされない結果を招くことです。ErrorBoundary とは、レンダリングエラーをキャッチして代替の UI を表示するための仕組みのことです。

アプリケーションが明快なエラーメッセージを返さないと、ユーザーは一体何が起きたのか判断できません。

UI の一部に JavaScript エラーがあってもアプリ全体が壊れてはいけません。
React ユーザがこの問題に対応できるように、React 16 では “error boundary” という新しい概念を導入しました。
(...中略...)
我々の経験上、壊れた UI をそのまま表示しておくことは、完全に削除してしまうよりももっと悪いことです。
例えば、Messenger のような製品において壊れた UI を表示したままにしておくと、誰かが誤って別の人にメッセージを送ってしまう可能性があります。
同様に、支払いアプリで間違った金額を表示することは、何も表示しないよりも悪いことです。

React 16 から、どの error boundary でもエラーがキャッチされなかった場合に React コンポーネントツリー全体がアンマウントされるようになりました。

つまり、ErrorBoundary を使用していない場合、レンダリングエラーが起きると全体がアンマウントされるということです。

改善案

ライブラリ react-error-boundary を使うことです。

現在、ErrorBoundary を関数コンポーネントとして記述する方法はありません。
しかし、ErrorBoundary のクラスを自分で書く必要はありません。
例えば、react-error-boundary を代わりに使うことができます。[3]

  1. FallbackComponent を用意します(Fallback とは”代替”という意味で、エラーが起きたときに壊れた UI の代わりに表示する UI のこと)
export const Fallback = () => (
  <div className="error-fallback">
    <h2>
      <i class="fa fa-warning" />
      <span>予期せぬエラーが発生しました</span>
    </h2>
    <p>時間をおいて、再度お試しください</p>
  </div>
);
  1. Fallback とエラーが起きたいときにやりたいことを react-error-boundary に渡します
import { ErrorBoundary } from "react-error-boundary";
import { Fallback } from "./Fallback";

export const ErrorBoundaryWithFallback = ({ children }) => (
  <ErrorBoundary
    fallbackRender={Fallback}
    onError={(error, info) => console.error(error)}
  >
    {children}
  </ErrorBoundary>
);
  1. 出来るだけルートに近いところに ErrorBoundaryWithFallback を配置します
import { BrowserRouter } from "react-router-dom";
import { Routes } from "./Routes";
import { ErrorBoundaryWithFallback } from "./ErrorBoundaryWithFallback";

export const App = ({ children }) => (
  <ErrorBoundaryWithFallback>
    <BrowserRouter>
      <Routes />
    </BrowserRouter>
  </ErrorBoundaryWithFallback>
);

Error Boundary x エラー監視システム

「ユーザーが報告してくれないとバグに気づけない」みたいな後手後手の体制にならないように、先手を打てる仕組みを設けておくべきです。Datadog を利用している場合は以下のようになります。

import { datadogLogs } from "@datadog/browser-logs";
import { ErrorBoundary } from "react-error-boundary";
import { Fallback } from "./Fallback";

export const ErrorBoundaryWithFallback = ({ children }) => {
  const errorDetails = {
    customFields: {
      severity: "critical",
      foundBy: "root-error-boundary",
    },
  };

  return (
    <ErrorBoundary
      fallbackRender={Fallback}
      onError={(error, info) =>
        datadogLogs.logger.log(error.message, errorDetails, "error")
      }
    >
      {children}
    </ErrorBoundary>
  );
};

React Query x Error Boundary

import { QueryClient } from "@tanstack/react-query";
import { datadogLogs } from "@datadog/browser-logs";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      useErrorBoundary: true,
    },
  },
  logger: {
    log: (message: Record<string, unknown>) => {
      datadogLogs.addLoggerGlobalContext("customFields", { queryLog: message });
    },
    warn: (message: Record<string, unknown>) => {
      datadogLogs.addLoggerGlobalContext("customFields", {
        queryWarn: message,
      });
    },
    error: (error: Record<string, unknown>) => {
      datadogLogs.addLoggerGlobalContext("customFields", { queryError: error });
    },
  },
});

※ ErrorBoundary は QueryClientProvider の外側に置いておく必要があるので気をつけてください

import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "./queryClient";
import { ErrorBoundaryWithFallback } from "./ErrorBoundaryWithFallback";

export const App: React.FC = () => {
  return (
    <ErrorBoundaryWithFallback>
      <QueryClientProvider client={queryClient}>
        <main>...省略...</main>
      </QueryClientProvider>
    </ErrorBoundaryWithFallback>
  );
};

しかし、基本的にはちゃんとエラーハンドリングを行うことが重要です。

14. useRef を使わない

問題

useRef の存在を忘れているパターンです。

改善案

DOM 操作

useRef の代表的な使い方の一つで、DOM 操作のために useRef を使うパターンです。

import { useRef, useEffect } from "react";

const ExampleRef: React.FC = () => {
  const buttonRef = useRef < HTMLButtonElement > null;

  useEffect(() => {
    if (buttonRef?.current) {
      buttonRef.current.scrollIntoView();
    }
  }, []);

  return (
    <div>
      <div>
        <input type="checkbox" id="confirm" name="confirm" />
        <label htmlFor="confirm">確認しました</label>
      </div>
      <button type="submit" ref={buttonRef}>
        提出
      </button>
    </div>
  );
};

表示させない状態

コンポーネントに何らかの情報を記憶させたいが、その情報を新たなレンダリングのトリガーにしたくない場合に useRef は有効です。無駄な再レンダリングを起こさなくて済みます。

import { useState } from "react";

const StateMemorizationTrouble = () => {
  const [receptionDate, setReceptionDate] = useState(new Date());

  const handleSubmit = () => {
    const body = JSON.stringify({ receptionDate });
    fetch("/api/patient/registration", { method: "POST", body });
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="button" onClick={() => setReceptionDate(new Date())}>
        受付時刻を更新
      </button>
      <input onChange={(e) => setText(e.target.value)} />
      <button type="submit">患者登録</button>
    </form>
  );
};