Next.js と TypeScript で、useCallback の使い方を理解する

TwitterFacebookHatena

TL;DR

この記事では、Next.js と TypeScript を用いて、React の useCallback フックの実装とその動作について解説します。最終的に、useCallback の概念と効率的な使用法を理解し、パフォーマンスを向上させるためのテクニックを身につけることができます。

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

useCallback とは?

useCallback は、特定の関数をメモ化(一度計算した結果を記憶しておくこと)するための React Hook です。これにより、依存関係が変化しない限り、同じ関数を再利用することができ、結果としてコンポーネントのパフォーマンスが向上します。と言っても難しいかと思いますので、わかりやすく説明します。

まず、useCallback が何かを理解するために、まず「メモ化」という言葉を理解する必要があります。

メモ化

「メモ化」とは、難しい問題を解くときに、一度解いた問題の答えをメモしておいて、次に同じ問題に出会ったときにはそのメモを見てすぐ答えを思い出すようなものです。例えば、九九の表は「メモ化」の一種と言えます。2x3 が 6 であることを覚えておけば、次に 2x3 の計算が出てきたときにはすぐに答えが 6 であることを思い出すことができます。

では、ここで useCallback が登場します。useCallback はこの「メモ化」をプログラミングに使うための道具です。

例えば、ボタンを押すとカウンターの数が 1 つずつ増えるプログラムを作るとします。このとき、ボタンを押すたびにカウンターを増やす「関数」という命令が実行されます。しかし、ボタンを何度も押していると、同じ命令が何度も繰り返され、プログラムが無駄に重くなることもあります。

そこで useCallback を使うと、一度実行した命令(関数)を「メモ」しておけるので、ボタンを押すたびに新しく命令を作るのではなく、前に作った命令を使い回すことができます。これにより、プログラムが重くなることを防ぐことができます。

つまり、useCallback は「一度覚えた答え(命令)を使い回すための道具」であり、これによってプログラムがスムーズに動くようになるのです。

基本的な使用法は以下のようになります。

const memoizedCallback = useCallback(() => {
  doSomething(a, b)
}, [a, b])

ここで、doSomething(a, b)はメモ化したい関数、[a, b]は依存配列と呼ばれます。この依存配列にリストされた変数のいずれかが変化すると、新しい関数が作成されます。

なぜ依存配列が必要なのか?

依存配列とは、React の特定のフック(useEffect や useMemo、useCallback など)で使われるもので、そのフックが「依存」する値をリストアップした配列のことを指します。

さて、なぜ依存配列が必要なのでしょうか?それを理解するためには、React がどのように動作するかを理解する必要があります。

React は、コンポーネントの状態が変更されると、そのコンポーネントを再描画します。そして、再描画の際に、useEffect や useMemo、useCallback などのフック内の処理も再度実行されます。しかし、すべての処理を毎回再実行すると、パフォーマンスが低下する可能性があります。

そこで依存配列が役立ちます。依存配列に指定された値が変更された場合に限り、フック内の処理が再実行されます。依存配列に何も指定しない場合、フック内の処理は一度だけ実行され、その結果はメモ化されます。依存配列に空の配列を指定すると、フック内の処理はコンポーネントの初回描画時にだけ実行されます。

依存配列を理解するには、「フックが依存する値をリストアップする」と考えると良いでしょう。その値が変わると、フック内の処理は再実行され、値が変わらなければ以前の結果が再利用されます。これにより、不必要な処理を減らすことでパフォーマンスを向上させることが可能です。

例えば、useCallback フックを使ってボタンのクリックイベントをハンドリングする関数をメモ化するとします。その関数が特定の状態(例えば、カウンターの値)に依存する場合、その状態を依存配列に追加します。このようにすると、その状態が変更されるたびに、新しい関数が作成され、メモ化されます。状態が変更されなければ、以前に作成された関数が再利用されます。これにより、関数の再作成を最小限に抑え、パフォーマンスを向上させることができます。

Next.js で、useCallback を実装

ここでは、useCallback の使用例として、ボタンクリックによるカウントアップを実装します。useCallback を使って、カウントアップする関数をメモ化します。

