Engineering from Scratch

エンジニア目指してます

2022/07/02

You Might Not Need an Effect

props や state の変化によりコンポーネントを更新したいときに,useEffect を使うべきではない。 以下の二つの場合,useEffect を使うべきではない。

  • レンダリングによりデータを変換するとき。
    • 例:あるリストを表示前にフィルタリングしたい時。
    • リストが変更されたときに,Effect により state を更新したくなるだろうが,これは非効率。
      • コンポーネントを更新しようとした時,画面上に何を表示すべきか計算するために,React はまず component の関数を呼び出す。それからこれらの変更を DOM に反映し,画面を更新する。そして,Effect を実行する。もし Effect 内で,state の更新が走ればコンポーネントの更新が最初から始まる。
      • 上記を避けるために,コンポーネントのトップレベルで全てのデータの変換を行えば良い。この処理は props や state が変化したときにいつでも再実行される。
  • ユーザーのイベントを処理する時。

外部のシステムと同期するときに Effect が必要となる。

  • React の state と jQuery widget との同期
  • データの取得
  • 検索結果

Updating state based on props or state

state を変換して作成される値を,state で管理して,useEffect で更新するべきではない。

悪い例

import { useEffect, useState } from "react";

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

  const [fullName, setFullName] = useState("");

  useEffect(() => {
    setFullName(firstName + " " + lastName);
  }, [firstName, lastName]);

  return (
    <>
      <input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
      <input value={lastName} onChange={(e) => setLastName(e.target.value)} />
      <div>{fullName}</div>
    </>
  );
};

export default App;

firstNamelastNameをつなげたfullNameを state として管理し,両者が変更されるたびに,fullNameを更新している。

また,firstNameもしくはlastNameが変更された時には,fullNameが元の古い値のままレンダリングされ,そこからfullNameが更新され,再レンダリングが走るという点で非効率的。

良い例

import { useState } from "react";

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

  const fullName = firstName + " " + lastName;

  return (
    <>
      <input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
      <input value={lastName} onChange={(e) => setLastName(e.target.value)} />
      <div>{fullName}</div>
    </>
  );
};

export default App;

fullNameをトップレベルで定数で定義している。firstNamelastNameの state が変化する度に,再レンダリングが起こりfullNameが再定義される。

Caching expensive calculations

先ほどの例で,emailも state で管理する時を考える。

悪い例

import { useState } from "react";

const App = () => {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [email, setEmail] = useState("");

  const fullName = firstName + " " + lastName;

  return (
    <>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
      <input value={lastName} onChange={(e) => setLastName(e.target.value)} />
      <div>{fullName}</div>
    </>
  );
};

export default App;

この時,emailstate が変更された時,fullNameの値は変更されていないにも関わらず,もう一度fullNameの計算処理が走ってしまい,非効率的。

良い例

import { useMemo, useState } from "react";

const App = () => {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [email, setEmail] = useState("");

  const fullName = useMemo(() => {
    console.log("fullName rendered!");
    return firstName + " " + lastName;
  }, [firstName, lastName]);

  return (
    <>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
      <input value={lastName} onChange={(e) => setLastName(e.target.value)} />
      <div>{fullName}</div>
    </>
  );
};

export default App;

fullNameを useMemo によりメモ化することで,firstNamelastNameが変更された時のみfullNameが計算され,emailstate が変更されてもfullNameは以前の値がそのまま使われる。

参考

beta.reactjs.org