Tips

コレさえ理解すればOK!ReactのuseEffectは何をしてるのか解説

  • このエントリーをはてなブックマークに追加

本日はReactにおけるuseEffectを深堀していきます。

学習教材やYoutubeなどで勉強していると、useEffectはイメージ的に中盤くらいから登場するのではないでしょうか?

propsやuseStateよりも少し取っつき辛く、使いどころも不明瞭なのと初学者向けの学習教材で深く解説されなかったりします。

しかし実務では必ず登場しますし、Reactの基本概念であるレンダリングと深く接点があるのでハマりやすいです。

「よくAPIとセットで紹介されるからuseEffectが何をしているのかが分からない」
「useEffectはコピペで使っているので自分で修正を加えるとエラーになって怖い」
「Todoアプリくらいは作れるようになったからステップアップしたい」

そんな方に向けてuseEffectの裏側を晒していきたいと思います。

また動画もあるのでお好みでどうぞ。

ReactにおけるuseEffectとレンダリングの関係

まず数字をカウントするアプリを題材にしていきます。

import React, { useState } from "react";

const CountUseEffect = () => {
  const [number, setNumber] = useState(0);
  return (
    <>
      <div>
        <button onClick={() => setNumber((num) => num + 1)}>+</button>
        <p>{number}</p>
      </div>
    </>
  );
};

export default CountUseEffect;

カウントする数字をuseStateで管理するだけの簡単な仕様ですね。

レンダリングとの関係を説明するためにconsole.logを入れておきましょう。

import React, { useState } from "react";

const CountUseEffect = () => {
  const [number, setNumber] = useState(0);
  console.log("レンダリングしました");
  return (
    <>
      <div>
        <button onClick={() => setNumber((num) => num + 1)}>+</button>
        <p>{number}</p>
      </div>
    </>
  );
};

export default CountUseEffect;

「レンダリングしました」とログが出ましたね。

ちなみに「2」となっているのは、「レンダリングしました」と2回出力されたことを表しています。

こちらエラーではなく「npx create-react-app アプリ名」でReactを構築したときに、デフォルトでstrict modeが搭載されていることが要因です。

index.jsを開いて試しに以下のように「<React.StrictMode>」をコメントアウトしてみましょう。

保存して再度ブラウザをリロードするとログが1回だけ出力されるようになったと思います。

少し話が逸れていますが、レンダリングの回数を説明するためにログが2回ずつ毎回でると紛らわしいので一時的に解除しておきたいわけなんです。

続いて「レンダリングしました」というログの回数をカウントできるように以下のように修正します。

import React, { useState } from "react";

const CountUseEffect = () => {
  const [number, setNumber] = useState(0);
  console.count("レンダリングしました");
  return (
    <>
      <div>
        <button onClick={() => setNumber((num) => num + 1)}>+</button>
        <p>{number}</p>
      </div>
    </>
  );
};

export default CountUseEffect;

もう一度ブラウザをリロードしてみると「レンダリングしました」の出力回数がログに残るようになりました。

こちらJavaScriptの関数で「console.count」というもので、ログをカウントしてくれるので覚えておくと良いでしょう。

カウントは「0」ですが初回のリロードでレンダリングしたことになるので、ログは「1」になっていますね。

Reactは①ブラウザのリロード②ステートの更新でレンダリングされます。

続いてカウントをクリックしてみましょう。

カウントは「1」に変わったのと、ログが「2」になりましたね。

ボタンをクリックしたことでステートが更新されたのでレンダリングも走っているわけです。

これらがReactやuseEffectを理解するうえで大事になってきます。

さてuseEffectを書いていきます。

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

const CountUseEffect = () => {
  const [number, setNumber] = useState(0);

  useEffect(() => {
    console.count("useEffectです");
  });

  console.count("レンダリングしました");
  return (
    <>
      <div>
        <button onClick={() => setNumber((num) => num + 1)}>+</button>
        <p>{number}</p>
      </div>
    </>
  );
};

export default CountUseEffect;

useEffectのほうにも「”useEffectしました”」というログが出るようにしています。

ブラウザを再度リロードすると以下のようになると思います。

