【useEffect】初回にも実行されて困るなら《何をキッカケに、どう更新されるか》を見直せ

> useEffect を使って「初回以外の再レンダリング時に実行される処理」を書くにはどうすれば良いのか？

という疑問を、たまに目にします。

---

たとえば、以下のような仕様の、「商品価格を編集する画面」を作ることを考えてみましょう。

- 日本での販売価格入力
    - ページ読み込み時には初期値が入っている
- アメリカでの販売価格入力
    - ページ読み込み時には、初期値が入っている
    - 日本での販売価格入力に入力するたび、その1/100の値が自動で入力される
        - あくまで、入力の参考にするため、というイメージ
    - この欄に入力すれば、上書きできる

![画面のイメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/a87a055e-ab95-c4fe-6ad0-c2429563ad64.png)

:::note info
そもそも、こういった自動入力は「入力するたび」ではなく、「フォーカスを外したとき」にするのが定石だと思いますが、

useEffect の話がしたいので、あえてこのような仕様に設定しています。
:::


## ❌️ useEffect を使って変更検知しようとすると暴発する

《日本での販売価格 `japanPrice` が変わるときに、アメリカでの販売価格 `usaPrice` が変化するから `useEffect` を使う》…これで良いでしょうか？

答えは NO です。実際の挙動を見てみましょう。

```tsx:❌️まちがい例（一部）
export const PriceForm: FC<Props> = ({ initialValues, action }) => {
  const [japanPrice, setJapanPrice] = useState(initialValues.japanPrice);
  const [usaPrice, setUsaPrice] = useState(initialValues.usaPrice);

  useEffect(() => {
    setUsaPrice(Math.floor(japanPrice / 100));
  }, [japanPrice]);
```

![useEffect暴発.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/b7c345ed-1c45-c547-4d9c-d3877c198d64.gif)

**期待する動作**: (2,024円, 17ドル) という価格なので、その価格がフォームの初期値としてそのままセットされほしい

**実際の動作**: フォームの初期値がセットされた直後に、アメリカ販売価格が~~17ドル~~ → 20ドルのように更新されてしまう！（※上の動画では、わざと動作を遅くしています）

そんなはずじゃなかったのに～😭


## ❌️ useEffect には「初回」も「二回目以降」もない

そこであなたはこう考えたかも知れません。

> マウント時には実行されへんかったらエエんや！
>
> そういうフックもあるやろ！

残念ながら、React には **「初回のレンダリング（つまりマウント）時は実行せず、再レンダリング時にのみ実行される」というフックが提供されていません。**

- React のクラスコンポーネント
    - `componentDidUpdate`
- Vue
    - `watch()` (非 immediate) 

は持っている機能なのに、関数コンポーネントが提供してくれないのはなぜでしょうか？

公式ドキュメントを見れば、その理由がわかります。

> エフェクトの開始/終了という 1 サイクルのみにフォーカスしてください。コンポーネントがマウント中なのか、更新中なのか、はたまたアンマウント中なのかは問題ではありません。
>
> *https://ja.react.dev/learn/lifecycle-of-reactive-effects#thinking-from-the-effects-perspective  
> リアクティブなエフェクトのライフサイクル | React 公式ドキュメント日本語版*

とあるように、エフェクトには「開始→終了」のサイクルの繰り返ししかなく、それが「初回」なのか「2回目以降なのか」も〈あえて〉分からないように設計されているからです。

---

では、ref を使って「2回目以降か」を判定すれば動くでしょうか？これも NO です。

StrictMode を使用している場合、**開発環境では不必要なエフェクトを 1 回多く実行して、本番環境ではそうしません。** 

React は StrictMode をこのように使って、「 1 サイクルのみにフォーカスする」という教義に従わない、《いま何回目か》に依存してしまっているコードを、積極的に炙り出すのです。

（**Activity** と呼ばれる、将来 React に搭載される機能と関係するようです。）

```tsx:❌️StrictModeで動かない
  const isFirstRef = useRef(true);

  useEffect(() => {
    if(isFirstRef.current) {
      isFirstRef.current = false;
      return;
    }
    setUsaPrice(Math.floor(japanPrice / 100));
  }, [japanPrice]);
```

---

> じゃあ何すか
>
> React を書くエンジニアは「謎の教義」に苦しめられるって事すか

いいえ、解決方法はあります。しかも、useEffect よりもきっと明確になります。

## ✅️ イベントハンドラを、きちんと書いてあげよう

`useEffect` で `japanPrice` という **ステートの変化を監視するのではなく**、

仕様をそのまんま素直に解釈して、「日本での販売価格入力の input」 の onChange **イベントハンドラの中で、`setUsaPrice(japanPrice / 100)` するだけ** で良いのです。

`PriceForm` コンポーネントが保持している `japanPrice` と `usaPrice` というステートが、**《何をキッカケに、どう更新されるか》** が明確になったと思います。

