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についてのイメージが具体化されたかと思います。

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

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

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

コメントを残す

*