察しの良い方なら気付かれたかもしれません、なぜか後続に書いてあったはずの「”レンダリングしました”」の方が先に出力されています。

まずuseEffectにおいて最初に知ってもらいたいは「useEffectはレンダリングの後に実行される」ということです。

さらに深堀していきます、コードにカウント以外の機能を追加します。

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

const CountUseEffect = () => {
  const [number, setNumber] = useState(0);
  const [name, setName] = useState("");

  useEffect(() => {
    console.count("useEffectです");
  });

  console.count("レンダリングしました");
  return (
    <>
      <div>
        <button onClick={() => setNumber((num) => num + 1)}>+</button>
        <p>{number}</p>
      </div>
      <div>
        <input onChange={(e) => setName(e.target.value)} />
      </div>
    </>
  );
};

export default CountUseEffect;

カウントとは別に入力欄を作り、入力されるテキストをuseStateで管理するように追加しました。

再度ブラウザをリロードして、カウントを1回だけクリックしてみましょう。

・レンダリングが2回実行されたこと
・レンダリング1回につき、useEffectが1回が後から実行されたこと

以上の2点が確認できました。

それでは入力欄に文字を入力してみます。

「aaa」と文字を3文字入力したことで、レンダリングとuseEffectが更に3回ずつ実行されましたね。

入力欄のステートが更新され、それに釣られて後追いでuseEffectも実行しています。

特に問題ないのですが、「入力中はuseEffectの中身が実行されると困る」なんてシーンがあったりします。

そこでuseEffectには第二引数に「実行タイミング」を設定することができます。

以下のようにuseEffectの部分を変更します。

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

const CountUseEffect = () => {
  const [number, setNumber] = useState(0);
  const [name, setName] = useState("");

  useEffect(() => {
    console.count("useEffectです");
  },[number]);

  console.count("レンダリングしました");
  return (
    <>
      <div>
        <button onClick={() => setNumber((num) => num + 1)}>+</button>
        <p>{number}</p>
      </div>
      <div>
        <input onChange={(e) => setName(e.target.value)} />
      </div>
    </>
  );
};

export default CountUseEffect;

それでは再度ブラウザをリロードしてカウントを1回クリックしてみましょう。

特に先ほどと結果は変わっていません。

続いて入力欄に文字を入力してみます。

同じく「aaa」と3文字入力したのですが、レンダリングだけ実行されてuseEffectは止まったままになっているのが分かります。

こちらが先ほど変更した部分です。

useEffectの第二引数を[number]としたことで、「numberが更新されたときのみ実行」という意味になっているんです。

そのためカウントのときは実行されたのですが、文字の入力のときには実行されなかったんですね。

ReactにおけるuseEffectの第二引数は少し注意が必要

さてuseEffectは第二引数に設定した内容によって、実行タイミングを制御できることを知りました。

ただ第二引数の設定方法も少しだけ注意が必要です。

新しく以下のようなコードを作成しました。

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

const CountUseEffect = () => {
  const [task, setTask] = useState("");
  const [state, setState] = useState({
    savedTask: "",
    saved: false,
  });

  const addTask = () => {
    setState((prev) => ({ ...prev, savedTask: task }));
  };

  const saveTask = () => {
    setState((prev) => ({ ...prev, saved: true }));
  };
  return (
    <>
      <div>
        <input type="text" onChange={(e) => setTask(e.target.value)} />
        <button onClick={addTask}>追加</button>
        <button onClick={saveTask}>保存</button>
        <div>{`(タスク:${state.savedTask}) (保存:${state.saved})`}</div>
      </div>
    </>
  );
};

export default CountUseEffect;

ブラウザをリロードして以下のような表示になります。

タスクを追加して保存する画面を想定しました。

入力欄に文字を入力して「追加」をクリックしてみましょう。

 

入力内容が「タスク:」に続いて表示されました。

さらに「保存」をクリックしてみましょう。

「保存」がfalseからtrueに切り替わりました。

今回のコードにもuseEffectを以下のように追加してみます。

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

