Next.js と TypeScript で、useEffect を分かりやすく解説

TwitterFacebookHatena

TL;DR

この記事では、Next.js と TypeScript を使った useEffect の効果的な使い方について詳しく解説します。useEffect は React の生命周期に対応した Hooks の一つであり、コンポーネントがレンダリングされた後に何らかの作業を行うことが可能になります。この作業は、データのフェッチ、手動な DOM の変更、ログの記録、そして他の購読や非同期操作など、多岐にわたります。

開発環境 バージョン
Next.js 13.4.3
TypeScript 5.0.4
Emotion 11.11.0
React 18.2.0

useEffect は「ある状況がきたら、あることをする」

useEffect は、React(ウェブサイトを作るためのツール)の一部で、「関数(やりたいことのまとまり)」を特定のタイミングで動かすためのものです。これが何を意味しているか説明するために、実際の日常生活から例を出してみましょう。

例えば、学校から帰った後に宿題をする、という日常的な行動がありますよね。これを useEffect で表現すると、学校から帰ることが「特定のタイミング」で、宿題をすることが「やりたいことのまとまり(関数)」になります。つまり、学校から帰ったら宿題をする、という動きを useEffect を使ってプログラムすることができるのです。

また、useEffect にはもう一つ特徴があります。それが「クリーンアップ」機能です。これは、例えば遊び終わった後に片付けをする、という行動を表しています。遊び終わったら片付けをする、という動きも useEffect を使ってプログラムすることができます。

このように、useEffect を使うと「ある状況がきたら、あることをする」というプログラムを書くことができます。そして、そのあとの片付けも一緒に書けます。つまり、useEffect は日常生活でいうところの「ルーティン(決まった習慣)」を作るためのツールなんですね。

だから、あなたも日常生活でのルーティンを考えてみて、それをどのように useEffect に置き換えるか考えてみると、もっと理解が深まると思いますよ!

レンダリングの前後

React のコンポーネントには、レンダリングの前後に何らかの作業を行う必要がある場合があります。例えば、API からのデータの取得やタイマーの設定などです。このような作業を「サイドエフェクト」(副作用)と呼びます。サイドエフェクトは、主に以下のようなものがあります。

  • 外部データのフェッチング、購読または手動な DOM の操作
  • パフォーマンスのために使われる不純な読み込み(例えば、特定の要素へのスクロール位置の設定)
  • 他のコンポーネントに影響を及ぼす可能性のあるもの(例えば、タイマーやログイン状態の変更)

React の useEffect Hook を使うと、これらの副作用を取り扱うことができます。

React ではコンポーネントのレンダリング後に何らかの処理を実行するために useEffect を使用します。これは React が提供するフック(Hook)の一つで、サイドエフェクト(外部 API の呼び出し、イベントリスナの登録など)を扱うことができます。useEffect の基本的な使い方は以下のような形です。

import React, { useEffect } from 'react'

type Props = {
  message: string
}

const ShowMessage = ({ message }: Props) => {
  useEffect(() => {
    console.log(message)
  })

  return <div>{message}</div>
}

依存配列[]は、何をしているのか?

useEffectの第二引数として与えられる[]は、依存配列と呼ばれます。

この依存配列はuseEffect内で使用されるプロパティや値(ステート、プロップ、コンテキスト、など)のリストで、配列内のどの値が変化した場合にもuseEffectの内容が再実行されます。

例えば、あるステート変数countuseEffect内で使われていて、その変化に応じて何らかの処理を行いたい場合、以下のようにcountを依存配列に含めます。

useEffect(() => {
  // 何らかの処理...
}, [count])

しかし、[]のように依存配列が空の場合、つまり何も依存しない場合、useEffectの内容は一度だけ実行されます。これはコンポーネントがマウント(描画)された直後の一度だけです。

そのため、特定の値の変化に対して反応する必要がなく、一度だけ何らかの処理を実行したい場合には、依存配列を空にします。これは一般的には API からデータを取得する時や、イベントリスナを設定する際などに利用されます。