ファイル名:components/Counter.tsx

import { useState, useCallback } from 'react'

type Props = {
  initialCount: number
}

const Counter = ({ initialCount }: Props) => {
  const [count, setCount] = useState(initialCount)

  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1)
  }, [])

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  )
}

export default Counter

上記のコードでは、useStateを使ってカウントの状態を管理し、その値を増やすための関数incrementを定義しています。この関数はuseCallbackを使ってメモ化され、その結果としてパフォーマンスが向上します。

使い所

useCallback フックは、主に 2 つのシチュエーションで使うと良いです。

イベントハンドラを最適化するとき

React コンポーネントでイベントハンドラ(例えば、ボタンクリックの処理)を定義するとき、その関数はコンポーネントが再レンダリングされるたびに新しく生成されます。これはパフォーマンスに影響を与える可能性があります。また、この新しく生成される関数は、他のコンポーネントやフックの依存配列をトリガーする可能性があります。ここで useCallback を使うと、依存性が変わらない限り同じ関数を再利用できるので、このような問題を回避できます。

子コンポーネントに関数を渡すとき

React コンポーネントが子コンポーネントに関数を props として渡すとき、その関数が再レンダリングのたびに新しく生成されると、子コンポーネントも不必要に再レンダリングされます。この問題を避けるためにも useCallback を使います。

しかし、全ての場面で useCallback を使うべきかというと、そうではありません。なぜなら、useCallback 自体もリソースを消費します。つまり、useCallback を使うことによる恩恵がそれを上回る場面で使うべきです。

また、useCallback は最適化の一種ですので、無闇に使うと逆にコードの読みやすさを損なう可能性があります。パフォーマンスが実際に問題になっている場面で、そしてその問題が useCallback によって解決可能である場合にのみ使用することをおすすめします。

要するに、useCallback の主な使い道はパフォーマンス最適化であり、全ての関数で使うべきツールではありません。パフォーマンスの問題が観測され、その原因が関数の再生成によるものであると明らかになった場合にのみ、useCallback を使うことを考慮すると良いでしょう。

useCallback の進化的実装

useCallback の主な使用例は、コンポーネントのパフォーマンス最適化です。ここでは、useCallback を使って子コンポーネントの不要な再レンダリングを防ぐ例を見てみましょう。

ファイル名:components/Parent.tsx

import { useState, useCallback } from 'react'
import Child from './Child'

type Props = {
  initialCount: number
}

const Parent = ({ initialCount }: Props) => {
  const [count, setCount] = useState(initialCount)

  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1)
  }, [])

  return (
    <div>
      <p>Count: {count}</p>
      <Child onIncrement={increment} />
    </div>
  )
}

export default Parent

ファイル名:components/Child.tsx

type Props = {
  onIncrement: () => void
}

const Child = ({ onIncrement }: Props) => {
  console.log('Child component rendered')

  return <button onClick={onIncrement}>Increment</button>
}

export default Child

ここでは、ParentコンポーネントがChildコンポーネントにincrement関数を渡しています。このincrement関数がメモ化されているため、Parentが再レンダリングされても、Childは不必要に再レンダリングされません。

Emotion で実装

Emotion と組み合わせると、useCallback はユーザーのインタラクションに基づいてスタイルを動的に変更する際に役立ちます。以下に具体的な実装例を示します。

/** @jsxImportSource @emotion/react */
import { useState, useCallback } from 'react'
import { css } from '@emotion/react'

type Props = {
  initialColor: string
}

const ColorChanger = ({ initialColor }: Props) => {
  const [color, setColor] = useState(initialColor)

  const changeColor = useCallback(() => {
    setColor((prevColor) => (prevColor === 'red' ? 'blue' : 'red'))
  }, [])

  return (
    <div
      onClick={changeColor}
      css={css`
        background-color: ${color};
        width: 100px;
        height: 100px;
      `}
    />
  )
}

export default ColorChanger

ここで、ColorChanger コンポーネントはクリックするたびに背景色を切り替えます。色を変えるための関数 changeColor が useCallback でメモ化されているため、パフォーマンスが向上します。

Next.js と TypeScript で、useCallback の使い方を理解する