const CountUseEffect = () => {
  const [task, setTask] = useState("");
  const [state, setState] = useState({
    savedTask: "",
    saved: false,
  });

  uesEffect(() => {
    console.count("useEffectです");
  }, [state]);

  const addTask = () => {
    setState((prev) => ({ ...prev, savedTask: task }));
  };

  const saveTask = () => {
    setState((prev) => ({ ...prev, saved: true }));
  };
  return (
    <>
      <div>
        <input type="text" onChange={(e) => setTask(e.target.value)} />
        <button onClick={addTask}>追加</button>
        <button onClick={saveTask}>保存</button>
        <div>{`(タスク:${state.savedTask}) (保存:${state.saved})`}</div>
      </div>
    </>
  );
};

export default CountUseEffect;

こちらもログを出力するだけにして、ブラウザのリロード以外での実行タイミングをstateつまり「タスクに変更があったとき」「保存に変更があったとき」にしました。

いきなりですが上記の書き方だと良くない点があります。

まずブラウザをリロードしてみます。

初回のレンダリングなのでuseEffectが1回実行されました。

続いてテキストを入力して「追加」をクリックしてみましょう。

useEffectの第二引数に[state]としていました。

stateの初期値はオブジェクトで「savedTask」を空文字、「saved」をfalseで格納してありましたので、useEffectは実行されました。

さらに「保存」をクリックしてみましょう。

こちらも先ほど同様にuseEffectが実行されました、ここまでは想定通りです。

では試しに「保存」をもう一度クリックしてみましょう。

useEffectがさらに追加で実行されましたね。

エラーってわけではないのですが、既に保存はしていて変更は無いはずなので少し変ですよね。

実はReactでは「保存に変更があった」と見なされています。

もっと言うとJavaScript自体の性質も絡んでいて、「プリミティブ型」「非プリミティブ型」を知る必要があるます。

プリミティブ型と非プリミティブ型について

JavaScriptでは以下のようにプリミティブ型と非プリミティブ型をすみ分けています。

■プリミティブ型
文字列、数値、真偽値(true, false)、undifined

■非プリミティブ型
オブジェクト、配列

「何が違うのか?」という話になるのですが、めちゃくちゃザックリいうと「プリミティブ型:==がtrueになる、非プリミティブ型:==がfalseになる」って感じです。

例えば「100 == 100」はtrueです、さすがに簡単ですよね。

いっぽうで「{ } == { }」はfalseになります、ちょっと意外ですよね。

上記のことがuseEffectとなぜリンクするかというと、useEffectの第二引数には「変更があったかを監視するもの」を設定しましたよね。

プリミティブ型と非プリミティブ型で「変更があったか?」の判断基準が違うんです。

プリミティブ型の数値だと「100 == 100」は「変更なし」となります。

しかし非プリミティブ型のオブジェクトだと「{ } == { }」は「変更あり」となってしまいます。

もう一度コードに戻ります。

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

const CountUseEffect = () => {
  const [task, setTask] = useState("");
  const [state, setState] = useState({
    savedTask: "",
    saved: false,
  });

  uesEffect(() => {
    console.count("useEffectです");
  }, [state]);

  const addTask = () => {
    setState((prev) => ({ ...prev, savedTask: task }));
  };

  const saveTask = () => {
    setState((prev) => ({ ...prev, saved: true }));
  };
  return (
    <>
      <div>
        <input type="text" onChange={(e) => setTask(e.target.value)} />
        <button onClick={addTask}>追加</button>
        <button onClick={saveTask}>保存</button>
        <div>{`(タスク:${state.savedTask}) (保存:${state.saved})`}</div>
      </div>
    </>
  );
};

export default CountUseEffect;

useEffectの第二引数にはオブジェクトであるstateを入れてしまっています。

オブジェクトは非プリミティブ型なので、一見すると変更無さそうに見えても「変更あり」とReact側で判断されてuseEffectの中身が実行されていたんです。

少し話が逸れましたが以下のように修正します。

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