useEffect(() => {
  // APIからデータを取得するなどの処理...
}, [])

ただし、依存配列を正しく設定しないと予期しない挙動やパフォーマンス問題を引き起こす可能性があるため、注意が必要です。例えば、useEffect内で使われている値が依存配列に含まれていない場合、その値が変化してもuseEffectが再実行されないため、期待しない結果を引き起こす可能性があります。

この配列の要素が更新された時にだけ、useEffect 内の処理が行われます。依存配列を使用した例を見てみましょう。

import React, { useEffect } from 'react'

type Props = {
  message: string
}

const ShowMessage = ({ message }: Props) => {
  useEffect(() => {
    console.log(message)
  }, [message]) // messageが更新された時にだけconsole.logが実行されます

  return <div>{message}</div>
}

useeffect で初回のみ実行するには?

useEffectを使用して何かの処理をコンポーネントの初回レンダリング(マウント)時にのみ実行したい場合、第二引数に空の配列[]を渡します。この空の配列は、依存配列として機能し、その中のどの値も変化しない(つまり、値が存在しないため)ことを示します。結果として、useEffect内の処理はコンポーネントのマウント時に一度だけ実行されます。

以下に具体的なコードを示します。

useEffect(() => {
  console.log('This will only run once, after the first render')
}, []) // ← 空の依存配列

このコードでは、useEffect内の関数は初回レンダリング時にのみ実行され、その後のレンダリングでは実行されません。

注意点として、useEffect内で利用する値や関数がある場合、それらは依存配列に必ず含める必要があります。それらを含めずに空の依存配列を使用すると、予期せぬバグを引き起こす可能性があります。ただし、初回マウント時にのみ実行したい処理(API からのデータ取得など)が含まれている場合などは、このルールが適用されません。

クリーンアップ

クリーンアップとは、useEffectの一部として、エフェクト(副作用)がもたらす変更を元に戻すために実行される関数のことを指します。

useEffectは、あるコード片(エフェクトと呼ばれる)を特定のタイミングで実行します。このエフェクトは、外部のデータをフェッチする、サブスクリプションを設定する、タイマーを設定するなど、通常のレンダリングフローの外部で何かを実行する可能性があります。

しかし、これらのエフェクトは一時的なものであり、後でクリーンアップ(解除または元に戻す)する必要があります。例えば、サブスクリプションを設定した場合、それを解除しないとメモリリークを引き起こす可能性があります。また、タイマーを設定した場合、そのタイマーが未解除のままになると、それが不要になった時点での問題を引き起こす可能性があります。

このようなクリーンアップ処理をuseEffectでは、エフェクト関数からクリーンアップ関数を返すことで行います。React はこの返された関数を記憶し、次のレンダリングの前か、コンポーネントがアンマウントされる時にそれを実行します。つまり、エフェクトがもたらした変更を元に戻すための手段としてクリーンアップ関数が活用されるわけです。

実際の生活に例えてみると、クリーンアップは、遊びに出かける前に部屋を片付ける、または遊んだ後におもちゃを片付けるといった行動に相当します。ある行動が終わった後で、その影響を元に戻すための行動、それがクリーンアップなのです。

クリーンアップは少し難しそうに思えるかもしれませんが、シンプルな例で説明することで理解しやすくなるでしょう。

タイマーを使った例を考えてみましょう。ボタンを押すとカウントダウンが始まり、時間が経つとメッセージが表示されるというものです。

まず、カウントダウンの状態を管理するためのuseStateと、タイマーを設定およびクリーンアップするためのuseEffectを使います。

import { useState, useEffect } from 'react'

type Props = {
  startCount: number
}