```tsx
"use client";

import { type ChangeEventHandler, useState, type FC } from "react";
import styles from "./price-form.module.css";

type Props = {
  action: (formData: FormData) => Promise<void>,
  initialValues: {
    japanPrice: number,
    usaPrice: number,
  },
};

export const PriceForm: FC<Props> = ({ initialValues, action }) => {
  const [japanPrice, setJapanPrice] = useState(initialValues.japanPrice);
  const [usaPrice, setUsaPrice] = useState(initialValues.usaPrice);

  const handleChangeJapanPrice: ChangeEventHandler<HTMLInputElement> = (e) => {
    const japanPrice = Number.parseInt(e.target.value);
    setJapanPrice(japanPrice);
    setUsaPrice(Math.floor(japanPrise / 100));
  };

  const handleChangeUsaPrice: ChangeEventHandler<HTMLInputElement> = (e) => {
    setUsaPrice(Number.parseInt(e.target.value));
  };

  return (
    <form className={styles.root} action={action}>
      <div>
        <label>
          日本での販売価格（円）
          <input
            type="number"
            name="japanPrice"
            value={japanPrice}
            onChange={handleChangeJapanPrice}
          />
        </label>
      </div>
      <div>
        <label>
          アメリカでの販売価格（ドル）
          <input
            type="number"
            name="usaPrice"
            value={usaPrice}
            onChange={handleChangeUsaPrice}
          />
        </label>
      </div>
      <div>
        <button type="submit">更新する</button>
      </div>
    </form>
  );
};
```

このようなパターンについては、公式ドキュメントに記載されているので、そちらも読んでみることをオススメします。

https://ja.react.dev/learn/you-might-not-need-an-effect#chains-of-computations

ちなみに、ステートを更新する部分で `const japanPrice = ...; setJapanPrice(japanPrice)` のように、二度手間のような書き方になっているのは、React のステートの仕様に対応するためです。

```tsx
const handleChangeJapanPrice: ChangeEventHandler<HTMLInputElement> = (e) => {
  // ❌️これで動きそうだけど、古い japanPrice の値に基づいて usaPrice を計算してしまうのでダメ
  setJapanPrice(Number.parseInt(e.target.value));    
  setUsaPrice(Math.floor(japanPrise / 100));
};
```

ちょうどその話題について説明して記事があるので、こちらもどうぞ。

https://qiita.com/honey32/items/ee8d1577e68b0d58678d


## まとめ

今回のような複雑なインタラクションを、無理に useEffect で実装するのは、ピタゴラ装置を組み立てるようなものです。

- ロジックが複雑すぎて、「ただし、〇〇のときは実行されたくない」が出てくるな…
- useEffect って、セットアップ→クリーンアップの流れのはずだけど、そうじゃないな…

という場合には、いちど立ち止まって、

「コンポーネントの状態を、何をキッカケに、どう遷移させるのか」をじっくり考えてみると良いかもしれません。


> **ユーザイベントの処理にエフェクトは必要ありません。**（中略）
> 
> 購入ボタンのクリックイベントハンドラでは、何が起こったかが正確にわかります。
> 
> エフェクトが実行される時点では、ユーザが _何をした_ のか（例えば、どのボタンがクリックされたのか）はもうわかりません。
>
> したがって、通常は対応するイベントハンドラでユーザイベントを処理するべきです。
> 
> *https://ja.react.dev/learn/you-might-not-need-an-effect  
> そのエフェクトは不要かも | React 公式ドキュメント日本語版*  
> 段落区切りの追加は筆者
>

![(japanPrice, usaPrice) の直積をステートとする状態機械の図のようなもの](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/69971ce1-168b-d64b-6b3c-f0e1fcb8ca51.png)

この状態遷移図のようなものでは、

-  赤い矢印 `#DC6363`
    -  「日本での販売価格」への《入力時》の状態遷移
- 青い矢印 `#5D6BED`
    - 「アメリカでの販売価格」への《入力時》の状態遷移

でそれぞれ表しています。

## つづき: やっぱりエフェクトが必要な場合

> とは言っても、今回は《ステートと外部の同期》という本来の用途に当てはまるケースだから、イベントハンドラには移せないよ

という場合もあるでしょう。

そのような場合については別の記事で解説しました。

https://qiita.com/honey32/items/f8f12afa1bd69e714e40


## おまけ: 参考になる記事紹介

React 公式ドキュメントの記事です。「何をきっかけに、どう更新するか」を整理するための概念である「ステートマシン」のアイデアを使って、インタラクションを記述するプロセスを、やさしく噛み砕いて説明してくれています。 


https://ja.react.dev/learn/reacting-to-input-with-state

---

「useEffect は、本来の目的にしたがったとき、どのような動きをするのか」説明した私の記事です。動画もあるので分かりやすいと思います。

https://zenn.dev/yumemi_inc/articles/react-effect-simply-explained