const CountUseEffect = () => {
  const [task, setTask] = useState("");
  const [state, setState] = useState({
    savedTask: "",
    saved: false,
  });

  uesEffect(() => {
    console.count("useEffectです");
  }, [state.savedTask, state.saved]); //こちらを修正!

  const addTask = () => {
    setState((prev) => ({ ...prev, savedTask: task }));
  };

  const saveTask = () => {
    setState((prev) => ({ ...prev, saved: true }));
  };
  return (
    <>
      <div>
        <input type="text" onChange={(e) => setTask(e.target.value)} />
        <button onClick={addTask}>追加</button>
        <button onClick={saveTask}>保存</button>
        <div>{`(タスク:${state.savedTask}) (保存:${state.saved})`}</div>
      </div>
    </>
  );
};

export default CountUseEffect;

useEffectの第二引数を修正しました。

「savedTask(文字列)」「saved(真偽値)」なので、ともにプリミティブ型です。

ブラウザをリロードして確認してみます。

「保存」を何回クリックしてもuseEffectは追加されませんね。

自分もプリミティブ型と非プリミティブ型について意識せず書いていたことがあり、「なぜ実行されるんだ?」とハマったことがあったので共有です。

更新する値をそのままuseEffect内で使うと危険?

ついでに自分が経験したハマりポイントをもう一つ紹介します。

冒頭のカウントアプリを以下のように作ってみました。

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

const CountUseEffect = () => {
  const [number, setNumber] = useState(0);

  useEffect(() => {
    console.count("useEffectです");
    setInterval(() => {
      setNumber(number + 1);
    }, 1000);
  }, [number]);

  return (
    <>
      <div>{number}</div>
    </>
  );
};

export default CountUseEffect;

カウントを追加するだけなのですが、useEffectの第二引数にステート変数をそのまま入れています。

さらにuseEffect内でsetInterval関数を使って関数が一定間隔で自動実行されるようになっています。

早速ブラウザを見てみます。

何だかとんでもないことになっていますね。

「無限ループ」とも呼んだりしますので、エラーではないにしても絶対にやらないようにしましょう。

useEffectは第二引数に入れた値に「変更があったとき」に実行されますので、第二引数に変更があるnumberを入れて、そのnumberをそのまま関数の中でも使ってしまっていることが原因です。

こちら以下のように修正します。

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

const CountUseEffect = () => {
  const [number, setNumber] = useState(0);

  useEffect(() => {
    console.count("useEffectです");
    setInterval(() => {
      setNumber((num) => num + 1);
    }, 1000);
  }, []);

  return (
    <>
      <div>{number}</div>
    </>
  );
};

export default CountUseEffect;

まず状態が変わるnumberはそのまま使わず「num」という引数に置き換えました。

さらにuseEffectの第二引数を[ ]という空配列にしていて、空配列にすると「初回リロードのみuseEffectを実行」という意味になっています。

画面上の数字はsetIntervalで1,000sごとに追加されている一方で、コンソールに出力されたログは1回のみになっていますね。

「ログは1回しか残ってないけど、画面のカウントはなぜ続くの?」と思われた方がいるかもしれません、というか自分も作りながら一瞬迷いました。

こちらReactというよりsetInterval関数だから、というのが答えです。

setIntervalは「指定時間において処理を継続する」という関数なので、useEffectだろうと無かろうと一度スタートすると継続するんですよね。

setIntervalは1度きりしか実行されてないんですが、その1回さえスタートしたら後はカウントを継続しているからんですね。

「繰り返し処理の予約を1回だけした」とでも言えるかもしれません。

ちょっと今回useEffectの中身をsetIntervalにしたのが逆にややこしく感じさせてしまったかもしれません。

useEffectの中身を数値の計算、APIの呼び出しなど、単発で終わる処理を書いていればログのように1回だけの動作になります。

初期化関数について

先ほどの無限ループの解消方法として代表的な手法に「初期化関数」というものがあります。

名前の通り、処理を初期化する関数をuseEffectのなかで用意しておきます。

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

