Reactでは、関数コンポーネントでさまざまなライフサイクルを実現するためのフックがたくさん組み込まれておりますが、その中にuseMemoというフックがあります。
Reactの初心者でなければ、useMemoおよびuseCallbackフックには既に馴染みがあるかと思います。
useMemoおよびuseCallbackフックの使い道はいくつかの非常に特殊で具体的なケースに限られます。
負荷の高い計算によってアプリのパフォーマンスが低下していることに気付く確率は10%も満たないでしょう。
これらフックに関係なく、実行されるすべてのコード行にはコストがかかります。
事前に最適化をしたいだけかもしれません。
しかし、ほとんどの場合は私たち(開発者)はそれらを使って不必要なものをラップしています。
useMemoとuseCallbackのこれらの実際の目的は何なのか、そしてそれらを適切に使用する方法と仕組みについて解説していきます。
メモ化の概念とは?
ほとんどの場合、関数型プログラミング言語でメモ化に遭遇します。
ただし、この手法には幅広い用途があります。
JavaScriptでは、一部の関数はメモ化できないことに注意が必要です。
プログラミングにおいて、メモ化はアプリケーションをより効率的に、そしてより高速にする最適化技術であり、計算結果をキャッシュに保存し、同じ情報が必要になったときに、再度計算する代わりにキャッシュから取り出すことで、高速化を実現するための技術です。
つまりメモ化とは、関数から返された値または関数自体を保存しておき、送信された値が変更されない場合は再度実行しないという考え方の概念となります。
アルゴリズムについて精通している方であれば、メモ化は動的計画法(DP)の問題に対する一般的な戦略の1つです。
複雑な問題をより単純な部分の問題に分解することによって解決する方法です。
メモ化は、主にコンピュータープログラムを高速化するために使用される最適化手法となっています。
useMemoフックとは?
このフックは、Reactアプリケーションで値をメモ化するために使用します。
Reactでのメモ化でも、計算された値をメモリに保存して再計算を回避する最適化手法です。
この処理によって再計算が指定されるまでは計算が一度だけ実行されることになり、プログラムの実行が高速化されます。
useMemoフックは、Reactコンポーネントの計算値をメモして、コンポーネントの再レンダリング時に再計算されないようにします。
useCallbackフックとは?
このフックはuseMemoと非常によく似ていますが、関数をキャッシュに保存します。
useMemoを使ってもできますが、構文は useCallbackの方が少し簡単となります。
ではいつ使用するべきなのか?
キャッシュされたコンポーネントにPropsとして異なる関数を渡すと、不要な再レンダリングが発生することになります。
useCallbackフックを使用すると、コンポーネントが初回レンダリングされたときだけ関数を作成します。
コンポーネントが再レンダリングされるときには、キャッシュから関数を取得するだけで、今度は同じ関数になり、子コンポーネントでの再レンダリングはトリガーされません。
useMemoとuseCallbackが必要な理由
単刀直入に言いますと、それは再レンダリング間のメモ書きとなります。
値または関数がこれらのフックにラップされている場合、Reactは初回のレンダリング時にそれをキャッシュして、連続したレンダリング時に保存された値への参照を返します。
このフックを使用しない場合、配列、オブジェクト、関数などの非プリミティブ値が再レンダリングのたびに最初から再作成されます。
メモ化は、それらの値を比較するときに非常に役立ちます。
つまり、Reactコンポーネントの一部の計算値は、更新される状態に依存するため状態が変化した際に、その値の再計算によって更新されたデータが取得されることが期待されます。
ただし、一部の計算値は異なる状態に依存する場合があり、これはそのような値を常に再計算する必要がないことを意味しています。
これらの2つのフックは、初心者には混乱を招く可能性があります。
どちらも関数と依存関係の配列を引数として受け取り、依存関係の1つが変更された場合にのみ新しい値を計算します。
この2つの違いは、useMemoは渡された関数の結果であるメモ化された値を返し、useCallbackはメモ化された関数そのものを返すことです。
では、それらをいつ、どのように使用するかを見ていきましょう。
このuseMemoフックは、useEffectフックとほぼ同じ挙動をします。
const catsValue = useMemo(() =>
highCostQueryCall());
つまり、2番目の引数に空の配列[ ]を渡さない場合、useMemoは更新時にトリガーされます。
const catsValue = useMemo(() => highCostQueryCall(), []);
ですのでフックを再度トリガーする場合では、その空の配列にいくつかの依存関係を追加します。
const catsValue = useMemo(() => highCostQueryCall(sample1, sample2), [sample1, sample2 ]);
上記の配列内のこれらの依存関係のいずれかの値が変更されると、useMemoは再トリガーされ、新しい値がメモ化された値として保存される事になります。
下記の典型的なReactのユースケースを確認下さい。
const App = () => { const sampleValue = { test: 1 }; useEffect(() => { // sampleValueは再レンダリング時に比較される }, [sampleValue]); // code… };
sampleValueはuseEffectフックの依存値となっています。
App内で定義されたオブジェクトなので、再レンダリングのたびにゼロから再作成されます。
これは再レンダリング前と再レンダリング後の比較は falseを返し、再レンダリングのたびにuseEffectがトリガーされていきます。
これを避けるために、ここでuseMemoの出番という事です。
useMemoフックでsampleValueの値をラップしてあげます。
const App = () => { // 再レンダリング間でsampleValueの参照を保持する const sampleValue = useMemo(() => ({ test: 1 }), []); useEffect(() => { //値が変更された場合のみトリガーされる }, [sampleValue]); // code… };
useMemoの第一引数はコストのかかる操作を含む関数です。
2番目の引数は前述した通り依存関係の配列となります。
依存関係のいずれかの値が変更されると、Reactはキャッシュを削除し、コストのかかるタスクを実行します。
こうする事によってuseEffectフックは、sampleValueの値が実際に変化したときだけ、トリガーされるようになります。
これは、複雑な計算によって値を計算する必要がある場合または高コストのかかるデータベースおよびネットワークを呼び出す必要がある場合があります。
useMemoフックは1 度実行され、値がメモ化された値として保存されます、そして次回にローカル変数を参照すると、値がより速く取得されます。
useCallbackフックも同様です、関数そのものをメモ化するのに便利なだけとなります。
const App = () => { //再レンダリング間で関数を保持する const fetchPosts = useCallback(() => { console.log('データの取得'); }, []); useEffect(() => { //値が変更されたときにのみトリガーされる fetchPosts(); }, [fetchPosts]); // code… };
他の例でも見てみましょう。
const App = () => { const [counter, setCounter] = useState(0); const [state, setState] = useState(false); const handleClick = useCallback(() => { setState(counter % 2 === 0); }, [counter]); useEffect(() => { handleClick() }, [handleClick]); return ( <div className="App"> <button onClick={() => setCounter((event) => event + 1)}> Click me </button> <h2>Counter: {counter}</h2> <h3>output: {state ? " true" : "false"}</h3> <MyChild onClick={handleClick} /> </div> ); }
See the Pen React useCallback Example② by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
最初の引数としてコールバック関数、2番目の引数として依存関係を受け入れるuseCallbackを呼び出す必要があります。
const handleClick = useCallback(() => { setState(counter % 2 === 0); }, [counter]);
依存関係の空の配列を渡すこともできます。
その場合では関数は一度だけ実行されます、そうでない場合は、呼び出すたびに新しい値を返します。
上記では、メモ化される関数と値が変更されるたびにその関数を更新する依存関係の配列の2つの値を受け取ります。
最も重要なポイントは、useMemoおよびuseCallbackの両方は、再レンダリング時のフェーズのみ有効であることです。
初回レンダリング時に役に立たないだけでなく、有害でもあります。
初回レンダリング時に、アプリの動作がわずかですが遅くなります。
アプリケーションのいたるところに多くものレンダリングがある場合、この速度低下は測定可能なレベルにまで達することさえあります。
再レンダリングを防ぐためのPropsのメモ化
useCallbackとuseMemoが本当に役立つことの1つとして最も重要で頻繁に使用されるのは、子コンポーネントに値を渡してProps値をメモ化することです。
つまり、子コンポーネントに値を渡す場合です。
//親コンポーネント const App = () => { const [counter, setCounter] = useState(0); const handleClick = () => { setCounter(counter + 1); }; const onClick = useCallback(() => { console.log("handler"); }, []); console.log("親のレンダリング"); return ( <div className="App"> <button onClick={handleClick}> Increment </button> <h2>{counter}</h2> <Child name={"Taro"} childHandler={onClick} /> </div> ); }
Childコンポーネントは下記のようにします。
const Child = (props) => { console.log("子のレンダリング"); return ( <div><h2>{props.name}</h2></div> ); }
See the Pen React useCallback Child Props Example by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
子コンポーネントの再レンダリングのトリガーは、親コンポーネントが再レンダリングするたびに新しいonClick関数が生成されて子コンポーネントに送信されます。
onClick関数は再レンダリングのたびに生成されるので、ChildではonClickの参照が更新されたことを検知し、Propsの基本的な比較のみによってChildコンポーネントを再レンダリングします。
useCallbackフックは、親コンポーネントがレンダリングされるたびにメソッドが再作成されるのを防ぐのに役立ちます。
const onClick= useCallback(() => { console.log("handler"); }, []);
useCallbackで指定された依存関係が更新されない場合、最初の引数として提供されたコールバックのメモ化された結果が返されます。
Appコンポーネントの再レンダリングのたびにonClickが生成されることはなく、onClickのインスタンスがメモ化されてChildに渡されることになります。
const Child = (callback) => { console.log(callback) return ( <div><h2>Child</h2></div> ); } const App = ({props}) => { const callback = () => { return 'Result'; }; const memoCallback = useMemo(() => (callback), [props]); return ( <div className="App"> <Child callback={memoCallback} /> </div> ); }
コンソールの出力はこちらのcodepenでご確認下さい。
上記の例では、useCallback(() => (...), [props]) は、propsの依存関係が同じである限り、同じ関数のインスタンスを返します。
useMemoフックを使用しメモ化された関数をテストし、子コンポーネントに値を渡します。
下記のAppコンポーネントでは、Propsが同じでもbuttonをクリックするたびにexpensive()関数が呼び出されています。
const App = () => { const [toogle, setToogle] = useState(false); return ( <div> <Child num={1} /> <button onClick={() => setToogle(!toogle)}> {toogle.toString()} </button> </div> ); } const Child = (props) => { const { num } = props; const expensive = (num) => { console.log("高コストな関数の実行"); return num + num; }; const result = useMemo(() => expensive(num), [num]); return <h1>{result}</h1>; }
コンソールの出力はこちらのcodepenで確認可能です
Childは初回のレンダリング中に関数を1 回だけ呼び出し、expensive()の、その後の呼び出しのためにメモリに格納された値を返します。
useMemoフックの依存関係は通常では、親コンポーネントから渡されたPropsに由来しています。
つまり、コンポーネントに渡されたPropsが同じままである限り、メモ化された関数はスキップされます。
メモ化された結果がフックによって返されています。
値がメモ化されていない場合、ページは再レンダリングされていきます。
コンポーネントのPropsをメモ化する、それ以外で渡した場合ではすべてメモリの無駄使いであり、コードを不必要に複雑にします。
それが、useCallbackでラップされているかどうかは関係ありません。
つまり、下記ではhandleClickがメモ化されているかどうかに関係なく、ページは再レンダリングされます。
//親コンポーネント <Child handleClick={handleClick} />
親コンポーネントは、子を見つけ再レンダリングしてしまいます。
これらフックの使用で意識しておかなければいけない事は、PropsのuseCallbackとuseMemoは、それだけでは再レンダリングを防げません。
すべてのPropsとコンポーネント自体がメモ化されて初めて、再レンダリングが防げます。
たった一つの間違いで、すべてが崩れてしまい、これらのフックは役に立たなくなります。
見つけたら削除するように心掛けて下さい。
最適化しすぎに注意
一部のReact開発者がよく行う間違いは、パフォーマンスの問題を防ごうとして、必要のないときにもこれらのフックを乱用してしまう事です。
それによってコードがより複雑になり、場合によってはパフォーマンスが低下することさえあるので、お勧めできません。
これは、パフォーマンスの問題を発見した後に適用すべきものです。
ReactフレームワークであるRemixの共同創設者でもあるプログラマのKent C. Dodds氏も以前にこれらフックの使用について、下記のように述べていました。
パフォーマンスの最適化は無料ではありません。それらには常にコストが伴いますが、そのコストを相殺するメリットが常にもたらされるとは限りません。したがって、責任を持って最適化する必要があります。
useCallbackとuseMemoを使用する場合は、コンポーネントが複雑になることに注意してください。
思い通りに動作しないときは、ボトルネックがどこにあるのかをチェックし、その部分を最適化するように心掛けて下さい。
コードの複雑さが増すだけではございません、useCallbackおよびuseMemoフックが役立つのと同様に、このフックがこれらの値を格納するためにメモリの一部を使用していることも、あなたは理解する必要があります。
メモ化された関数が多ければ多いほど、より多くのメモリが必要になります。
さらに、これらのコールバックを使用するたびに、React内部にはキャッシュされた出力を提供するためにさらに多くのメモリを使用する必要があるコードが多数存在します。
メモ化されていない他の関数では、Reactによって破棄され、メモリが解放されます。
useCallbackおよびuseMemoフックはパフォーマンスの向上に役立ちますが、落とし穴もあるという事です。
パフォーマンス上の利点より欠点が上回る可能性があることを念頭に入れといて下さい。
最後に
ほとんどの場合では、これらの最適化を使用する必要はありません。
そして、すべてにコストがかかり、その場合によりコードが複雑になります。
ですが、それが価値があるかどうかを判断するのはあなた次第となります。
これらフックはReactが提供する優れた機能の一部です。
Reactプロジェクトで最高のパフォーマンスとレンダリング時間の速度を確保するために、特定の使用ケースをすべて考慮する必要があります。
本日は以上となります。
最後まで読んで頂きありがとうございます。