こんにちは!Reactの学習、楽しく進めていますか?
「画面に文字が出た!」「ボタンが動いた!」という感動を味わったあと、少しずつアプリが複雑になってくると、必ずと言っていいほどぶつかる壁があります。
それが「あれ? なんかこのコンポーネント、関係ないはずなのに何度も再レンダリングされてる……?」という現象です。
今回は、そんなモヤモヤをスッキリ解消してくれる魔法のフック、`useCallback` について、身近な例え話を取り入れながら優しく紐解いていきたいと思います。
「難しい言葉は苦手だな……」という方も大丈夫ですよ。一歩ずつ、のんびり見ていきましょう!
—
1. なぜ子コンポーネントは「余計な再レンダリング」をしてしまうのか?
まずは、Reactが画面を新しく描画(再レンダリング)する仕組みの「お約束」を知ることから始めましょう。
Reactの世界では、親コンポーネントが再レンダリングされると、その中にいる子コンポーネントたちも「とりあえず全員もう一回描き直そう!」と巻き添えを食らいます。
これ自体はReactの仕様なので仕方ないのですが、問題は「親から子へ渡している関数」の存在です。
身近な例え:毎回作り直される「特製クーポン」
ここに、お母さん(親コンポーネント)と、お手伝いをしてくれる子ども(子コンポーネント)がいるとします。
お母さんは、子どもにお使いを頼むとき、毎回メモ帳にこう書きます。
「このクーポンを持っていったら、お駄賃をあげるね」
たとえお駄賃の金額が1円も変わっていなくても、お母さんがメモ帳を新しく書き換えた(=JavaScriptが新しく関数オブジェクトを作り直した)場合、子どもから見るとどう見えるでしょうか?
子どもはこう思います。
「あれ? さっきもらったメモと、インクの匂いや紙の質感が違うぞ? これは新しいお仕事の依頼に違いない!」
結果、子どもは毎回「新しい指示が来た!」と勘違いして、無駄にキョロキョロしたり、準備をやり直したりしてしまいます。これが、Reactでよくある「関数が新しくなったと勘違いして子コンポーネントが再レンダリングされる現象」の正体です。
—
2. 救世主 `useCallback` の登場!
この「毎回メモを新しく作り直してしまう問題」を解決するのが、`useCallback` です。
`useCallback` は、いわば「一度書いたメモをパウチ(ラミネート加工)して、中身が変わらない限り使い回す仕組み」です。
お母さんが「このお使いのルールは変わらないから、このパウチされたクーポンをずっと使ってね」と子どもに渡せば、子どもは「あ、いつものおなじみのクーポンだね!」と安心して、余計な動きをしなくなります。
`useCallback` の基本的な使い方
実際のコードを見てみましょう。と言っても、書き方はすごくシンプルです。
import React, { useState, useCallback } from ‘react’;
// 子コンポーネント(React.memoでメモ化しておくのがポイントです)
const ChildButton = React.memo(({ onClick }) => {
console.log(‘子コンポーネントが再レンダリングされました!’);
return ;
});
function ParentComponent() {
const [count, setCount] = useState(0);
// useCallbackを使って、関数をメモ化(安定化)する
const handleClick = useCallback(() => {
console.log(‘ボタンがクリックされました’);
}, []); // 依存配列が空なので、この関数はずっと同じものを使い回します
return (
親のカウンター: {count}
{/ 安定した関数を子コンポーネントに渡す /}
);
}
このコードでは、親のカウンター(`count`)をポチポチと増やしても、`ChildButton` のコンソールには「子コンポーネントが再レンダリングされました!」というログは流れません。
なぜなら、`useCallback` のおかげで `handleClick` というメモがずっと同じもので守られているからです!
—
3. 「依存配列」というお友達のお話
`useCallback` を使う上で、一番つまずきやすいのが第2引数にある「依存配列(`[]` の部分)」です。
ここを正しく理解しておかないと、「あれ? 動かないぞ?」というバグに繋がってしまうので、優しく解説しますね。
ルール:外の世界の変数を使うときは、お母さんに教えてあげよう
先ほどの例では、配列の中身が空っぽ(`[]`)でした。これは「この関数の中で、外から持ってきた特別な変数を使っていないよ」という意味です。
では、もしボタンを押したときに「親が持っている最新のカウント数」を使いたくなったらどうでしょう?
const [count, setCount] = useState(0);
// NGな例:countが変わっても、古いcountの値を掴み続けてしまうことがある
const handleBadClick = useCallback(() => {
console.log(`現在のカウントは ${count} です`);
}, []); // 空っぽにすると、countが更新されても古い数字のまま記憶されちゃいます!
ここで依存配列の登場です。「この変数(`count`)が変わったときは、メモを新しく作り直してね!」と、Reactにお願いする場所がここです。
// OKな例:countが変わったときだけ、新しいメモに書き換える
const handleGoodClick = useCallback(() => {
console.log(`現在のカウントは ${count} です`);
}, [count]); // ここに count を入れる!
これで、`count` が変わったときだけ関数が新しくなり、それ以外のとき(例えば親の別の部分が変わったときなど)は古いメモを使い回すという、絶妙なバランスが取れるようになります。
—
4.ちょっと待って! なんでもかんでも `useCallback` すればいいの?
ここまで読むと、「じゃあ、アプリ内のすべての関数に `useCallback` をつければ最強じゃん!」と思われるかもしれません。
ですが、現場のプロからちょっとしたアドバイスです。実は、それは逆効果になることがあります。
豆知識:メモ化にもコストがかかる
`useCallback` は、「Reactにメモを記憶させておくための管理コスト」を常に支払っています。
すごくシンプルな子コンポーネントや、処理が軽い関数に対してすべて `useCallback` を貼ってしまうと、「メモを管理する手間」のほうが大きくなって、かえってアプリの動作が重くなる原因になります。
使うべき目安:
1. その関数を渡す先の子コンポーネントが、`React.memo` で最適化されている場合
2. その関数が、別のフック(`useEffect` など)のトリガーになっていて、無限ループを防ぎたい場合
「まずは普通に書いてみて、パフォーマンスが気になるところ(リストが大きい場所や、重い描画がある場所など)だけ `useCallback` を導入する」というスタンスでまったく問題ありませんよ!
—
まとめ
- `useCallback` とは?
関数を毎回新しく作り直さず、パウチして使い回す(安定化させる)ためのもの。
- 何が嬉しいの?
子コンポーネントが「あれ、新しい指示かな?」と勘違いして、無駄に再レンダリングするのを防げる。
- 注意点
関数の中で使っている変数があるときは、依存配列(`[変数名]`)にちゃんと教えてあげよう!
なんでもかんでも貼ればいいわけではないので、必要な場所にしぼって使おう。
最初は「おまじない」のように感じるかもしれませんが、仕組みが分かるとReactの挙動をコントロールできるようになって、とっても楽しくなってきます。
あなたのReact開発が、少しでも軽やかで楽しいものになりますように。
つまずいたときは、いつでもまたこのブログに帰ってきてくださいね。応援しています!

コメント