⚡️

useEffectのコールバック関数とcleanUp関数の実行タイミング、正しく説明できますか?

2023/05/28に公開

TL;DR

「正しく説明できないな」となった人は useEffect を使ったり useEffect の関連記事を読む前に、ここで一緒に理解していきましょう。

この記事を最後まで読めば useEffect の基本についてはバッチリになると思います。

useEffect の基本

以下のような形が基本形となります。

第一引数には useEffect のコールバック関数、第二引数には依存配列と呼ばれるものを渡します。

依存配列に値を渡した場合、その値が更新された際にコールバック関数が実行されます。

useEffect(() => {
    console.log("useEffect called");
}, [])

useEffect の cleanUp 処理

ところで useEffect には cleanUp 処理を用意することができます。

どういうことかというと、コンポーネントが消滅した際(アンマウント or 再レンダリングされたとき)に実行される処理で以下のように定義します。

useEffect(() => {
    console.log("useEffect called");
    return () => {
        console.log("cleanUp");
    }
}, [])

このコードの例だとコンポーネントが消滅した際にはコンソールに"cleanUp"が表示されることになります。

検証

ではみなさん、以下の 3 つの状況で useEffect の本体と cleanUp 処理がどのタイミングで実行されるか即答できますでしょうか?

  • 依存配列が空の時
  • 依存配列に値が設定されているとき
  • 依存配列が定義されていないとき

この 3 つのパターンについて、以下のサンプルコードで確認していきたいと思います。

test.tsx
import { useState } from "react"
import Counter from "../Components/counter"

export default function Test() {
    const [isDisp, setIsDisp] = useState(true)
    return (
        <>
            <div>
                {isDisp && <Counter />}
            </div>
            <button className='border border-black text-black font-bold py-2 px-4 mt-2' onClick={() => setIsDisp(prev => !prev)}>表示/非表示</button>
        </>
    )
}
counter.tsx
import { useState, useEffect } from 'react';

export default function Counter() {
    const [count, setCount] = useState(100)
    useEffect(() => {
        console.log("useEffect called");
        return () => {
            console.log("cleanUp");
        }
    }, [])
    return (
        <>
            <h1>{count}</h1>
            <button onClick={() => setCount(prev => prev + 1)} className='border border-black text-black font-bold mr-2 py-2 px-4'>+</button>
            <button onClick={() => setCount(prev => prev - 1)} className='border border-black text-black font-bold py-2 px-4 '>-</button>
        </>
    )
}

このコードは以下のようなカウンターコンポーネントです。

それぞれのコンポーネントの役割ですが、counter コンポーネントは useEffect を持っており、そこに指定したコールバック関数と cleanUp 関数の動きを確認します。

一方で親コンポーネントとなるTestコンポーネントはマウント or アンマウントを制御するためコンポーネントです。「表示・非表示」ボタンを押下することでマウント or アンマウントを発生させます。

依存配列が空の場合

まずは依存配列が空の場合から確認します。

counter.tsx
import { useState, useEffect } from 'react';

export default function Counter() {
    const [count, setCount] = useState(100)
    useEffect(() => {
        console.log("useEffect called");
        return () => {
            console.log("cleanUp");
        }
    }, [])
    return (
        <>
            <h1>{count}</h1>
            <button onClick={() => setCount(prev => prev + 1)} className='border border-black text-black font-bold mr-2 py-2 px-4'>+</button>
            <button onClick={() => setCount(prev => prev - 1)} className='border border-black text-black font-bold py-2 px-4 '>-</button>
        </>
    )
}

初回のレンダリングが終わったタイミングでコンソールを確認します。

"useEffect called"が一度だけ出力されているのがわかります。

では、「+」ボタンを押下してみます。ここで再レンダリングが走ることになります。

しかし、このタイミングではコンソールに"useEffect called"も"cleanUp"も出力されていません。

最後に「表示・非表示」ボタンを押下してアンマウントを発生させます。

ここでCounterコンポーネントが DOM ツリーからアンマウントされたので、cleanUp 関数が実行されコンソールには"cleanUp"が表示されました。

すなわち、依存配列が空の場合は初回のレンダリング時に useEffect のコールバック関数が実行され、アンマウント時には cleanUp 関数が実行されている。

しかし、state の更新に伴う再レンダリングが発生した際には useEffect のコールバック関数も cleanUp 関数も実行されていないということです。

依存配列に値を指定した場合

では次に、依存配列に値を指定した場合を見てみます。

counter.tsx
import { useState, useEffect } from 'react';

export default function Counter() {
    const [count, setCount] = useState(100)
    useEffect(() => {
        console.log("useEffect called");
        return () => {
            console.log("cleanUp");
        }
    }, [count])
    return (
        <>
            <h1>{count}</h1>
            <button onClick={() => setCount(prev => prev + 1)} className='border border-black text-black font-bold mr-2 py-2 px-4'>+</button>
            <button onClick={() => setCount(prev => prev - 1)} className='border border-black text-black font-bold py-2 px-4 '>-</button>
        </>
    )
}

まずは初回のレンダリングが終わったタイミングでコンソールを確認します。

ここでは先ほどと変わらず、"useEffect called"が一度だけ出力されているのがわかります。

では、「+」ボタンを一度押します。

すると今度は「+」ボタンを押下したタイミングで"cleanUp"と"useEffect called"が出力されたのがわかると思います。

最後に「表示・非表示」ボタンを押下してアンマウントを発生させます。

ここでCounterコンポーネントが DOM ツリーからアンマウントされたので、cleanUp 関数が実行されコンソールには"cleanUp"が表示されました。

すなわち、依存配列に値を指定した場合は初回のレンダリング時に useEffect のコールバック関数が実行され、アンマウント時には cleanUp 関数が実行されている。

また、state の更新に伴う再レンダリングが発生した際には useEffect のコールバック関数も cleanUp 関数も実行されているということです。

依存配列を省略した場合

では最後に、依存配列を省略した場合を見てみます。

counter.tsx
import { useState, useEffect } from 'react';

export default function Counter() {
    const [count, setCount] = useState(100)
    useEffect(() => {
        console.log("useEffect called");
        return () => {
            console.log("cleanUp");
        }
    })
    return (
        <>
            <h1>{count}</h1>
            <button onClick={() => setCount(prev => prev + 1)} className='border border-black text-black font-bold mr-2 py-2 px-4'>+</button>
            <button onClick={() => setCount(prev => prev - 1)} className='border border-black text-black font-bold py-2 px-4 '>-</button>
        </>
    )
}

まずは初回のレンダリングが終わったタイミングでコンソールを確認します。

ここでは先ほどと変わらず、"useEffect called"が一度だけ出力されているのがわかります。

では、「+」ボタンを一度押します。

すると今度は「+」ボタンを押下したタイミングで"cleanUp"と"useEffect called"が出力されたのがわかると思います。

最後に「表示・非表示」ボタンを押下してアンマウントを発生させます。

ここでCounterコンポーネントが DOM ツリーからアンマウントされたので、cleanUp 関数が実行されコンソールには"cleanUp"が表示されました。

すなわち、依存配列を省略した場合は初回のレンダリング時に useEffect のコールバック関数が実行され、アンマウント時には cleanUp 関数が実行されている。

また、state の更新に伴う再レンダリングが発生した際には useEffect のコールバック関数も cleanUp 関数も実行されているということです。

まとめ

以下の図のイメージです。

依存配列が空の場合

依存配列に値を指定した場合

依存配列を省略した場合

Discussion