24
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Reactで「useEffectを使わなくてよいパターン」をReact公式より整理する

24
Posted at

前回の記事では、useEffect の正しい使い方について整理しました。
今回は反対に、「そもそもuseEffectを使わなくてよいパターン」 を整理します。

React公式ドキュメントでは、こうした「useEffectを使わなくてよいパターン」が明確に整理されています。
本記事では、その内容をもとに、useEffect を書く前に立ち止まるための視点を整理します。

今回の内容と合わせることで、使うべき場面使わなくてよい場面 の両方を把握できるようになります。

2. useEffectを使用しないパターン

2-1. stateから計算できる値を、useEffectで別のstateにしない

最もやりがちなパターンです。
例えば、firstNamelastName の 2 つの state 変数を持つコンポーネントがあるとします。
これらを連結して fullName を計算したいとき、以下のように書きたくなることがあります。

const [firstName, setFirstName] = useState("Taylor");
const [lastName, setLastName] = useState("Swift");
const [fullName, setFullName] = useState("");

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

一見問題なさそうに見えますが、fullNamefirstNamelastName からそのまま求められる値です。
わざわざ state として持つ必要はなく、以下のように直接計算すれば十分です。

const fullName = firstName + " " + lastName;

なぜuseEffectが不要なのか

useEffect の中で setState を行うと、次のような流れになります。

  1. まず firstName または lastName の変更でレンダリングされる
  2. その後 useEffect が実行される
  3. setFullName によって再度レンダリングされる

本来その場で計算できる値のために、余分なレンダリングが発生してしまいます。

このパターンで意識すること

既存の state や props から計算できる値は、新たな state として持たず、その場で計算する。

2-2. ユーザー操作に応じた処理をuseEffectに書かない

次に見直したいのは、ユーザー操作をきっかけとした処理 です。
例えば、ボタン押下時に API を実行したい場合に、フラグを state で管理し、
useEffect でその変化を監視して処理を書くことがあります。

const [isBuying, setIsBuying] = useState(false);

useEffect(() => {
  if (isBuying) {
    buyProduct();
    showNotification("購入しました");
  }
}, [isBuying]);

この場合は useEffect を使うよりも、ボタン押下時のイベントハンドラに直接書いた方が自然です。

const handleBuy = async () => {
  await buyProduct();
  showNotification("購入しました");
};

<button onClick={handleBuy}>購入</button>

なぜuseEffectが不要なのか

このケースで行いたいことは、画面が表示されたから実行する処理 ではなく、
ユーザーがボタンを押したから実行する処理 です。

イベントハンドラに書くことで、

  • 何がきっかけで処理されるのかが明確になる
  • stateや依存配列で遠回しに制御しなくてよい
  • 意図しない再実行を防ぎやすい

というメリットがあります。

このパターンで意識すること

ユーザー操作をきっかけとした処理は、まずイベントハンドラに書けないかを考える。

2-3. 表示用データの加工をuseEffectで行わない

画面表示用にデータを加工するためだけに useEffect を使うパターンもよく見かけます。
例えば、受け取った配列データを画面用に整形するために、以下のように書くことがあります。

const [processedItems, setProcessedItems] = useState([]);

useEffect(() => {
  setProcessedItems(
    rawItems.map(item => ({
      id: item.id,
      label: item.name.toUpperCase(),
    }))
  );
}, [rawItems]);

この場合も processedItemsrawItems から計算できる値であり、
useEffect で別 state へ詰め直す必要はありません。

const processedItems = rawItems.map(item => ({
  id: item.id,
  label: item.name.toUpperCase(),
}));

なぜuseEffectが不要なのか

2-1 と同様に、useEffect 内で setState を行うことで余分なレンダリングが発生します。

  1. まず rawItems の変更でレンダリングされる
  2. その後 useEffect が実行される
  3. setProcessedItems によって再度レンダリングされる

rawItems から計算できる値であれば、レンダリング中にそのまま計算する方が効率的です。

重い処理の場合はどうするか