const Countdown = ({ startCount }: Props) => {
  const [count, setCount] = useState(startCount)

  useEffect(() => {
    // タイマーを設定
    const timerId = setInterval(() => {
      setCount((prevCount) => prevCount - 1)
    }, 1000)

    // クリーンアップ関数を返す
    return () => {
      // タイマーをクリア
      clearInterval(timerId)
    }
  }, []) // 空の依存配列を渡してエフェクトを一度だけ実行

  return <div>{count > 0 ? <h1>{count}</h1> : <h1>Time's up!</h1>}</div>
}

export default Countdown

ここで、setIntervalはタイマーを設定して、一定の時間ごとにカウントダウンを進行させます。そして、そのタイマーはclearIntervalで解除(クリーンアップ)することができます。これがuseEffectのクリーンアップの基本的な例です。

このコードの中で重要な点は、useEffectから返される関数です。この関数は、次のuseEffectが実行される前か、もしくはコンポーネントがアンマウントされる際に実行されます。つまり、useEffectが再度実行される前にタイマーがクリアされるため、古いタイマーが残り続けて問題を引き起こすことはありません。

これをおもちゃで遊ぶシチュエーションに例えると、タイマー設定(setInterval)は新しいおもちゃを出すこと、タイマー解除(clearInterval)は遊んだ後におもちゃを片付けることに相当します。これにより、新しいおもちゃを出す前に古いおもちゃが片付けられ、お部屋がいつも整った状態に保たれるのです。

Next.js で、useEffect を実装

それでは、Next.js と TypeScript を使って useEffect をどのように実装するか見ていきましょう。

// pages/index.tsx

import { useEffect, useState } from 'react'

type Data = {
  id: number
  title: string
}

const HomePage = () => {
  const [data, setData] = useState<Data[]>([])

  useEffect(() => {
    fetch('https://api.example.com/posts')
      .then((response) => response.json())
      .then((data: Data[]) => setData(data))
  }, [])

  return (
    <div>
      {data.map((item) => (
        <div key={item.id}>{item.title}</div>
      ))}
    </div>
  )
}

export default HomePage

このコードは、useEffect と useState を使って外部 API からデータをフェッチし、そのデータを表示する Next.js のコンポーネントです。

この useEffect の中で行われている操作は次のとおりです。

  1. 空の配列([])で useState を初期化しています。これは、フェッチしたデータを格納するためのものです。
  2. useEffect を使って、コンポーネントがマウントされたときに API からデータをフェッチします。
  3. フェッチしたデータを JSON 形式に変換し、そのデータを useState にセットします。
  4. データをマップして表示します。

不要な副作用を回避する

useEffect は、第二引数として依存性配列を受け取ることができるとお話しました。この依存性配列に変数を追加すると、その変数の値が変更されたときにだけ、useEffect 内の関数が実行されます。これにより、不要な副作用を回避することができます。

useEffect からのクリーンアップは非常に重要な要素です。useEffect 内の関数が返す関数は、副作用のクリーンアップを行うために使用されます。これは、購読を解除したり、タイマーをクリアしたりするために使用されます。

以下に、useState と useEffect を用いて、簡易的なカウンターを作成する例を示します。

// pages/counter.tsx

import { useEffect, useState } from 'react'

const CounterPage = () => {
  const [count, setCount] = useState(0)
  const [timerId, setTimerId] = useState<NodeJS.Timeout | null>(null)

  useEffect(() => {
    setTimerId(setInterval(() => setCount((prev) => prev + 1), 1000))

    return () => {
      if (timerId) {
        clearInterval(timerId)
      }
    }
  }, [timerId])

  return (
    <div>
      <p>{count}</p>
    </div>
  )
}

export default CounterPage

この例では、useEffect を用いてタイマーを設定し、1 秒ごとにカウンターの値を増加させています。また、コンポーネントがアンマウントされるときにタイマーをクリアするクリーンアップ関数を設定しています。これにより、不要なメモリの消費を防ぎ、パフォーマンスを向上させることができます。

Next.js と TypeScript で、useEffect を分かりやすく解説