🐕

【React】 useEffectの中で前回のstateの値を取得したい!

2023/06/02に公開2

【React】 useEffectの中で前回のstateの値を取得したい!

フロントエンドの開発していると稀に起こりうる可能性があるので、自分への備忘録も兼ねて記事として残しておきます。

  • サンプルコードのみを見たい方へ
    ↓こちらからこの記事にて紹介しているサンプルコードへアクセスすることができます!
    サンプルコード(codesandbox)

useEffectの中で前回のstateの値を取得したい理由

React公式 では、以下のように説明されています。

前回の props や state が欲しくなるというケースは 2 つあります。
ひとつは、前回の props を副作用のクリーンアップに使用したいという場合です。例えば、userId プロパティに基づいてソケットを購読する副作用を書いている場合などです。
別の場面では、props や他の state の変更に基づいて state を調整したいということがあるかもしれません。これはめったに必要なものではありませんし、通常はコードに重複した冗長な state があるというサインです。しかしこれが必要な稀なパターンでは、前の state や props を state に保存してレンダー中に更新することができます。

実際に前回のstateを取得したくなったケースの紹介

実際に私がフロントエンドの開発をしていて、今回のようなuseEffectの中で前回の値を取得したくなったケースは以下のようなものでした。

  • 複数の入力項目を持つフォームの一要素である通貨のセレクトフォームにて、ある通貨(円など)から違う通貨(ドル)に変更したときに、どの通貨からどの通貨に変わったかに応じて、他のフォーム要素の値を修正する必要があるケース

useEffectの中で前回のstateの値を取得する方法

useEffectのクリーンアップフェーズに関して

useEffectの仕様として、useEffectの中にcleanup関数(returnで始まる関数)を書くことができまして、その関数がクリーンアップフェーズにて呼び出される、といった仕組みがあります。
このcleanup関数は、useEffectのsetup関数が実行される前に実行されます。
今回は、こちらのcleanup関数の中で、useEffectの中で前回のstateを取得していきます。

(詳しくは、Reactの副作用フックの利用法のページにて紹介されています。)

サンプルコードを用いた紹介

実際の挙動を紹介するために、codesandboxにて簡易なフォーム(以下サンプルコードとします)を作成しました。
内容は通貨を選択し、submitボタンを押せるフォームになります。
↓こちらはフォームのスクリーンショットです。

サンプルコードの挙動

サンプルコード内のuseEffect関数は以下のように実装されています。

  useEffect(() => {
    console.log("Submitボタンを押した時の通貨(現在の値)", submittedCurrency);
    console.log("executed normally");
    return () => {
      console.log(
        "Submitボタンを一回前に押した時の通貨(一個前の値)",
        submittedCurrency
      );
      console.log("executed return function");
    };
  }, [submittedCurrency]);

このuseEffectに関して、意図している挙動は以下になります。

  1. (前提)submittedCurrencyが変更されたら、useEffectの中身が呼ばれます。
  2. cleanup関数の中の処理が呼ばれます。(クリーンアップフェーズ)
  3. setup関数(useEffect内のcleanup関数の外の処理)が呼ばれます。

ポイントは、cleanup関数がクリーンアップフェーズとなるため、setup関数より先に実行される点です。
ですので、フォームにて通貨を円→ドルに変更してみると、コンソールでは以下のようにcleanup関数が先に呼ばれていることを確認することができます。また、このときのstateは一個前のものになります。

また、コンソールでは今回の記事の趣旨である、一つ前のstateを取得することが、cleanup関数の中でできていることが確認できます。また、このときのstateは最新のものになります。

React useEffect 公式ドキュメント ではuseEffectのcleanup関数とsetup関数が呼ばれるタイミングや条件について、このように言及されています。

React calls your setup and cleanup functions whenever it’s necessary, which may happen multiple times:
Your setup code runs when your component is added to the page (mounts).
After every re-render of your component where the dependencies have changed:
First, your cleanup code runs with the old props and state.
Then, your setup code runs with the new props and state.
Your cleanup code runs one final time after your component is removed from the page (unmounts).

その他

別トピックになりますが、React 18 のstrictモードでは、開発環境での初回のコンポーネントのレンダリング時にコンポーネントのマウント、アンマウント、再マウントが意図的に走るので、useEffectも2回コールされます。

codeboxの起動時のコンソールを確認すると、マウント、アンマウント、再マウントが実行されていることを確認することができます。

参考記事

サンプルコード

最後に、今回用いたサンプルコードの紹介になります。
(codesandboxを消してしまったとき用に、念の為ここにも実装内容を残しておきます。)

import React, { useEffect, useState } from "react";

type Currency = "JPY" | "USD" | "EUR";
const App = (): JSX.Element => {
  // 現在入力されている通貨
  const [inputCurrency, setInputCurrency] = useState<Currency>("JPY");
  // Submitボタンを押されたときの通貨
  const [submittedCurrency, setSubmittedCurrency] = useState<Currency>("JPY");

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    setSubmittedCurrency(inputCurrency);
    e.preventDefault();
  };

  // submittedCurrencyが変更されたときに呼ばれます。
  useEffect(() => {
    console.log("Submitボタンを押した時の通貨(現在の値)", submittedCurrency);
    console.log("executed normally");
    return () => {
      console.log(
        "Submitボタンを一回前に押した時の通貨(一個前の値)",
        submittedCurrency
      );
      console.log("executed return function");
    };
  }, [submittedCurrency]);

  return (
    <div className="App">
      <h1>前回のstateを取得する!</h1>
      <div>
        <form onSubmit={(e) => handleSubmit(e)}>
          <label>
            <span style={{ marginRight: 10 }}>通貨:</span>
            <select
              onChange={(e) => setInputCurrency(e.target.value as Currency)}
            >
              <option value="JPY"></option>
              <option value="USD">ドル</option>
              <option value="EUR">ユーロ</option>
            </select>
          </label>
          <input type="submit" value="Submit" />
        </form>
      </div>
    </div>
  );
};

export default App;

参照

最後に

記事を読んでいただきありがとうございます!
React Hooks、とても奥深いので少しずつ慣れていきましょう〜👍

株式会社モニクル

Discussion

fiorizenfiorizen

Updating state based on previous state from an Effect
https://react.dev/reference/react/useEffect#updating-state-based-on-previous-state-from-an-effect

公式DocではsetStateにstate更新関数を渡して前のstateを持ってくる方法が案内されていますね。この方法ならcurrencystate1種で対応できるはずです。
前の値を使って行う処理にもよりますが、cleanupを目的外の方法に使うのは直感的ではないので個人的には避けます。