Post

React Hook Object Dependency Note

React Hook Object Dependency Note

React Hook のオブジェクト依存でハマったメモ

問題

RecordDetailPage.jsx では、もともと次のように書いていました。

1
2
3
4
5
const acting = getActingLeaderFromSearchParams(searchParams);

useEffect(() => {
  RecordAPI.getRecordDetail(year, recordId, acting, false, abortController.signal);
}, [acting, recordId, year]);

一見すると問題なさそうですが、実際には「明細を見る」を押したあと、画面が何度も揺れるように再描画されました。特に npm run dev では目立ちました。

原因

acting はオブジェクトです。

中に入っている値が同じでも、render のたびに新しいオブジェクトが作られると、React はそれを「別物」として扱います。

つまり、次の 2 つは同じ内容でも同一参照ではありません。

1
{ unitSn: "192", degree: "1" } !== { unitSn: "192", degree: "1" }

React Hook の dependency array は、オブジェクトの中身ではなく reference を比較します。

そのため useEffect(..., [acting]) は不要に何度も再実行されます。

症状

よく出る症状は次のとおりです。

  • API が何度も再送される
  • loading 状態が何度も切り替わる
  • 画面がちらつく、揺れる
  • dev モードで特に目立つ

正しい書き方

まずは安定した primitive 値に分解して依存させます。

1
2
3
4
5
6
7
8
9
10
11
12
13
const acting = useMemo(() => getActingLeaderFromSearchParams(searchParams), [searchParams]);
const actingUnitSn = acting?.unitSn ?? "";
const actingDegree = acting?.degree ?? "";

const requestActing = useMemo(() => (
  actingUnitSn && actingDegree
    ? { unitSn: actingUnitSn, degree: actingDegree }
    : null
), [actingUnitSn, actingDegree]);

useEffect(() => {
  RecordAPI.getRecordDetail(year, recordId, requestActing, false, abortController.signal);
}, [year, recordId, actingUnitSn, actingDegree, requestActing]);

実務上の原則

  1. useEffect の依存にオブジェクト、配列、関数が入っている場合はまず疑う。
  2. 実際に見ているのが一部の項目だけなら、まず primitive に分解する。
  3. オブジェクト自体が必要な場合は useMemo で reference を安定させる。
  4. lint warning を消すためだけに、不安定なオブジェクトをそのまま dependency array に入れない。

ひとことで言うと

React Hook の dependency array は、オブジェクトに対して「同じ内容か」ではなく「同じ参照か」を見ています。

そのため、

  • 画面パラメータは primitive に寄せる
  • request payload を effect に渡すなら useMemo で安定化する

今回の acting はその典型例でした。

This post is licensed under CC BY 4.0 by the author.