この計算が重く、毎回実行したくない場合は useMemo を使います。

const processedItems = useMemo(() => {
  return rawItems.map(item => ({
    id: item.id,
    label: item.name.toUpperCase(),
  }));
}, [rawItems]);

このパターンで意識すること

表示のための加工は副作用ではなく、計算として扱う。

2-4. state更新のuseEffectを数珠繋ぎにしない

ある state が変わったら別の state を更新し、その変化でさらに別の state を更新する、
という形で useEffect を連鎖させてしまうパターンがあります。
例えば、以下のような実装をしたとします。

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);
  const [isGameOver, setIsGameOver] = useState(false);

  // 🔴 useEffect の数珠繋ぎ
  useEffect(() => {
    if (card !== null && card.gold) {
      setGoldCardCount(c => c + 1);
    }
  }, [card]);

  useEffect(() => {
    if (goldCardCount > 3) {
      setRound(r => r + 1);
      setGoldCardCount(0);
    }
  }, [goldCardCount]);

  useEffect(() => {
    if (round > 5) {
      setIsGameOver(true);
    }
  }, [round]);

  useEffect(() => {
    alert("Good game!");
  }, [isGameOver]);

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error("Game already ended.");
    } else {
      setCard(nextCard);
    }
  }
  // ...
}

なぜuseEffectが不要なのか

この実装には 2 つの問題があります。

1 つ目は 効率の問題 です。
連鎖内の各 setState ごとにレンダリングが走るため、最悪の場合、
setCard → レンダー → setGoldCardCount → レンダー → setRound → レンダー → setIsGameOver → レンダー
と、不要なレンダリングが何度も発生します。

2 つ目は 拡張性の問題 です。
例えば、ゲームの手順を遡る機能を追加した場合、過去の state に戻すだけで
useEffect の連鎖が再び発火してしまい、意図しない動作を引き起こします。

どう改善するか

レンダー中に計算できるものはそこで行い、残りはイベントハンドラの中で処理します。

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // ✅ レンダー中に計算する
  const isGameOver = round > 5;

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error("Game already ended.");
    }

    // ✅ イベントハンドラの中でまとめて state を更新する
    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount < 3) {
        setGoldCardCount(goldCardCount + 1);
      } else {
        setGoldCardCount(0);
        setRound(round + 1);
        if (round === 5) {
          alert("Good game!");
        }
      }
    }
  }
  // ...
}

こうすることで、isGameOver は計算で求められる値として扱い、
その他の state 更新はイベントハンドラの中で一度に行えます。

このパターンで意識すること

useEffect で state の変化を監視して別の state を更新する連鎖は、まずイベントハンドラにまとめられないかを考える。

2-5. 値のリセット・調整目的だけでuseEffectを使わない

props が変わったときに内部 state をリセットしたい、という場面があります。
例えば、プロフィールページで userId を props として受け取っており、
userId が変わるたびにコメント入力欄をリセットしたいとします。

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState("");

  // 🔴 props(userId)の変更に応じて useEffect で state をリセットしている
  useEffect(() => {
    setComment("");
  }, [userId]);
  // ...
}

これも状況によっては問題ありませんが、
コンポーネント自体を別物として扱いたい のであれば、key を使った方が意図として明確になります。

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
    />
  );
}

function Profile({ userId }) {
  // ✅ key が変わるとコンポーネントが再マウントされ、state が自然に初期化される
  const [comment, setComment] = useState("");
  // ...
}

こう書くことで、userId が変わったタイミングでコンポーネントが再マウントされ、
内部 state も自然に初期化されます。

一部の state だけを調整したい場合

すべての state をリセットするのではなく、props の変化に応じて一部の state だけ変えたいケースもあります。
例えば、items リストを props で受け取り、選択中のアイテムを state で管理しているとします。

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // 🔴 items が変わるたびに selection を useEffect でリセットしている
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}