const CountUseEffect = () => {
  const [number, setNumber] = useState(0);

  useEffect(() => {
    console.count("useEffectです");
    const interval = setInterval(() => {
      setNumber((num) => num + 1);
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  }, [number]);

  return (
    <>
      <div>{number}</div>
    </>
  );
};

export default CountUseEffect;

useEffectのなかでclearIntervalを使ってsetIntervalの内容を初期化しています。

「初期化したらカウントしなくなるのでは?」と思われたかもしれませんが、setNumberを引数を取ってnumberを更新して結果を返すように作っているので大丈夫です。

ReactにおけるuseEffectを使ったAPIの呼び出し

いろいろuseEffectについて説明してきましたが、初心者の方がuseEffectを使うタイミングとしては「APIの活用」だと思います。

特定の画面でAPIからデータを読み込んで画面を表示する、みたいなときに「APIを呼ぶ」ことをuseEffectの中で発生させます。

なぜなら画面移動がなくてもレンダリングが発生することでAPIが複数回に渡って呼び出されることを防ぐためです。

実は先ほど紹介した「初期化関数」はuseEffectを使ったAPIの呼び出しにも活用することができます。

新しく画面を作り直します。

まずApp.jsにルーティングを設定します。

ルーティングにはReact-router-domというライブラリを使っています。

「create-react-app アプリ名」で環境構築しているのであれば、「npm i react-router-dom」をターミナルに実行することでインストールされます。

import logo from "./logo.svg";
import "./App.css";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import UseEffectHome from "./pages/UseEffectHome";
import UseEffectUsers from "./pages/UseEffectUsers";

function App() {
  return (
    <>
      <BrowserRouter>
        <Routes>
          <Route path={"/"} exact element={<UseEffectHome />} />
          <Route path={"/users"} element={<UseEffectUsers />} />
        </Routes>
      </BrowserRouter>
    </>
  );
}

export default App;

続いてホームパスの画面をUseEffectHome.jsというファイル名で用意します。

UseEffectHome.jsは別の画面に遷移するためのリンクがあるだけのものにします。

import React from "react";
import { Link } from "react-router-dom";
import UseEffectUsers from "./UseEffectUsers";

const UseEffectHome = () => {
  return (
    <div>
      <Link to="/users" element={<UseEffectUsers />}>
        ユーザー画面
      </Link>
    </div>
  );
};

export default UseEffectHome;

続いて遷移先の画面をUseEffectUsers.jsというファイル名で用意します。

こちらではAPIの呼び出しを行っています。

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

const UseEffectUsers = () => {
  const [members, setMembers] = useState([]);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users")
      .then((res) => res.json())
      .then((data) => {
        setMembers(data);
        console.log(data);
      });
  }, []);

  return (
    <>
      <h1>APIの画面</h1>
    </>
  );
};

export default UseEffectUsers;

