ReactでHooks APIが登場したのは2019年2月。現在では当たり前のように使われているHooksですが、みなさんは正しく使いこなせているでしょうか? Hooksの基本的な使い方を振り返り、正しく使えているかの参考にしていただければと思います。
useStateで更新したのにレンダリングされない?
Reactコンポーネントの状態管理を行うのにほぼ必須といえるState。useState
はそんなステートを関数型で使える便利なHooksです。実は、useState
は注意しなくてはいけない点があります。たとえば以下のようなTODOリストを実装したとします。
import React, { useCallback, useState } from "react";
/**
* TODOリストを作成するコンポーネントです。
*/
export const TodoList = () => {
const [lists, setLists] = useState([]);
const [value, setValue] = useState("");
// ボタンをクリックしたとき、TODOリストの配列をひとつ増やすイベントハンドラー
const handleClick = useCallback(() => {
const id = lists.length;
// pushメソッドで破壊的に配列を変更
lists.push({ id, text: value });
setValue("");
}, [lists, value]);
// inputの値をvalueにセットするイベントハンドラー
const handleChange = useCallback(
(e) => {
setValue(e.target.value);
},
[setValue]
);
return (
<div>
<h1>TODO LIST</h1>
<button onClick={handleClick}>タスクを追加</button>
<input value={value} onChange={handleChange} />
{lists.map((list) => (
<div key={list.id}>
list.text
</div>
))}
</div>
);
};
useState
は既存のStateと状態更新関数で渡ってきた新しい値を比較し、違いがあれば更新を行います。ですが、現在のstateの配列にsplice()
やpush()
等の破壊的なメソッドで変更を加えてから渡し直しても画面の再描画が起こりません。
現在の状態を表す配列を直接変更するのではなく、別の配列を新たに生成してuseState
の更新関数に渡すようにすれば正しく更新されます。
// ボタンをクリックしたとき、TODOリストの配列をひとつ増やすイベントハンドラー
const handleClick = useCallback(() => {
const id = lists.length;
// 新しく配列を作成して、更新関数に渡す
const newList = [...lists, { id, text: value, isComplete: false }];
setLists(newList);
setValue("");
}, [lists, value]);
正しく使えてる?useEffectの落とし穴
useEffect
以下は一見問題なさそうに見えますが、非常に危険なコードです。
import React, { useEffect, useState } from 'react';
export const Counter => {
const [count, setCount] = useState(0);
// ステータスが変更されるたびに関数を実行させる。
useEffect(() => {
setCount(count + 1);
}, [count]);
return <div>{count}</div>;
}
useEffect
のコールバック関数でステートを変更しているにもかかわらず、第二引数にステートを渡してしまうと処理が無限ループしてしまいます。
このとき、コンソールには以下のような警告が発生しています。
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn’t have a dependency array, or one of the dependencies changes on every render.
依存関係にある値が更新されるごとにuseEffect
が呼び出されています。上記の関数ではcount
を第二引数にセットしていますが、この値はuseEffect
の関数が実行されるたびに更新されます。そのため、
useEffect
の処理を実行count
の数値を更新count
が更新されたのでuseEffect
が再度実行
…という無限ループに陥ってしまうのです。対策として、useEffect
の第二引数を空にします。第二引数を空にすると、マウント・アンマウント時にしか呼ばれなくなります。
import React, { useEffect, useState } from 'react';
export const Counter => {
const [count, setCount] = useState(0);
// 第二引数を空にする
useEffect(() => {
setCount(count + 1);
}, []);
return <div>{count}</div>;
}
useLayoutEffectは使った方がいい?
現在提供されているHooksには、useEffect
とuseLayoutEffect
という似たようなものがあります。どのようなときに使い分ければよいのでしょうか?
useLayoutEffect
はuseEffect
とほぼ同じ動作を行いますが、副作用として設定しているコールバック関数が同期的に呼び出されるところにおおきな違いがあります。useEffect
はDOMの変更を待たずに非同期で呼び出されるために、コールバック関数の処理が完了する前のDOMがユーザーに一瞬見えてしまうことがあります。それに対しuseLayoutEffect
はDOMの変更を待ってから同期的に処理が行われるため、画面に不整合が生じません。
しかし残念ながらデメリットもあります。同期的に実行されるということは、画面描画完了までの時間が長くなってしまうということです。結果的にどうしてもユーザーへのページ表示が遅くなってしまい、ユーザーの体験を損なってしまいます。どうしても都合が悪い時など以外は useEffect
の方を使ったほうが無難でしょう。
公式ドキュメントでも、基本的にuseEffect
の方を使うことが推奨されています。
また、プロジェクトでサーバーサイドレンダリングを使用している場合は、useEffect
およびuseLayoutEffect
のどちらもJavaScriptのダウンロードが完了するまで動作しないため、注意しましょう。
useMemoとuseCallbackの違い
useMemo
とuseCallback
は両方とも実行する処理を第一引数に、監視対象となる値の配列を第二引数に持ちます。
useMemo
は一度計算した結果を変数に保持してくれる「メモ化」を行います。最初のレンダリングで一度作業を行い、第二引数に渡した依存する値が変化しない限りはキャッシュされたものを返します。それに対してuseCallback
は、第一引数に渡しているコールバック関数をメモ化し、不要な再レンダリングを防いでくれます。
useMemo
は大きなデータ処理に、useCallback
は関数に依存関係を追加して不要なレンダリングを抑制したいときに使用するとよいでしょう。また、useCallback
でメモ化しているコールバック関数は、メモ化しているコンポーネントに渡して利用することで正しく機能します。作成したコンポーネント自身で利用しても意味がないので気をつけましょう。
// NG: メモ化したコンポーネントに渡さず、そのまま利用
const Component = () => {
const handleClick = useCallback(() => { ... }, []);
return (
<button onClick={handleClick}>Click!</button>
);
};
// OK: メモ化したコンポーネントに渡して利用
const Component = () => {
const handleClick = useCallback(() => { ... }, []);
return (
<ChildButtonComponent handleClick={handleClick} />
);
};
const ChildButtonComponent = React.memo(({ handleClick }) => {
return (
<button onClick={handleClick}>Click!</button>
);
});
useRefはDOMアクセスに使うだけ?
ReactにおいてDOMの制御を行いたい時はuseRef
のHooksを使ってref属性を取得し、DOMにアクセスすることが多いです。
const TextInput = () => {
// useRefを使ってrefオブジェクトをReactに渡すと、
// .currentプロパティの中に指定された値を格納します。
const inputRef = useRef(null);
return (
{/* 以下のようにref属性にuseRefで指定した値を渡すと、
DOMに変更があるたびに .current プロパティをDOMノードに設定します。 */}
<input ref={inputEl} type="text" />
);
};
ですが、useRef
はより便利に使う方法があります。
useRef
で作成される.current
プロパティは汎用的なコンテナーとして用意されているもので、書き換え可能かつどのような値でも保持できます。これはクラスでインスタンス変数を使うのと同様の利用ができます。たとえば以下のコードのように、イベントハンドラーの中でインターバルのタイマーをクリアしたいときなどに活用できます。
const Timer = () => {
// タイマーID用ののrefオブジェクトを作成
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// setIntervalの処理
});
// 発行されたタイマーIDをrefオブジェクトに格納
intervalRef.current = id;
});
const handleCancel = () => {
// タイマーのインターバルをリセット
clearInterval(intervalRef.current);
};
return (
<button onClick={handleCancel}>タイマーストップ</button>
);
};
また、useRef
は中身が変更されても、その変更をコンポーネント側に通知することはありません。つまり、useState
で値を変更した時とは違い、画面の再レンダリングが行われません。
無駄なレンダリングを避けたい時や、画面遷移によるコンポーネントの再レンダリング時において、値を保持したいときなどに活用するとよいでしょう。
意外と便利、useDebugValue
その名の通り、useDebugValue
はデバッグに使えるフックです。Reactの開発を進めていくと、基本のHooksを組み合わせた独自のカスタムフックを作成することも多いです。useDebugValue
は、カスタムフックのデバッグ情報をReactの開発者ツール用拡張機能(React Dev Tools)に表示させることができます。useDebugValue
はどのカスタムフックから呼び出されたのか検知し、対応するカスタムフックの横に指定したデバッグ情報を表示します。
React用拡張機能で見ると下の画像のように表示されています。
▼開発者ツールで確認した図
useDebugValue
フックで指定した値がカスタムフックのデバッグ情報として表示されていることがわかります。
まとめ
HooksはReactでのアプリケーション開発において、頻繁に使用するようになりました。ですが、正しく使用しないとパフォーマンスが落ちてしまったり無限ループバグの温床となってしまう可能性もあります。予期せぬ不具合を避けるため、プロジェクトでHooksを使う時はしっかりとルールを定めて使用するように決めておくのもひとつの手です。
公式でeslint-plugin-react-hooksという、Hooksをルールどおり正しく使えているかどうかをチェックするプラグインを提供してくれていますので、これを活用するとよいでしょう。