この場合も、そもそも selection を state として持つ必要があるかを見直します。
選択中のアイテム ID だけを state で持ち、実体は計算で求める形にすれば、useEffect は不要になります。

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);

  // ✅ items と selectedId から計算で求める
  const selection = items.find(item => item.id === selectedId) ?? null;
  // ...
}

このパターンで意識すること

props変更に応じたリセットは key による再マウントを、一部の調整は計算で済まないかをまず検討する。

2-6. 親コンポーネントへの通知にuseEffectを使わない

自コンポーネントの state が変わったときに、useEffect を使って親コンポーネントに通知する、という実装を見かけることがあります。
例えば、トグルコンポーネントで内部の isOn state が変化したときに、
useEffect で親の onChange を呼び出すパターンです。

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  // 🔴 state の変化を useEffect で親に通知している
  useEffect(() => {
    onChange(isOn);
  }, [isOn, onChange]);

  function handleClick() {
    setIsOn(!isOn);
  }

  // ...
}

この場合、まず Toggle が state を更新してレンダリングされ、その後 useEffect が実行されて親の onChange が呼ばれ、親側でも再レンダリングが走ります。
これは useEffect を使わなくても、イベントハンドラの中で直接解決できます。

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  function handleClick() {
    const nextIsOn = !isOn;
    setIsOn(nextIsOn);
    onChange(nextIsOn);  // ✅ イベントの中で親にも通知する
  }

  // ...
}

こうすることで、Toggle と親コンポーネントの state 更新が同じイベント内で行われ、React のバッチ処理により 1 回のレンダリングで済みます。
さらに、そもそも isOn を自身の state として持つ必要がなければ、親から props として受け取る形にすることもできます。

// ✅ 親が完全に制御する形
function Toggle({ isOn, onChange }) {
  function handleClick() {
    onChange(!isOn);
  }

  // ...
}

なぜuseEffectが不要なのか

useEffect で state の変化を監視して親に通知すると、
自コンポーネントのレンダリング → useEffect 実行 → 親の state 更新 → 再レンダリング
という流れになり、余分なレンダリングが発生します。
イベントハンドラの中で親への通知も一緒に行えば、この無駄を避けられます。

このパターンで意識すること

state の変化を親に伝えたくなったら、まずイベントハンドラの中で一緒に更新できないかを考える。

2-7. アプリケーションの初期化にuseEffectを使わない

アプリが読み込まれるときに一度だけ実行したい処理を、トップレベルのコンポーネントの useEffect に書くことがあります。

function App() {
  // 🔴 アプリ起動時に一度だけ実行したい処理
  useEffect(() => {
    loadDataFromLocalStorage();
    checkAuthToken();
  }, []);
  // ...
}

なぜuseEffectが不要なのか

開発環境では Strict Mode によりコンポーネントが 2 回マウントされるため、この処理も 2 回実行されます。例えば、認証トークンの検証が 2 回走ることでトークンが無効になるなど、予期しない問題が起きる可能性もあります。

どう改善するか

アプリの読み込みごとに 1 回だけ実行したい処理であれば、モジュールのトップレベルで実行するか、フラグで制御します。

// ✅ モジュールのトップレベルで実行する
if (typeof window !== "undefined") {
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // ...
}

トップレベルのコードは、コンポーネントがインポートされたときに一度だけ実行されます。

このパターンで意識すること

コンポーネントのマウントごとではなく、アプリの起動ごとに実行したい処理は、useEffect の外に出す。

3. まとめ

今回は、React公式の内容をもとに、useEffect を使わなくてよいパターンを整理しました。
useEffect を書く前に、今回記載した 7 つのパターンを思い出して不要かどうか見直してください。
useEffect は、Reactの外側にある仕組みと同期するための手段です。
React公式でも "escape hatch"と位置づけられており、他に適切な方法がないときに使うものです。
今回は代表的なパターンを整理しましたが、React公式ではより詳細なケースも紹介されています。
そちらも確認いただけると、useEffect を使わないパターンをより鮮明に理解できると思います。
前回の記事と合わせて、useEffect を適切に使えるようになっていきたいです。

24
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?