LoginSignup
25
21

useEffect・useLayoutEffectについて学ぶ(個人学習の備忘録)

Posted at

この記事は個人学習の備忘録です。

Udemyの講座【2023年最新】React(v18)完全入門ガイド|Hooks、Next.js、Redux、TypeScript(CodeMafia)

書籍Reactハンズオンラーニング 第2版 ―Webアプリケーション開発のベストプラクティス

上記を参考・引用しながら基本のReact Hooks・useEffectについて個人的に学んだことのアウトプット記事です。

1.useEffect はどのような場合に必要か

描画が完了した後に何か処理を実行したい場合に必要です。

例えば、チェックボックスのUIを提供するコンポーネントを使って、チェックボックスにチェックをつけたり外したりする実装を行う場合。
useStateフックを使えばコンポーネントの実装ができるが、アラートを表示させたい場合、useStateのみでは実装不可能です。

Checkbox.js
import React,{ useState } from "react";

function Checkbox(){
    const [checked, setChecked] = useState(false);
    
    alert(`checked: ${checked.toString()}`)

    return (
    <>
        <input 
            type="checkbox"
            value={checked}
            onChange={()=> setChecked(checked => !checked)} 
        />
        {checked ? "checked : "not checked"}
    </>
    );
};

上記のコードでは動きません。
alertは同期関数なので、ボタンがクリックされるまで次の処理が実行されないためです。この場合ではalertがコンポーネントの描画をブロックしてしまいます。

ではalertをreturn文の直後に呼び出すのはどうでしょうか?

Checkbox.js
function Checkbox(){
    const [checked, setChecked] = useState(false);

    return (
    <>
        <input 
            type="checkbox"
            value={checked}
            onChange={()=> setChecked(checked => !checked)} 
        />
        {checked ? "checked : "not checked"}
    </>
    );
    alert(`checked: ${checked.toString()}`)
};

上記のコードも動きません。
alertの行に到達する前に関数はreturn文で抜けてしまうからです。

useEffectを使用することでコンポーネントの描画が完了した後に副作用としてalertが呼び出すことができる

Checkbox.js
function Checkbox(){
    const [checked, setChecked] = useState(false);

    useEffect(() => {
        alert(`checked: ${checked.toString()}`)
    });

    return (
    <>
        <input 
            type="checkbox"
            value={checked}
            onChange={()=> setChecked(checked => !checked)} 
        />
        {checked ? "checked : "not checked"}
    </>
    );
 
};

useEffectはコールバック関数を引数に取るため、コンポーネントの描画が完了したあとにalert関数を呼び出すことができます。
useEffectには副作用として実行したい処理を実行します。

副作用とは

ここでの副作用とは、描画の一部ではない処理のことです。
上記の例では、Checkboxコンポーネントの描画関数にはチェックボックスのUI構築に関する処理のみが記述されているべきで、それ以外の処理は副作用となります。

alertやconsole.log等のAPI呼び出しはコンポーネントの描画の一部ではないのでuseEffectに記述するべきです。

また、副作用の中には描画の結果に依存するものがあり、そのような処理は描画が完了するのを待ってから実行する必要があるため、必然的にuseEffectに記述することになります。

useEffect(() => {
     console.log(checked? 'Yes,checked' : 'No, not checked')
});

useEffectフックは確実に描画が完了することを待ってから実行したい処理を記述するためにも使用できます。

2.タイマーを作りながら依存配列を学ぶ

ステートが更新され、コンポーネントツリーが再描画されたあと、最終的にuseEffectフックに設定された副作用関数が実行されます。
依存配列はuseEffectの2番目の引数として渡される配列です。

タイマーを作りながら依存配列を学びます。

2-1.第二引数に空の配列を渡す場合

useEffectの第二引数に空の配列を渡すことによって、最初にTimerコンポーネントが読み込まれた時だけ、window.setInterval()のコールバック関数が実行されます。
console.log('useEffect is called')が一度だけ読み込まれます。

Timer.js
import { useEffect, useState } from "react";

const Timer = () => {
  const [time, setTime] = useState(0);

  // 空の場合は依存している配列がない、Timerコンポーネントが生成された場合のみ実行される
  useEffect (() => {
    console.log('useEffect is called');
    window.setInterval(() => {
      setTime((prev) => prev + 1);
        }, 1000);
    // 空の配列
  },[])

  return <>{time}秒経過</>;
};

export default Timer;

実行結果

ezgif.com-video-to-gif.gif

コンポーネントが生成された時に一度だけ呼び出したい場合はこのように第二引数は空の配列にしておきます。

2-2.依存配列に含めたstateが更新された場合

Timer.js
import { useEffect, useState } from "react";

const Timer = () => {
  const [time, setTime] = useState(0);

  useEffect (() => {
    console.log('useEffect is called');
    window.setInterval(() => {
      setTime((prev) => prev + 1);
        }, 1000);
  },[])

  // 第二引数に依存配列timeを追記。
  // 配列に含めたステートが更新されると、コールバック関数が再度実行される
  useEffect(()=>{
    console.log('updated')
  },[time])


  return <>{time}秒経過</>;
};

export default Timer;

実行結果

ezgif.com-video-to-gif (1).gif

依存配列に含めたstateが更新されるたび、コールバック関数は再度実行されます。

2-3.依存配列を削除した場合

依存配列を削除すると、window.setIntervalは外側に書かれた状態と同じになるため、再レンダリングされるたびにsetInterval関数が実行されます。

Timer.js
import { useEffect, useState } from "react";

const Timer = () => {
  const [time, setTime] = useState(0);

  useEffect (() => {
    console.log('useEffect is called');
    window.setInterval(() => {
      setTime((prev) => prev + 1);
        }, 1000);
  },[])

  // 依存配列を消した場合、window.setIntervalは外側に書かれた状態と同じになるため、再レンダリングされるたびにsetInterval関数が実行される
  useEffect (() => {
    console.log('useEffect is called');
    window.setInterval(() => {
      setTime((prev) => prev + 1);
    }, 1000);
  })

  useEffect(()=>{
    console.log('updated')
  },[time])


  return <>{time}秒経過</>;
};

export default Timer;

実行結果

ezgif.com-video-to-gif (2).gif

このようにsetInterval関数がサイレンダリングする度に実行されてしまうので、依存配列の記述は忘れないようにしましょう。

注意すること:クリーンアップ関数

セットインターバル関数の使用が終わった時点で処理をクリアにする必要があります。
そうしないとメモリリーク(使用していないメモリを開放することなく確保し続けてしまう現象)に繋がってしまう可能性があるためです。

今回は、toggleボタンを押すとタイマーがストップし、止まる実装を行いました。
ですが、このままではタイマーを非表示にした際もsetInterval()関数が動き続けたままの状態になってしまいます。

ezgif.com-video-to-gif (5).gif

そこで、returnの戻り値に、クリーンアップ関数を設定します。

Timer.js
import { useEffect, useState } from "react";

const Example = () => {
  const [isDisp, setIsDisp] = useState(true);

  return (
    <>
      {isDisp && <Timer />}
      <button onClick={() => setIsDisp((prev) => !prev)}>toggle</button>
    </>
  );
};

const Timer = () => {
  const [time, setTime] = useState(0);

  useEffect(() => {
    console.log("init");
    // セットインターバルの戻り値としてセット
    let interValId = null;

    interValId = window.setInterval(() => {
      console.log("init called");
      setTime((prev) => prev + 1);
    }, 1000);

    return () => {
      // returnで渡したコールバック関数が、Timerコンポーネントが消滅する際に実行される
      // クリーンアップ関数 セットインターバルの使用が終わった時点でクリアにする必要がある

      console.log("end");
      window.clearInterval(interValId);
    };
  }, []);

  return (
    <h3>
      <time>{time}</time>
      <span>秒経過</span>
    </h3>
  );
};

export default Example;

実行結果

クリーンアップ関数を設定したことで、 セットインターバルの使用が終わった時点で処理がクリアされ、toggleボタンを切り替えてタイマーを止めた際は、処理も止めることができました。

ezgif.com-video-to-gif (5).gif

3.useEffectが実行されるタイミング

useEffectが実行される流れについての復習です。

第二引数が空の配列の場合

1.コンポーネントがマウント(生成)される
2.マウントされたタイミングでコールバック関数が実行される
3.何らかのステートが実行される(アップデート)
(空の配列の場合はこのタイミングでは特に何も起こらない)
4.コンポーネントが消滅した時、リターンに渡したクリーンアップ関数が実行される

第二引数が依存配列あり・更新ありの場合

1.コンポーネントがマウント(生成)される
2.コールバック関数が実行される
3.ステートが更新されていないか・依存配列に渡された値が元の値と違わないかチェック
〜〜元の値と違う場合〜〜
4.クリーンアップ処理が行われる
5.コールバック関数が実行される //ステートがアップデートされるたびに毎回呼び出される
〜〜元の値と違う場合〜〜
6.コンポーネントが消滅した際にコールバック関数が実行される

副作用のスキップによるパフォーマンス改善

Reactの公式ドキュメントでも記載されているように、副作用のクリーンアップと適用とをレンダーごとに毎回行うことはパフォーマンスの問題を引き起こす可能性があります。

再レンダー間で特定の値が変わっていない場合には副作用の適用をスキップするように、useEffectのオプションの第2引数に配列を渡します。

副作用をスキップすることを「最適化」と表現します。

4.useLayoutEffect について

Reactのドキュメントにはもう一つの副作用を記述するフックuseLayoutEffectがあります。

特性1.コンポーネントの描画の前に処理が実行される

これまで見てきた通り、useEffectで設定した副作用は必ずコンポーネントの描画の後に実行されますが、useLayoutEffectは、useEffectと呼び出しタイミングが異なり、コンポーネントの描画の前に行われます。

呼び出しタイミングの違い

1.コンポーネントの描画関数が呼び出される
2.useLayoutEffectで設定した副作用関数が呼び出される
3.ブラウザのPaint処理によりコンポーネントの描画結果が画面に反映される
4.useEffectで設定した副作用関数が呼び出される

App.js
import React, { useEffect, useLayoutEffect } from "react"

function App(){
    useEffect(()=>console.log("useEffect"));
    useLayoutEffect(()=>console.log("useLayoutEffect"));
    return <div>ready</div>
}

上記を実行すると、コンソールの実行結果は以下になります。

useLayoutEffect
useEffect

useLayoutEffectは、描画が画面に反映される手前で何か処理を実行したい場合に利用されます。

例)ブラウザウィンドウのサイズをもとにコンポーネントのサイズを計算

useWindowSize.js
function useWindowSize(){
    const [width,setWidth] = useState(0);
    const [height,setHeight] = useState(0);

const resize = () => {
    setWidth(window.innerWidth);
    setHeight(window.innerHeight);
}

useLayoutEffect(() => {
    window.addEventListener('resize',resize);
    resize();
    return () => window.removeEventListener('resize',resize);
},[]);

return [width,height];
}

戻り値のwidthとheightはコンポーネントが画面に表示されるよりも前に必要な情報であるため、useLayoutEffectフックを使用することでPaint処理が行われる前に処理の実行が可能になります。

特性2.コンポーネントの描画の前に処理が実行される

useEffectより処理が先に実行されるだけではなく、画面への反映よりも先にコールバック関数が実行されます。

Random.js
import { useLayoutEffect, useEffect, useState, useRef } from "react";

const Random = () => {
  const [state, setState] = useState(0);
  // POINT useLayoutEffect:useEffectで画面がちらつく場合に使用
  useLayoutEffect(() => {
// 画面への反映よりも先に実行される
    if (state === 0) {
      setState(Math.random() * 300);
    }
  }, [state]);

  return (
    <button
      className="effect-btn"
      onClick={() => setState(0)}
      style={{ fontSize: "2.5em" }}
    >
      state: {state}
    </button>
  );
};
export default Random;

参考

25
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
21