はじめに#
前回の記事では、useEffectフックを使って副作用処理とライフサイクルを管理する方法を解説しました。本記事では、Reactの重要なフックである「useRef」「useMemo」「useCallback」の3つについて、それぞれの特徴と使い分けを実践的に解説します。
これらのフックは、Reactアプリケーションの機能拡張とパフォーマンス最適化に欠かせないツールです。useRefはDOM要素への参照や再レンダリングを伴わない値の保持に、useMemoとuseCallbackはメモ化による不要な再計算・再レンダリングの防止に使用します。
本記事を読むことで、以下のことができるようになります。
- useRefでDOM要素を操作し、レンダリング間で値を保持する
- useMemoで計算コストの高い処理をメモ化する
- useCallbackで関数をメモ化し、子コンポーネントの再レンダリングを防止する
- 各フックの適切な使い分けと注意点を理解する
実行環境・前提条件#
必要な環境#
- Node.js 20.x以上
- Viteで作成したReactプロジェクト(TypeScript推奨)
- VS Code(推奨)
前提知識#
- 関数コンポーネントの基本
- useStateとuseEffectの使い方
- JavaScriptの参照とオブジェクトの基礎
useRef - DOM参照とミュータブルな値の保持#
useRefとは#
useRefは、レンダリング間で値を保持するためのフックです。useStateとは異なり、useRefの値を変更しても再レンダリングは発生しません。主に以下の2つの用途で使用されます。
- DOM要素への参照: input要素へのフォーカス、スクロール位置の制御など
- ミュータブルな値の保持: タイマーID、前回の値、レンダリングに影響しない情報など
基本構文#
useRefの基本構文は以下のとおりです。
1
2
3
4
5
6
7
8
9
10
11
12
|
import { useRef } from 'react';
function MyComponent() {
// 初期値を指定してrefオブジェクトを作成
const ref = useRef<number>(0);
// ref.currentで値にアクセス・変更
console.log(ref.current); // 0
ref.current = 1;
return <div>...</div>;
}
|
useRefは{ current: 初期値 }という形式のオブジェクトを返します。このオブジェクトはコンポーネントの全ライフサイクルを通じて同じ参照を維持します。
useStateとuseRefの違い#
useStateとuseRefの違いを以下の表にまとめます。
| 特性 |
useState |
useRef |
| 値の変更時 |
再レンダリングが発生 |
再レンダリングは発生しない |
| 値の保持 |
レンダリング間で保持 |
レンダリング間で保持 |
| 用途 |
UIに表示するデータ |
DOM参照、内部的な値の保持 |
| 変更方法 |
setter関数を使用 |
ref.currentに直接代入 |
実践例1: DOM要素へのフォーカス制御#
ボタンをクリックしたときに、入力フィールドにフォーカスを当てる例を見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
import { useRef } from 'react';
function FocusInput() {
// DOM要素への参照を作成(初期値はnull)
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = () => {
// current経由でDOM要素にアクセス
inputRef.current?.focus();
};
return (
<div>
<input
ref={inputRef}
type="text"
placeholder="ここにフォーカスが当たります"
/>
<button onClick={handleClick}>
入力欄にフォーカス
</button>
</div>
);
}
export default FocusInput;
|
この例では、inputRefをinput要素のref属性に渡すことで、DOM要素への参照を取得しています。ボタンがクリックされると、inputRef.current.focus()でプログラム的にフォーカスを制御できます。
実践例2: タイマーIDの保持#
setIntervalのタイマーIDを保持し、コンポーネントのアンマウント時にクリアする例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
import { useState, useRef, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
// タイマーIDを保持(再レンダリングは不要)
const intervalRef = useRef<number | null>(null);
useEffect(() => {
if (isRunning) {
intervalRef.current = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
}
// クリーンアップ関数
return () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
}
};
}, [isRunning]);
const handleStart = () => setIsRunning(true);
const handleStop = () => setIsRunning(false);
const handleReset = () => {
setIsRunning(false);
setSeconds(0);
};
return (
<div>
<p>経過時間: {seconds}秒</p>
<button onClick={handleStart} disabled={isRunning}>
開始
</button>
<button onClick={handleStop} disabled={!isRunning}>
停止
</button>
<button onClick={handleReset}>
リセット
</button>
</div>
);
}
export default Timer;
|
タイマーIDはUIに表示するデータではないため、useStateではなくuseRefで保持します。これにより、タイマーIDの変更時に不要な再レンダリングを防止できます。
実践例3: 前回の値を保持する#
useRefを使って、stateの前回の値を保持するカスタムフックを作成できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import { useState, useRef, useEffect } from 'react';
// 前回の値を返すカスタムフック
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const previousCount = usePrevious(count);
return (
<div>
<p>現在の値: {count}</p>
<p>前回の値: {previousCount ?? '(なし)'}</p>
<button onClick={() => setCount(count + 1)}>
カウントアップ
</button>
</div>
);
}
export default Counter;
|
このパターンは、値の変化を検知して特定の処理を行いたい場合に便利です。
useRefの注意点#
useRefを使う際の重要な注意点を以下にまとめます。
- レンダリング中にref.currentを読み書きしない
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 悪い例: レンダリング中にrefを変更
function BadComponent() {
const ref = useRef(0);
ref.current += 1; // レンダリング中の変更は予測不能な動作を引き起こす
return <div>{ref.current}</div>;
}
// 良い例: イベントハンドラやuseEffect内で変更
function GoodComponent() {
const ref = useRef(0);
const handleClick = () => {
ref.current += 1;
console.log(ref.current);
};
return <button onClick={handleClick}>クリック</button>;
}
|
- UIに表示する値にはuseStateを使う
useRefの変更は再レンダリングを引き起こさないため、UIに表示する値にはuseStateを使用してください。
useMemo - 計算結果のメモ化#
useMemoとは#
useMemoは、計算結果をメモ化(キャッシュ)するフックです。依存配列の値が変更されない限り、前回の計算結果を再利用します。これにより、コストの高い計算処理の再実行を防止できます。
基本構文#
useMemoの基本構文は以下のとおりです。
1
2
3
4
5
6
7
8
9
10
|
import { useMemo } from 'react';
function MyComponent({ items, filter }) {
const filteredItems = useMemo(() => {
// コストの高い計算処理
return items.filter((item) => item.includes(filter));
}, [items, filter]); // 依存配列
return <ul>{filteredItems.map((item) => <li key={item}>{item}</li>)}</ul>;
}
|
useMemoに渡す引数は以下の2つです。
- 計算関数: メモ化したい処理を返す関数
- 依存配列: この配列の値が変更されたときのみ再計算を実行
useMemoが有効なケース#
useMemoは以下のようなケースで効果を発揮します。
-
コストの高い計算処理
- 大量のデータのフィルタリング・ソート
- 複雑な数学的計算
- オブジェクトや配列の変換処理
-
React.memoと組み合わせた子コンポーネントの最適化
- 新しい配列/オブジェクトを子コンポーネントに渡す場合
実践例1: 重い計算処理のメモ化#
フィルタリング処理をメモ化する例を見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
import { useState, useMemo } from 'react';
// 商品データ(実際にはAPIから取得することが多い)
const products = Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
name: `商品 ${i + 1}`,
category: ['電化製品', 'ファッション', '食品', '書籍'][i % 4],
price: Math.floor(Math.random() * 10000) + 100,
}));
function ProductList() {
const [category, setCategory] = useState('');
const [minPrice, setMinPrice] = useState(0);
const [theme, setTheme] = useState('light');
// フィルタリング結果をメモ化
const filteredProducts = useMemo(() => {
console.log('フィルタリング処理を実行');
return products.filter((product) => {
const matchesCategory = !category || product.category === category;
const matchesPrice = product.price >= minPrice;
return matchesCategory && matchesPrice;
});
}, [category, minPrice]); // themeは依存配列に含めない
return (
<div className={theme}>
<div>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
>
<option value="">すべてのカテゴリ</option>
<option value="電化製品">電化製品</option>
<option value="ファッション">ファッション</option>
<option value="食品">食品</option>
<option value="書籍">書籍</option>
</select>
<input
type="number"
value={minPrice}
onChange={(e) => setMinPrice(Number(e.target.value))}
placeholder="最低価格"
/>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
テーマ切り替え
</button>
</div>
<p>検索結果: {filteredProducts.length}件</p>
<ul>
{filteredProducts.slice(0, 20).map((product) => (
<li key={product.id}>
{product.name} - {product.category} - {product.price}円
</li>
))}
</ul>
</div>
);
}
export default ProductList;
|
この例では、themeの変更時にはフィルタリング処理は再実行されません。useMemoがcategoryとminPriceが変更されたときのみ再計算を行うためです。
実践例2: React.memoとの組み合わせ#
useMemoはReact.memoと組み合わせることで、子コンポーネントの不要な再レンダリングを防止できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
import { useState, useMemo, memo } from 'react';
// React.memoでラップした子コンポーネント
const ItemList = memo(function ItemList({ items }: { items: string[] }) {
console.log('ItemListがレンダリングされました');
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// useMemoを使わない場合、countが変わるたびに新しい配列が作られる
// const items = text.split(',').filter(Boolean);
// useMemoで配列をメモ化
const items = useMemo(() => {
return text.split(',').filter(Boolean);
}, [text]);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>
カウントアップ
</button>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="カンマ区切りで入力"
/>
<ItemList items={items} />
</div>
);
}
export default ParentComponent;
|
useMemoを使用することで、textが変更されない限り同じ配列参照が維持されます。これにより、ItemListコンポーネントの不要な再レンダリングを防止できます。
useMemoの注意点#
useMemoを使う際の注意点を以下にまとめます。
- パフォーマンス最適化が本当に必要か検討する
useMemoの使用自体にもオーバーヘッドがあります。単純な計算に対してuseMemoを使うと、かえってパフォーマンスが低下する可能性があります。
1
2
3
4
5
|
// 不要なuseMemo: 単純な計算にはuseMemoは必要ない
const doubled = useMemo(() => count * 2, [count]);
// これで十分
const doubled = count * 2;
|
- 依存配列を正しく設定する
依存配列に必要な値を含め忘れると、古い値が使われるバグが発生します。
- オブジェクトを返す場合の構文に注意
アロー関数でオブジェクトを返す場合は、括弧で囲む必要があります。
1
2
3
4
5
6
7
8
9
10
|
// 誤り: オブジェクトを返せない
const options = useMemo(() => { serverUrl: 'localhost', port: 3000 }, []);
// 正しい: 括弧で囲む
const options = useMemo(() => ({ serverUrl: 'localhost', port: 3000 }), []);
// または明示的にreturn
const options = useMemo(() => {
return { serverUrl: 'localhost', port: 3000 };
}, []);
|
useCallback - 関数のメモ化#
useCallbackとは#
useCallbackは、関数定義をメモ化するフックです。依存配列の値が変更されない限り、同じ関数参照を返します。useMemoが「計算結果」をメモ化するのに対し、useCallbackは「関数そのもの」をメモ化します。
基本構文#
useCallbackの基本構文は以下のとおりです。
1
2
3
4
5
6
7
8
9
10
11
12
|
import { useCallback } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
// 関数をメモ化
const handleClick = useCallback(() => {
setCount((prev) => prev + 1);
}, []); // 依存配列が空なので、関数は再生成されない
return <button onClick={handleClick}>クリック</button>;
}
|
useCallbackに渡す引数は以下の2つです。
- メモ化したい関数
- 依存配列: この配列の値が変更されたときのみ関数を再生成
useMemoとuseCallbackの関係#
実は、useCallbackはuseMemoの特殊なケースです。以下の2つは同等の動作をします。
1
2
3
4
5
6
7
8
9
10
11
|
// useCallbackを使用
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
// useMemoで同等の処理
const handleClick = useMemo(() => {
return () => {
doSomething(a, b);
};
}, [a, b]);
|
useCallbackは関数をメモ化する際に、より簡潔に書けるシンタックスシュガーです。
useCallbackが有効なケース#
useCallbackは以下のケースで効果を発揮します。
- React.memoでラップした子コンポーネントに関数を渡す場合
- useEffectの依存配列に関数を含める場合
- カスタムフックから関数を返す場合
実践例1: 子コンポーネントの再レンダリング防止#
React.memoと組み合わせて、子コンポーネントの不要な再レンダリングを防止する例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
import { useState, useCallback, memo } from 'react';
// React.memoでラップしたボタンコンポーネント
const ActionButton = memo(function ActionButton({
onClick,
children,
}: {
onClick: () => void;
children: React.ReactNode;
}) {
console.log(`ActionButton "${children}" がレンダリングされました`);
return <button onClick={onClick}>{children}</button>;
});
function Counter() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// useCallbackでメモ化しない場合、textが変わるたびに
// 新しい関数が作られ、ActionButtonが再レンダリングされる
const handleIncrement = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
const handleDecrement = useCallback(() => {
setCount((prev) => prev - 1);
}, []);
const handleReset = useCallback(() => {
setCount(0);
}, []);
return (
<div>
<p>カウント: {count}</p>
<ActionButton onClick={handleIncrement}>+1</ActionButton>
<ActionButton onClick={handleDecrement}>-1</ActionButton>
<ActionButton onClick={handleReset}>リセット</ActionButton>
<div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="テキストを入力(ボタンに影響しない)"
/>
</div>
</div>
);
}
export default Counter;
|
この例では、textを入力してもActionButtonコンポーネントは再レンダリングされません。useCallbackによって関数参照が維持されているためです。
実践例2: useEffectの依存配列との組み合わせ#
useCallbackは、useEffectの依存配列に関数を含める場合にも有効です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
import { useState, useEffect, useCallback } from 'react';
function SearchComponent({ query }: { query: string }) {
const [results, setResults] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
// 検索関数をメモ化
const fetchResults = useCallback(async () => {
if (!query) {
setResults([]);
return;
}
setIsLoading(true);
try {
// 実際にはAPIを呼び出す
await new Promise((resolve) => setTimeout(resolve, 500));
const mockResults = [
`${query}に関する結果1`,
`${query}に関する結果2`,
`${query}に関する結果3`,
];
setResults(mockResults);
} finally {
setIsLoading(false);
}
}, [query]); // queryが変わったときのみ関数を再生成
useEffect(() => {
fetchResults();
}, [fetchResults]); // fetchResultsが変わったときのみ実行
return (
<div>
{isLoading ? (
<p>検索中...</p>
) : (
<ul>
{results.map((result, index) => (
<li key={index}>{result}</li>
))}
</ul>
)}
</div>
);
}
export default SearchComponent;
|
ただし、この例では関数をuseEffect内に直接定義するほうがシンプルです。
1
2
3
4
5
6
7
|
// よりシンプルな書き方
useEffect(() => {
const fetchResults = async () => {
// 処理
};
fetchResults();
}, [query]);
|
useCallbackが必要になるのは、関数を複数の場所で使用する場合や、子コンポーネントに渡す場合です。
実践例3: カスタムフックでの使用#
カスタムフックから関数を返す場合は、useCallbackでラップすることが推奨されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
import { useState, useCallback } from 'react';
// トグル機能を提供するカスタムフック
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue((prev) => !prev);
}, []);
const setTrue = useCallback(() => {
setValue(true);
}, []);
const setFalse = useCallback(() => {
setValue(false);
}, []);
return { value, toggle, setTrue, setFalse };
}
// 使用例
function Modal() {
const { value: isOpen, toggle, setFalse: close } = useToggle(false);
return (
<div>
<button onClick={toggle}>モーダルを開く/閉じる</button>
{isOpen && (
<div className="modal">
<div className="modal-content">
<p>モーダルの内容</p>
<button onClick={close}>閉じる</button>
</div>
</div>
)}
</div>
);
}
export default Modal;
|
カスタムフックから返す関数をuseCallbackでラップすることで、そのフックを使用するコンポーネントの最適化が容易になります。
useCallbackの注意点#
useCallbackを使う際の注意点を以下にまとめます。
- すべての関数にuseCallbackは不要
React.memoを使っていない子コンポーネントに渡す関数や、DOMイベントハンドラには、useCallbackは通常不要です。
1
2
3
4
5
6
7
8
9
10
11
|
// useCallbackは不要なケースが多い
function SimpleComponent() {
const [count, setCount] = useState(0);
// 単純なイベントハンドラにはuseCallbackは不要
const handleClick = () => {
setCount(count + 1);
};
return <button onClick={handleClick}>クリック</button>;
}
|
- 依存配列を正しく設定する
関数内で使用する外部の値は、すべて依存配列に含める必要があります。
1
2
3
4
|
const handleSubmit = useCallback(() => {
// userIdを使用しているので依存配列に含める
submitData(userId, formData);
}, [userId, formData]);
|
- updater関数を活用する
stateを依存配列から除外するために、updater関数を使用できます。
1
2
3
4
5
6
7
8
9
|
// 悪い例: countが依存配列に含まれる
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
// 良い例: updater関数を使用して依存配列を空に
const handleClick = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
|
3つのHooksの使い分けガイド#
フローチャートで理解する使い分け#
以下のフローチャートで、どのフックを使うべきか判断できます。
-
DOM要素を操作したい、またはレンダリングに影響しない値を保持したい
-
計算コストの高い処理結果をキャッシュしたい
-
関数をメモ化して子コンポーネントの再レンダリングを防止したい
-
上記に当てはまらない
比較表#
| フック |
目的 |
返り値 |
再レンダリング |
| useRef |
値の保持・DOM参照 |
{ current: 値 } |
変更しても発生しない |
| useMemo |
計算結果のメモ化 |
計算された値 |
依存配列の変更時に再計算 |
| useCallback |
関数のメモ化 |
メモ化された関数 |
依存配列の変更時に再生成 |
よくある間違いと正しい使い方#
間違い1: すべてをメモ化する#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// 過剰なメモ化は避ける
function OverOptimized() {
const [count, setCount] = useState(0);
// 不要: 単純な計算
const doubled = useMemo(() => count * 2, [count]);
// 不要: シンプルなハンドラ
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);
return <button onClick={handleClick}>{doubled}</button>;
}
// シンプルに書く
function Simple() {
const [count, setCount] = useState(0);
const doubled = count * 2;
return (
<button onClick={() => setCount((c) => c + 1)}>
{doubled}
</button>
);
}
|
間違い2: 依存配列の設定ミス#
1
2
3
4
5
6
7
8
9
10
11
|
// 悪い例: 依存配列にuserIdが含まれていない
const fetchUser = useCallback(async () => {
const response = await fetch(`/api/users/${userId}`);
setUser(await response.json());
}, []); // バグ: userIdが変わっても古い値が使われる
// 良い例
const fetchUser = useCallback(async () => {
const response = await fetch(`/api/users/${userId}`);
setUser(await response.json());
}, [userId]);
|
間違い3: useRefとuseStateの混同#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 悪い例: UIに表示する値にuseRefを使用
function BadCounter() {
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1;
// UIは更新されない!
};
return <p>カウント: {countRef.current}</p>;
}
// 良い例: UIに表示する値にはuseStateを使用
function GoodCounter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((c) => c + 1);
};
return <p>カウント: {count}</p>;
}
|
React Compilerと将来の展望#
React 19では「React Compiler」が導入され、useMemoやuseCallbackの手動メモ化が自動化される方向に進んでいます。将来的には、開発者がこれらのフックを明示的に書く必要性が減る可能性があります。
しかし、現時点では以下の理由から、これらのフックの理解は重要です。
- 既存のコードベースで広く使用されている
- React Compilerはまだ実験段階
- メモ化の仕組みを理解することで、パフォーマンス問題のデバッグが容易になる
まとめ#
本記事では、useRef・useMemo・useCallbackの3つのフックについて解説しました。それぞれのポイントを振り返ります。
useRef#
- DOM要素への参照やミュータブルな値の保持に使用
- 値を変更しても再レンダリングは発生しない
- タイマーID、前回の値の保持などに活用
useMemo#
- 計算結果をメモ化してパフォーマンスを最適化
- 依存配列の値が変更されたときのみ再計算
- 重い計算処理やReact.memoとの組み合わせで効果を発揮
useCallback#
- 関数定義をメモ化
- React.memoと組み合わせて子コンポーネントの再レンダリングを防止
- カスタムフックから関数を返す際に推奨
これらのフックを適切に使い分けることで、機能性とパフォーマンスを両立したReactアプリケーションを構築できます。
次に読む記事#
次の記事では、外部APIとの連携方法について解説します。fetch/axiosを使ったデータ取得、ローディング・エラー状態の管理など、実践的なWebアプリ開発に必要な知識を学びましょう。
- 次回: ReactでのAPI連携 - fetch/axiosを使ったデータ取得
サンプルコード#
本記事で紹介したサンプルコードは、以下のオンライン環境で動作を確認できます。
参考リンク#