NU:LOGiC Logo

【React.js】useEffectの使い方: レンダリングを最小限に抑える方法やuseLayoutEffectとの違いも解説!

【React.js】useEffectの使い方: レンダリングを最小限に抑える方法やuseLayoutEffectとの違いも解説!

今回は、Reactの重要なフックuseEffectの使い方やその周辺知識について説明します。useEffectの基本から、useLayoutEffectとの微妙な違い、そして使用する際のベストプラクティスまでを紹介していきます!

3 行で要約すると

  • useEffectは React のライフサイクルや副作用を扱う強力なフック。
  • useEffectuseLayoutEffectは実行タイミングに違いがあり、使用シーンに注意が必要。
  • 正しくuseEffectを使用することでアプリケーションの品質とメンテナンス性が大幅に向上する。

useEffect の基本的な使い方

useEffect とは?

useEffect は React のフックの一つで、関数コンポーネント内で副作用を扱うためのものです。副作用とは、データの取得、購読、手動での DOM の変更など、レンダリングに関係ない作業のことを指します。useEffect は主に 2 つの使い方があります。一つは、コンポーネントのマウント(初回レンダリング)時に実行する処理、もう一つは特定の変数の値が変わった時に実行する処理です。

例として、ユーザーの設定情報を API から取得する場合を考えます。

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

function UserProfile() {
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    // APIからユーザーデータを取得
    fetch("/api/user")
      .then((response) => response.json())
      .then((data) => setUserData(data));
  }, []); // この空の配列が重要です。これにより、この処理はコンポーネントのマウント時にのみ実行されます。

  if (!userData) return <div>Loading...</div>;
  return <div>Hello, {userData.name}!</div>;
}

上の例では、useEffectの中で API からユーザーデータを取得しています。そして、空の配列[]を第二引数として渡しているため、この API の呼び出しはコンポーネントのマウント時にのみ実行されます。

useEffect の第二引数について

useEffect の第二引数は、依存配列として知られています。この依存配列の役割は、特定の変数の変更を監視し、その変数の値が変わったときのみ useEffect 内の処理を実行するというものです。

具体的な動作は以下のようになります:

  • 空の配列([])の場合
    useEffect 内の処理は、コンポーネントがマウントされた時(初回レンダリング時)にのみ実行されます。コンポーネントが更新されても、useEffect 内の処理は再度実行されません。

  • 変数を含む配列の場合(例:[var1, var2]
    この場合、var1var2の値が変更されるたびに、useEffect 内の処理が実行されます。初回マウント時にも実行されます。

  • 引数を省略した場合
    useEffect 内の処理は、コンポーネントが更新されるたびに実行されます。初回マウント時も含めて、再レンダリングが行われるたびに useEffect は実行されることになります。

実例で考えてみましょう。ユーザー名を変更するたびに、何かの処理を行いたいとします。

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

function UserNameDisplay() {
  const [userName, setUserName] = useState("John");

  useEffect(() => {
    console.log("User name was updated:", userName);
  }, [userName]); // この場合、userNameの値が変わるたびに上の処理が実行されます

  return (
    <div>
      <p>Hello, {userName}!</p>
      <button onClick={() => setUserName("Doe")}>Change Name</button>
    </div>
  );
}

この例では、userName が変更されるたびに、コンソールにユーザー名が出力されます。[userName]という依存配列により、userName の変更を監視しています。

このように、useEffect の第二引数を適切に使うことで、さまざまな条件下での副作用の実行をコントロールすることができます。

useEffect の使用例 3 パターン

使用例 1: データの取得

初心者が React を学び始めるとき、API からデータを取得する方法は一つの大きなステップとなります。useEffect を使うと、このような非同期処理を簡単に扱うことができます。

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

function TodoList() {
  const [todos, setTodos] = useState([]);

  useEffect(() => {
    fetch("/api/todos")
      .then((response) => response.json())
      .then((data) => setTodos(data));
  }, []);

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

この例では、コンポーネントがマウントされた時に API から ToDo リストのデータを取得し、それを表示しています。

使用例 2: イベントリスナーの追加と削除

ブラウザのイベントリスナーを扱う場合、追加と削除の両方を忘れずに行うことが重要です。useEffectを使えば、これもスムーズに実装できます。

import React, { useEffect } from "react";

function MousePosition() {
  useEffect(() => {
    const handleMouseMove = (e) => {
      console.log(`Mouse position: ${e.clientX}, ${e.clientY}`);
    };

    window.addEventListener("mousemove", handleMouseMove);

    return () => {
      // コンポーネントのアンマウント時にイベントリスナーを削除
      window.removeEventListener("mousemove", handleMouseMove);
    };
  }, []);

  return <div>Move your mouse and check the console!</div>;
}

ここでのポイントは、useEffect 内でイベントリスナーを追加して、その return 文でリスナーを削除していることです。これにより、コンポーネントがアンマウントされるときにイベントリスナーもきちんと削除され、メモリリークを防ぐことができます。

使用例 3: ローカルストレージへのデータ保存

ユーザーの設定や、一時的なデータをローカルストレージに保存するケースもよくあります。この操作も useEffect と組み合わせることで簡潔に書けます。

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

function ThemeSelector() {
  const [theme, setTheme] = useState("light");

  useEffect(() => {
    const savedTheme = localStorage.getItem("user-theme");
    if (savedTheme) {
      setTheme(savedTheme);
    }
  }, []);

  useEffect(() => {
    localStorage.setItem("user-theme", theme);
  }, [theme]);

  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={() => setTheme("light")}>Light</button>
      <button onClick={() => setTheme("dark")}>Dark</button>
    </div>
  );
}

この例では、初回レンダリング時にローカルストレージからテーマの設定を取得し、テーマが変更されるたびにそれをローカルストレージに保存しています。

useEffect と「useLayoutEffect」

「useLayoutEffect」とは?

useLayoutEffectは、useEffectと非常によく似ていますが、実行タイミングが異なる React のフックです。useEffectは DOM の変更後、ブラウザのペイント処理が終了してから非同期に呼び出されますが、useLayoutEffectはペイント前に同期的に呼び出されます。

実際のコード例を考えてみましょう:

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

function ComponentSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useLayoutEffect(() => {
    const updateSize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };
    window.addEventListener("resize", updateSize);
    updateSize();
    return () => window.removeEventListener("resize", updateSize);
  }, []);

  return (
    <div>
      Window size: {size.width} x {size.height}
    </div>
  );
}