まずホームパス(http://localhost:3000/)でブラウザをリロードするとUseEffectHome.jsの内容が表示されます。

こちらのリンクをクリックしてみましょう。

そうすると遷移先(http://localhost/users)でUseEffectUsers.jsの内容が表示されます。

コンソールにAPIのレスポンスが出力されているかと思います。

特にエラーは起きていないのですが、このままだと1点困ったことがあります。

「間違えて画面遷移して、すぐにブラウザバックする」なんてケースです。

ユーザーは私たち開発者が想定していない行動を取ったりするものです。

「間違えて指が当たってリンクをクリックしたけど、直ちにブラウザバックした」という際には、本来はAPIは呼び出し不要です。

しかし現状だと「直ちにブラウザバックした」としても以下のようにAPIは呼び出されます。

ちなみに「直ちにブラウザバック」を実験するのは結構ムズイ作業だったりするので、もし試されたい方はChromeの検証ツールで「ネットワーク」タブを開いて「低速3G」にすることをおススメします。

低速回線を疑似的に体験できるツールです。

「低速3G」で画面遷移を行うとスローみたいな動きなるので、「画面が切り替わっている最中にブラウザバック」を体験しやすくなります。

上記の動画のように直ちにブラウザバックしたとしてもAPIの呼び出しは止められませんでした。

正直言ってユーザー目線からすると何も困らないんですが、開発者目線としては「無駄なコールはコストに関わる」ため避けたい事情があるんですよね。

それではUseEffectUsers.jsのuseEffectを初期化関数を使って修正してみたいと思います。

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

const UseEffectUsers = () => {
  const [members, setMembers] = useState([]);

  useEffect(() => {
    let back = false;
    fetch("https://jsonplaceholder.typicode.com/users")
      .then((res) => res.json())
      .then((data) => {
        if (!back) {
          setMembers(data);
          console.log(data);
        }
      });

    return () => {
      back = true;
    };
  }, [members]);

  return (
    <>
      <h1>APIの画面</h1>
    </>
  );
};

export default UseEffectUsers;

一連の動作を再度やってみましょう。

画面遷移した状態であれば予定通りにAPIを呼び出しています。

続いて画面遷移して直ちにブラウザバックしてみます。

コンソールには何も出力されていませんね。

ちなみに初期化関数の内容としては、backという変数を用意しておきfalseのときにAPIを実行するという形です。

初期化関数のやり方は状況によって変わるので正解はないのですが、代表的な2パターンを紹介します。

シーンを変えてみたいと思いますので、UseEffectUsers.jsを以下のように変更します。

import React, { useEffect, useState } from "react";
import { Link, useLocation } from "react-router-dom";

const UseEffectUsers = () => {
  const [members, setMembers] = useState([]);
  const id = useLocation().pathname.split("/")[2];

  useEffect(() => {
    if (id !== undefined) {
      fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
        .then((res) => res.json())
        .then((data) => {
          setMembers(data);
        });
    }
  }, [id]);

  return (
    <>
      <h1>APIの画面</h1>
      <p>名前:{members.name}</p>
      <p>メールアドレス:{members.email}</p>
      <div>
        <Link to="/users/1">ユーザー1の詳細</Link>
      </div>
      <div>
        <Link to="/users/2">ユーザー2の詳細</Link>
      </div>
      <div>
        <Link to="/users/3">ユーザー3の詳細</Link>
      </div>
    </>
  );
};

export default UseEffectUsers;

個別のユーザー情報をAPIを使って取得するようなシーンを想定しました。

ルーティングも変更が必要なのでApp.jsを下記のようにします。

import logo from "./logo.svg";
import "./App.css";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import UseEffectHome from "./pages/UseEffectHome";
import UseEffectUsers from "./pages/UseEffectUsers";

function App() {
  return (
    <>
      <BrowserRouter>
        <Routes>
          <Route path={"/"} exact element={<UseEffectHome />} />
          <Route path={"/users"} element={<UseEffectUsers />} />
          <Route path={"/users/:id"} element={<UseEffectUsers />} />
        </Routes>
      </BrowserRouter>
    </>
  );
}

export default App;

まずはホームパス(http://localhost:3000/)に戻ってリロードしておきます。

特に何も変更ないはずです。

リンクをクリックしてみます。

先ほどと違って3パターンのユーザーへのリンクが用意されていますね。

順番にクリックして確認しておきます。

「ユーザー1の詳細」をクリックすると名前とメールアドレスが表示されました。

「ユーザー2の詳細」をクリックすると名前とメールアドレスが変わりました。

「ユーザー3の詳細」をクリックすると名前とメールアドレスが変わりました。

ユーザーそれぞれの名前とメールアドレスがAPIを通じて動的に変わるようになっていますね。

こちらも以下の動画のように「間違えてクリックした」ことを想定して実験してみましょう。

先ほど紹介した低速3Gで試す方が良いかと思います。

お分かりでしょうか?

クリックしたことに間違えて違う場所をクリックし直しても、1個前のクリックの結果が一瞬は表示されますね。

こちらもユーザー目線としては大きなクレームに発展することはないでしょうけど、開発者目線としては少し困ります。

初期化の別パターン①

まずJavaScriptでは標準で初期化関数が用意されています。

UseEffectUsers.jsを以下のように修正してみます。

import React, { useEffect, useState } from "react";
import { Link, useLocation } from "react-router-dom";

const UseEffectUsers = () => {
  const [members, setMembers] = useState([]);
  const id = useLocation().pathname.split("/")[2];

  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;
    if (id !== undefined) {
      fetch(`https://jsonplaceholder.typicode.com/users/${id}`, { signal })
        .then((res) => res.json())
        .then((data) => {
          setMembers(data);
        })
        .catch((err) => {
          if (err.name === "AbortError") {
            console.log("キャンセルしました");
          }
        });
    }

    return () => {
      controller.abort();
    };
  }, [id]);

  return (
    <>
      <h1>APIの画面</h1>
      <p>名前:{members.name}</p>
      <p>メールアドレス:{members.email}</p>
      <div>
        <Link to="/users/1">ユーザー1の詳細</Link>
      </div>
      <div>
        <Link to="/users/2">ユーザー2の詳細</Link>
      </div>
      <div>
        <Link to="/users/3">ユーザー3の詳細</Link>
      </div>
    </>
  );
};

export default UseEffectUsers;

AbortControllerというクラスがJavaScriptには用意されていて、詳細は割愛しますが専用の関数を使うことで初期化関数を作ることができます。

実行結果を見てみましょう。

fetch関数にcatchをつなげてクリックをキャンセルしたときの動作をコンソールに出力しています。

さらにクリックして直ちに別のリンクをクリックし直しても、1個前の表示内容は画面に出ていませんよね。

初期化の別パターン②

最後にAPIの処理では一番有名なaxiosを使った処理も紹介します。

恐らくこちらのパターンが一番有名で、初心者も安心して実装できるかと思います。

axiosはライブラリになるので「npm i axios」でインストールしておきます。

UseEffectUsers.jsを変更します。

import axios from "axios";
import React, { useEffect, useState } from "react";
import { Link, useLocation } from "react-router-dom";

const UseEffectUsers = () => {
  const [members, setMembers] = useState([]);
  const id = useLocation().pathname.split("/")[2];

  useEffect(() => {
    const cancelToken = axios.CancelToken.source();

    if (id !== undefined) {
      axios
        .get(`https://jsonplaceholder.typicode.com/users/${id}`, {
          cancelToken: cancelToken.token,
        })
        .then((res) => {
          setMembers(res.data);
        })
        .catch((err) => {
          if (axios.isCancel(err)) {
            console.log("キャンセルしました");
          }
        });
    }

    return () => {
      cancelToken.cancel();
    };
  }, [id]);

  return (
    <>
      <h1>APIの画面</h1>
      <p>名前:{members.name}</p>
      <p>メールアドレス:{members.email}</p>
      <div>
        <Link to="/users/1">ユーザー1の詳細</Link>
      </div>
      <div>
        <Link to="/users/2">ユーザー2の詳細</Link>
      </div>
      <div>
        <Link to="/users/3">ユーザー3の詳細</Link>
      </div>
    </>
  );
};

export default UseEffectUsers;

やっていることは同じなので実行結果も同じになりますね。

AbortControllerとの違いは文法です。

・クラスのインスタンス化
・jsonオブジェクトの処理

が省略できるのでaxiosの方がコードの可読性は向上すると思います。

基本的な考え方さえ理解していればaxiosのような便利なライブラリを活用することをおススメします。

いかがでしたでしょうか?

少しはuseEffectについてのイメージが具体化されたかと思います。

使いどころは多いのですが、しっかり理解するには時間がかかる部分なので最初から全部を理解する必要はありません。

一歩ずつステップアップしていけば何も怖くないので、どんどん手を動かしていきましょう。

プログラミングにおける副作用を解決するためのuseEffect

そもそもなぜuseEffectのようなフックを利用する必要があるのでしょうか?

これにはプログラミングの副作用が関係していて、useEffectは副作用をコントロールする役割も持っています。

ちなみに初心者の方で副作用という言葉を習っている方はどれくらいいるでしょうか?

スクールや書籍ではあまり副作用についての説明はされていないようですが、実務に入ると知っておかないといけなくなるので一緒に説明します。

まず副作用がないとされるコードは以下のようなものです。

const calPrice = (price, tax) => {
  return Math.floor(price * (1 + tax / 100));
};
console.log(calPrice(100, 10));

こちらはECサイトのようなイメージで税込価格の計算をするものです。

引数に商品単価と税率を入れることでcalPriceの戻り値は100*10%=110円のように税込価格を出します。

基本的に引数と関数内の処理に一貫性があって、同じ引数を渡せば戻り値も同じ値で返ってきます。

一方で副作用が考えられるコードは以下のようなものです。

const todayFn = () => {
  return new Date();
};

こちらは実行したタイミングの日付を出すものです。

処理内容としては簡単な仕組みですが、JavaScriptのAPIを通じて当日の日付を計算して返してもらいますので実行するタイミングや国によって戻り値がバラバラです。

先ほどの税込計算のように「〇〇をしたら▲▲という値になる」という一貫性を示すことが難しいですよね。

上記のコード以外に、外部APIにアクセスしたりデータベースに接続する、ユーザーのローカルストレージの値を使用する、などが副作用が起きるコードとして挙げられます。

「それを言い出したら大体のコードが副作用をもってるじゃん」と思われた方がいるかもしれません。

まさにその通りで初心者の方でも遭遇するわけです。

そのため実務に入ると副作用を出来るだけコントロールすることもエンジニアとして求められます。

以下のコードがあります。

const data = [
  {
    id: "0001",
    item: "aaa",
    price: 100,
  },
];

const addItem = () => {
  data.push({
    id: "0002",
    item: "bbb",
    price: 200,
  });
  console.log(data);
  return data;
};
addItem();

ECサイトをイメージして、dataという配列に商品データがオブジェクトで格納されている状況を作っています。

関数addItemはdataに対して新しい商品を追加して返すものです。

こちらも特に問題ないのないのですが同じ処理を以下のようなコードで紹介する書籍、ブログ記事があります。

const data = [
  {
    id: "0001",
    item: "aaa",
    price: 100,
  },
];
// ここを変更
const addItem = () => {
  const newData = [...data, { id: "0002", item: "bbb", price: 200 }];
  console.log(newData);
  return newData;
};
addItem();

スプレッド構文を使ってdataをコピーした上で新しい商品を追加しています。

このような書き方をすることで元のdataに直接触らなくて良いというメリットがあります。

試しにdataをコンソールに出してみてみます。

const data = [
  {
    id: "0001",
    item: "aaa",
    price: 100,
  },
];

const addItem = () => {
  const newData = [...data, { id: "0002", item: "bbb", price: 200 }];
  console.log(newData);
  return newData;
};
addItem();
// ここを追加
console.log(data);

一方で1個前の書き方でも配列dataをコンソールで確認しておきましょう。

const data = [
  {
    id: "0001",
    item: "aaa",
    price: 100,
  },
];
// ここを変更
const addItem = () => {
  data.push({
    id: "0002",
    item: "bbb",
    price: 200,
  });
  console.log(data);
  return data;
};
addItem();
console.log(data);

同じような処理に見えて違う結果になっていることがわかりますね。

実際のところ2つの書き方のどちらを採用するかはケースバイケースなのですが、「同じデータを複数人で操作する」ようなケースでは注意することがあるわけです。

ここまで来て「何となくわかったけど副作用はエラーではないの?」という疑問を持たれた方がいるかもしれません。

プログラミングの世界では副作用とエラーは違う意味として紹介されます。

エラーというのは多くの場合、プログラムが途中で停止することにつながります。

そもそもエラーの原因としてはコードの不備やネット接続の遮断、ユーザーの不正操作があるからです。

副作用ではプログラムが途中で停止しないことが多いです。

これまでに何個か紹介したコード例ではエラーは起きていませんでした。

初心者の方からすると難しく感じられるかもしれませんが、「ユーザーに迷惑が掛からないけどエンジニアからすると想定外の動作が起きた」という具合です。

なかなか特定のケースに限定される事象ではないですし正解も作りにくいので、副作用に対する議論はチーム内でも議論が出ることが多いです。

そこで本記事のテーマであるReactのuseEffectは「副作用をコントロールする」ことを目的に作られました。

Reactを勉強し始めた時に「useStateとuseEffectってどっちを何に使うんだっけ?」となりやすいのですが、useEffectは副作用が考えられるAPI処理に利用されることが一般的です。

最初から頭で理解するのは難しいので、ポートフォリオ開発など実際の作業で少しずつ体感していくことをお勧めします。

わからないことがあれば本記事にまた戻ってきましょう。

また今回参考にした本は以下になりますのでよければどうぞ。

今回参考にした本はこちら

  • このエントリーをはてなブックマークに追加