はじめに
未経験でエンジニアになり、業務でよく見るReactのHooksについて、Hooksとはから始まり、カスタムHooksとはよくわかってなかったので今回勉強し、まとめました。
まだ理解が浅く、説明が不十分だったり誤りがある部分もあるかと思いますが、お気づきの点があればぜひご指摘いただけると嬉しいです。
対 象 読 者
未経験でエンジニアになった方へ
この記事で学べること
- React Hooksの基本概念
- Hooksの使い方
Hooksとは
概要
フック (hook) は 2019年にReact 16.8 で追加された新機能です。
useから始まる関数をフック(Hooks)と呼ばれる機能のことを指します。
フックとは
昔の書き方(クラスコンポーネント)は、複雑な設計図を最初から全部書かないといけない設計でした。難しくて時間もかかります。
でも2019年に登場したフックという機能のおかげで、シンプルな作り方(関数コンポーネント)のままでも、状態管理などの機能をクラスを書かずに使えるようになりました。
たとえば:
-
useStateは関数コンポーネントの状態を保持したり、更新したりするためのフック -
useEffectは外部システムとコンポーネントを同期させるためのReactフック
関数コンポーネントとは
関数が画面に何を表示するかを返す
return形で書くのが関数コンポーネント
つまりReact Hooksは、シンプルな作り方(関数コンポーネント)に、必要な機能を後から取り付けられるようにした、便利な機能です
フックのルール
フックはトップレベルでのみ呼び出す
ループ、条件分岐、ネストされた関数、try/catch/finally ブロックの内部でフックを呼び出してはいけません。代わりに、フックは常に React 関数のトップレベルで、早期 return を行う前に呼び出します。フックは React が関数コンポーネントをレンダーしている間にのみ呼び出すことができます。と公式に記載があります。
なぜダメなのか
Reactは、1回目のフック呼び出しはこの状態、2回目のフック呼び出しはこの状態…というふうに、呼ばれた順番で記憶しています。
もしif文の中に入れてしまうと、レンダリングのたびに呼ばれる時と呼ばれない時ができてしまい、Reactが混乱してしまい、毎回のレンダリングで必ず同じ順番・同じ回数呼ばれるトップレベルに置く必要がある、というルールになってます。
このルールについては公式ドキュメントで詳しく書いてあるのでぜひ読んでみてください。
- ✅ 関数コンポーネント本体のトップレベルで呼び出す。
- ✅ カスタムフック本体のトップレベルで呼び出す。
トップレベルとは
if文やfor文、関数の中など、何かの中に入れ子になっていない、一番外側の場所
Hooksの紹介(コード例つき)
useMemo
レンダー間で計算結果をキャッシュするための React フック
AIにわかりやすく言い換えてもらうと、計算した答えをとっておくメモとのこと。
難しい計算を毎回イチからやり直すのは大変なので、一度出した答えをメモしておいて、値が変わっていないときはメモを見るだけで済ませれます。
どんな時に使うの?
// 基本的な使い方
import { useMemo } from 'react';
useMemo(() => メモ化したい値を計算する関数, [依存配列]);
① 本当に重い計算を、無駄にやり直したくないとき
② オブジェクトや配列を他の処理に渡すとき(useEffectなど)
例えば:
// 毎回再レンダリングのたびに新しいオブジェクトが作られる
const options = { color, size };
useEffect(() => {
applyStyles(options);
}, [options]);
optionsが毎回新しいオブジェクトだと、useEffectはオブジェクトが変わった、と判断し、color, sizeの値が本当は同じでも、毎回律儀に動きます。
つまり、無駄な動作です。
なので、useMemoで
const options = useMemo(() => ({ color, size }), [color, size]);
// colorとsizeが変わらないときはこのuseEffectは処理されない
useEffect(() => {
applyStyles(options);
}, [options]);
colorとsizeが変わらない限り、更新せず同じオブジェクトを使い回すので
useEffectも同じオブジェクトだと正しく判断できるようになり、無駄な動作がなくなります。
注意点
- 常に使う必要はない
- 前回と比較する処理がかかるので軽い計算にまで使用するとメモの管理コストの方が高くつく
管理コストとは
const total = useMemo(() => a + b, [a, b]);
- 前回の
aとbの値を、どこかに保存しておく - 今回レンダリングされた時、保存しておいた
a・bと、今回のa・bを1つずつ比較する - 同じなら、保存しておいた計算結果を取り出して返す
- 違ったら、計算をやり直して、新しい
a・bと結果を保存し直す
一方、useMemoを使わない場合
const total = a + b;
これだけで済みます!
つまり、useMemoを使うと、
- 前回の値を覚えておく
- 比較する
- 保存し直す
という足し算そのものより手間のかかる作業が追加で発生するので、管理コストがかかるということです。
- 依存配列を正しく指定する
- 基本ルールは、計算の中で実際に使っている値だけを、過不足なく依存配列に入れること
例えば:
あとからコードを変更した時に、依存配列を正しく指定できてない場合に起こります。
少なすぎる場合(バグになる)
const total = useMemo(() => a + b, [a]); // bが抜けている
bが変わっても再計算されず、古い答えのままになってしまいます。
多すぎる場合(無駄になる)
const total = useMemo(() => a + b, [a, b, c]); // cは計算に使ってない
a + bの計算にcは関係ないのに、cが変わるたびに無駄に再計算されてしまいます。
これは人の手で管理すると見落としやすい部分なので、実務では
eslint-plugin-react-hooksというツールのexhaustive-depsというルールを使い、計算の中で使っている値と、依存配列の中身が一致しているかを自動でチェックすることができ、防ぐことができる
- Strict Modeでは、開発中だけ計算関数が2回呼ばれる
Strict Modeは、開発中だけオンにできる、Reactのバグ発見用チェック機能。useMemoに渡す計算関数は純粋(同じ材料なら必ず同じ答えを返し、外側の値を書き換えない)であることが前提になっているため、Strict Modeはこの関数をわざと2回呼び出して、純粋じゃない書き方をしていないかチェックします。ちゃんと純粋に書かれていれば、2回呼ばれても結果に影響はなし
- キャッシュは基本的には消えないが、絶対的な保証ではない
useCallback
useCallback は、関数の定義をキャッシュするためのフック
AIにわかりやすく言い換えてもらうとお願いカードの使い回しの道具とのこと
関数を、命令のたびに新しく書き直すと、子コンポーネントが前と違う関数と勘違いして、不要に関数を作成します。
なので、useCallback は関数が変わってない限りずっと同じ関数を使って使い回させてくれます。
どんな時に使うの?
// 基本的な使い方
import { useCallback } from 'react';
const cachedFn = useCallback(fn, dependencies)
-
パフォーマンスの向上
- 関数を使い回すことで、無駄な処理を減らし、アプリの動きを軽くすることが目的
-
関数の参照の一貫性を保つ
Reactでは、コンポーネントの中のstateが変わったり、親から渡されたpropsが変わったりして再レンダリングが起きるたびに、そのコンポーネントのコードは関係あるなしに関わらず1から実行し直されます。
そのため、コンポーネントの中で定義した関数も、再レンダリングのたびに新しく作り直されてしまいます。useCallbackを使うと、依存配列の中身が変わらない限り、新しく作り直さずに前回と同じ関数をそのまま使い回してくれます。
これが関数の参照の一貫性を保つということです -
関数を他の処理に渡すとき(useEffectなど)
例えば:
const applyStyles = (opts) => {
element.style.color = opts.color;
element.style.fontSize = opts.size;
};
useEffect(() => {
applyStyles(options);
}, [applyStyles]);
applyStylesが毎回新しい関数だと、useEffectは関数が変わったと判断し、中身は同じでも毎回処理をする。これが無駄な動作につながります。
なので、useCallbackで
// applyStylesに渡す値
const options = { color, size };
const applyStyles = useCallback((opts) => {
element.style.color = opts.color;
element.style.fontSize = opts.size;
}, []); // applyStyles自体はcolor, sizeを直接参照していないので、空配列でOK
// colorとsizeが変わらないときはこのuseEffectは処理されない
useEffect(() => {
applyStyles(options);
}, [applyStyles, color, size]);
colorとsizeが変わらない限り、同じ関数を使い回してねとお願いすることにより、
useEffectも同じ関数だ、と正しく判断できるようになり、無駄な動作がなくなります。
useMemoとuseCallbackって同じでは…?
実はほぼ同じ仕組みで、公式ドキュメントにも「useCallback(fn, deps)はuseMemo(() => fn, deps)と同じ」と書かれています。
違いは何をキャッシュするかだけです。
useMemo:関数を実行して、その"結果"をキャッシュする
const value = useMemo(() => {
return a + b; // この計算を実行して
}, [a, b]);
// value には計算した答え(数値やオブジェクトなど)が入る
useCallback:関数を実行せず、"関数そのもの"をキャッシュする
const fn = useCallback(() => {
return a + b; // この関数は実行されない
}, [a, b]);
// fn には実行できる関数そのものが入る
useMemoは中の処理をその場で実行して結果を受け取るのに対して、
useCallbackは中の処理を実行せずに関数自体をそのまま受け取る、というのが違いです。
なので実はuseCallback(fn, deps)は、fnを返す関数をuseMemoにかけているのと同じことになります。
useCallback(fn, deps)
// ↓ 中身的にはこれと同じ
useMemo(() => fn, deps)
ただ未経験からするとこの違いはわかっても、いざ実務で使い分けができるか不安です。
実際使われているコードを見て、自分の中で使い分けを理解していきたいと思います。
注意点
基本的な考え方はuseMemoと同じです。
- 必要なときだけ使う
-
useCallback自体にも依存配列を比較するコストがかかるので、軽い処理の関数にまで使うと、逆にコストの方が高くつくことがあります。
-
- 依存配列を正しく指定する
- 関数の中で使っている値を、過不足なく依存配列に入れる必要があります。少なすぎると古い値を使い続けるバグに、多すぎると無駄な再生成につながるのは
useMemoと同じです。
- 関数の中で使っている値を、過不足なく依存配列に入れる必要があります。少なすぎると古い値を使い続けるバグに、多すぎると無駄な再生成につながるのは
まとめ
本当であればまだ他のHooksの紹介をしたかったんですが、長くなりそうなので一旦ここで区切ります。
続きでは他のHooksの紹介と・カスタムHooksについてまとめていけたらなと思っています。
参 考 文 献
https://ja.react.dev/reference/react/hooks
https://ja.react.dev/reference/rules/rules-of-hooks
https://ja.react.dev/learn/reusing-logic-with-custom-hooks
続きの記事はこちら
未経験エンジニアがReact Hooksを一から勉強してみた【useRef・useLayoutEffect・カスタムフック編】