この例では、useLayoutEffectを使用してウィンドウのサイズを取得し、変更を監視しています。DOM の変更があった直後、ブラウザが画面をペイントする前にサイズ情報を更新したい場合、useLayoutEffectは適切な選択となります。

「useEffect」と「useLayoutEffect」の違い

前述のとおり、これら 2 つのフックの主な違いは実行タイミングにあります。useEffectは非同期に、そしてペイント後に呼び出されるのに対して、useLayoutEffectは同期的に、ペイント前に呼び出されます。

この違いにより、useLayoutEffectは DOM の変更が画面に反映される前に何らかの操作を行いたい場合に使用されます。一方、useEffectはペイント後の非同期のタスク、例えばデータの取得などに使われます。

useLayoutEffectを使用すると、DOM 要素の大きさなどの情報を、それが画面に反映される前に取得することができます。これにより、レンダリングのジッターや不必要な再レンダリングを防ぐことができる場合があります。ただし、useLayoutEffectは同期的に実行されるため、過度に使用するとパフォーマンスに影響を及ぼすことがあるので、適切な場面での使用が推奨されます。

useEffect を使用する際の注意点

useEffect と非同期処理

React のuseEffectフック内で非同期処理を行う際には、特定のパターンを守ることが推奨されています。

useEffect(() => {
  async function fetchData() {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    setData(data);
  }

  fetchData();
}, []);

上記のコードでは、非同期関数fetchDataを定義し、その中で非同期処理を行っています。このパターンを使用する理由は、useEffectのコールバック自体を非同期にすることはできないためです。

useEffect とクリーンアップ

useEffectはクリーンアップ関数を返すことができます。このクリーンアップ関数は、コンポーネントのアンマウント時や、useEffectが再度実行される前に呼び出されます。

useEffect(() => {
  const timer = setTimeout(() => {
    console.log("This will run after 1 second");
  }, 1000);

  return () => {
    clearTimeout(timer);
  };
}, []);

この例では、タイマーをクリアするためのクリーンアップ関数を提供しています。これにより、コンポーネントがアンマウントされたときに不要な動作を防ぐことができます。

useEffect の依存配列

useEffectの第二引数として提供する依存配列は非常に重要です。この配列にリストされている変数が変更されたときのみ、useEffectは再実行されます。依存配列を正しく提供しないと、予期しないバグや無駄な再レンダリングが発生する可能性があります。

const [count, setCount] = useState(0);

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // ここでcountを依存配列に追加

上記の例では、countが変更されたときのみuseEffectが再実行されるようにしています。

このように、useEffectを使用する際は、非同期処理のパターン、クリーンアップ、依存配列の正確な管理など、いくつかの注意点を理解することが重要です。

useEffect の使い方まとめ

useEffectは、React のフックの中でも特に多用されるものの一つです。コンポーネントのライフサイクルに関連する副作用を扱うためのツールとして、さまざまな場面で役立ちます。この記事を通じて、以下のポイントについての理解が深まったことを願っています。

  1. 基本的な使い方useEffectは、副作用を実行するためのフックです。ページのタイトルの変更、データの取得、DOM の操作など、さまざまな副作用を安全に扱うことができます。

  2. 実行タイミングの制御useEffectの依存配列を利用することで、副作用の実行タイミングを細かく制御できます。特定の状態やプロップが変更された時だけ、副作用を実行することも可能です。

  3. useLayoutEffectとの違いuseEffectはペイント後に非同期に実行されますが、useLayoutEffectはペイント前に同期的に実行されます。これにより、特定の場面での DOM の読み取りや更新をより制御しやすくなります。

  4. クリーンアップの重要性:副作用はしばしばクリーンアップを伴うものです。イベントリスナーの削除やタイマーのクリアなど、不要になったリソースを適切に解放することは、アプリケーションのバグを防ぎ、パフォーマンスを向上させるキーとなります。

  5. 非同期処理の取り扱いuseEffect内で非同期処理を行う際には、特定のパターンを守ることが推奨されています。これにより、非同期タスクの結果が必要とされる前にコンポーネントがアンマウントされるなどの問題を回避できます。

useEffectをうまく活用することで、React アプリケーションの品質とメンテナンス性を向上させることができます。