Next.js と TypeScript で、React カスタムフックを使う

TwitterFacebookHatena

TL;DR

このページでは、React カスタムフックの実装方法について解説しますね。一言でいうと、React カスタムフックとは、複数のコンポーネントでロジックを共有するための強力なツールです。

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

「カスタムフック」 とは?

React のカスタムフックとは、独自のロジックを含む関数を作成し、それを他のコンポーネントで再利用することができる仕組みです。これにより、コードの重複を避け、テストや保守が容易になります。

カスタムフックの基本的な形は以下のようになります。

const useCustomHook = () => {
  // 独自のロジック
}

// カスタムフックの使用例
const Component = () => {
  const value = useCustomHook()
  // ...
}

ここでは、独自のロジックを定義した useCustomHook というカスタムフックを作成しました。そして、Component というコンポーネント内で useCustomHook を呼び出すことでそのロジックを利用しています。

カスタムフックのルール

React フックのルールは、カスタムフックでも同様に適用されます。React フックを使用する際には以下の 2 つの主要なルールがあります。

1. フックはトップレベルでのみ呼び出す

フックはループ、条件文、ネストされた関数内で呼び出すべきではありません。これにより、React がフックの状態を正しく保持できます。

2. フックは React 関数内からのみ呼び出す

React の関数コンポーネントやカスタムフック内からフックを呼び出すことができます。しかし、通常の JavaScript 関数からはフックを呼び出さないでください。

これらのルールは React がフックの呼び出し順序を追跡して、フックの状態とライフサイクルを正しく管理するために必要です。また、React チームはこれらのルールを遵守するための ESLint プラグインも提供しています(eslint-plugin-react-hooks)。

カスタムフックを作成する際にも、これらのルールを遵守する必要があります。また、カスタムフックの名前は use で始まるべきです。これはカスタムフックが特別なフックルールに従うべき関数であることを示す一方、コードを読んでいる人にその関数がフックであることを知らせる役割も果たします。

上記のルールに違反すると、フックが正常に動作しない、または予期しないバグを引き起こす可能性があります。したがって、これらのルールを理解し、遵守することは重要です。

どうったケースで使われるか?

React のカスタムフックは、コンポーネント間でロジックを共有するために主に使われます。フックはステートフルなロジックを再利用することを可能にし、それをコンポーネント間で簡単に共有できます。例えば以下のようなケースでカスタムフックが使用されることが多いです。

データフェッチ

API からデータをフェッチするロジックは、アプリケーション全体で頻繁に行われるため、カスタムフックとして定義して再利用することがあります。カスタムフックはデータのローディング状態、成功時のデータ、エラー状態などを管理するのに役立ちます。

フォームハンドリング

フォームの値の状態管理、バリデーション、サブミットハンドリングなど、フォーム関連のロジックをカスタムフックで抽象化することもよく行われます。

タイマーまたはインターバル

一定時間ごとに何かを行うタイマーやインターバルのロジックは、カスタムフックとしてまとめると再利用が可能です。

これらは一例であり、カスタムフックの使用ケースは無限大です。基本的には、どのようなステートフルなロジックもカスタムフックにまとめ、コンポーネント間で共有できます。これにより、コードの重複を避け、可読性と保守性を高めることができます。

Next.js で、カスタムフックを実装

次に具体的な例を見てみましょう。ここでは、ユーザーのブラウザの幅と高さを返すカスタムフック、useWindowSize を作成します。

/hooks/useWindowSize.ts

import { useEffect, useState } from 'react'

const useWindowSize = () => {
  const [windowSize, setWindowSize] = useState({ width: 0, height: 0 })

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({ width: window.innerWidth, height: window.innerHeight })
    }

    window.addEventListener('resize', handleResize)
    handleResize()

    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])

  return windowSize
}

export default useWindowSize

useWindowSize は幅と高さを含むオブジェクトを状態として保持します。useState フックを使って、その状態を windowSize として初期化し、その更新関数を setWindowSize としています。

次に useEffect フックを使って、コンポーネントがマウントされた後と、ブラウザサイズが変更された時に setWindowSize を呼び出しています。その際、window.innerWidthwindow.innerHeight の値を新しい状態として設定しています。

このカスタムフックを使うと、どのコンポーネントでもブラウザのサイズを知ることができます。例えば次のようなコンポーネントが考えられます。

/components/WindowSizeDisplay.tsx

import useWindowSize from '../hooks/useWindowSize'

const WindowSizeDisplay = () => {
  const size = useWindowSize()

  return (
    <div>
      現在のウィンドウのサイズ: 幅 {size.width}px、高さ {size.height}px
    </div>
  )
}

export default WindowSizeDisplay

このコンポーネントは、ウィンドウの幅と高さを表示します。useWindowSize カスタムフックを呼び出し、得られた結果をレンダリングに使用しています。

カスタムフックを実装

次に、非同期データフェッチにカスタムフックを適用してみましょう。このような場合には、useStateuseEffect を組み合わせて使用することが一般的です。

以下に、外部 API からデータを取得し、その結果を提供する useFetch カスタムフックを示します。

/hooks/useFetch.ts

import { useState, useEffect } from 'react'

type State<T> = {
  data?: T
  loading: boolean
  error?: Error
}

const useFetch = <T,>(url: string): State<T> => {
  const [state, setState] = useState<State<T>>({
    data: undefined,
    loading: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url)
        const data = await response.json()
        setState({ data, loading: false })
      } catch (error) {
        setState({ loading: false, error })
      }
    }

    fetchData()
  }, [url])

  return state
}

export default useFetch

このカスタムフックは、url を引数に取り、その URL からデータをフェッチします。フェッチの結果を保持するために、dataloading、および error の 3 つの状態を useState で定義しています。

その後、useEffect フックの中で非同期関数 fetchData を呼び出しています。fetchDataurl をフェッチし、成功した場合は取得したデータを data 状態に設定し、loadingfalse に設定します。何らかのエラーが発生した場合は、そのエラーを error 状態に設定し、loadingfalse に設定します。

これにより、API からデータをフェッチする必要がある任意のコンポーネントでこのカスタムフックを使用できます。次にその使用例を示します。

/components/User.tsx

import useFetch from '../hooks/useFetch'

type User = {
  id: number
  name: string
  email: string
}

const User = ({ id }: { id: number }) => {
  const { data: user, loading, error } = useFetch<User>(`/api/user/${id}`)

  if (loading) return <div>読み込み中...</div>
  if (error) return <div>エラー: {error.message}</div>

  return (
    <div>
      <h2>{user?.name}</h2>
      <p>{user?.email}</p>
    </div>
  )
}

export default User

このコンポーネントは useFetch カスタムフックを使用してユーザー情報を取得し、それを表示します。取得中は "読み込み中..." を、エラーが発生した場合はエラーメッセージを表示します。

以上で、React のカスタムフックについての解説を終えます。カスタムフックは非常に強力なツールであり、同じロジックを再利用したり、複雑なロジックを抽象化したりすることが可能です。適切に使用することでコードの可読性と再利用性を大いに向上させることができますので、積極的に利用してみてください。

Next.js と TypeScript で、React カスタムフックを使う