Reactカスタムフックのための実践的なパターン
Lukas Schneider
DevOps Engineer · Leapcell

はじめに
現代のウェブ開発が急速に進化する中で、Reactはリッチでインタラクティブなユーザーインターフェースを作成するための主要なライブラリとしての地位を確立しています。Reactの強力さと柔軟性に大きく貢献したのはHooksの導入であり、これにより関数コンポーネントにステートフルなロジックと副作用を組み込むことが可能になりました。ReactはuseStateやuseEffectのような組み込みHooksを提供しますが、真の魔法はカスタムHooksを作成することにあります。これらのカスタムHooksは、複雑なコンポーネントロジックを抽出し再利用できるようにし、よりクリーンで、保守しやすく、非常にコンポーザブルなコードベースにつながります。この記事では、カスタムReact Hooksを構築するための一般的で非常に実践的なパターンを掘り下げ、useDebounceとuseLocalStorageという2つの広く適用可能な例を通してこれらの概念を例示します。
カスタムHooksのコアコンセプトの理解
具体的な例に入る前に、カスタムHooksの背後にある基本原則について共通の理解を確立しましょう。
カスタムHooksとは?
カスタムHooksは、名前がuseで始まり、他のHooksを呼び出すことができるJavaScript関数です。これらは、コードを重複させることなく、異なるコンポーネント間で再利用可能なロジックをカプセル化します。「use」プレフィックスは、ReactリンターがHooksのルールを強制できるようにする規約であり、HooksがカスタムHooksまたは関数コンポーネントのトップレベルでのみ呼び出されることを保証します。
カスタムHooksの利点:
- ロジックの再利用性: 非ビジュアルロジックの抽出と共有。
- 可読性の向上: コンポーネントをクリーンにし、レンダリングに集中させる。
- 保守性の向上: ロジックを一元化することで、変更の管理が容易になる。
- テスト容易性の向上: 分離されたロジックは、独立したテストが容易になる。
Hooksのルール:
- Hooksはトップレベルでのみ呼び出す: ループ、条件、またはネストされた関数内でHooksを呼び出さないでください。
- HooksはReact関数からのみ呼び出す: React関数コンポーネントまたは他のカスタムHooksからHooksを呼び出してください。
これらの基本的な概念を念頭に置いて、2つの非常に便利なカスタムHooksの構築方法を探りましょう。
カスタムHookパターン1:useDebounceによるステート更新のデバウンス
インタラクティブなUIを構築する際の一般的な課題は、検索バーでのタイピングやウィンドウのリサイズのような頻繁なユーザー入力を処理することです。すべてのキーストロークに対してイベントハンドラやAPIコールを発行すると、パフォーマンスの問題や不要なサーバー負荷につながる可能性があります。「デバウウンス」技術は、一定時間トリガーがない場合に、関数の実行を遅延させることでこれに対処します。
useDebounceが解決する問題
検索入力を想像してみてください。ユーザーが入力すると、検索結果を取得したくなるかもしれません。onChangeイベントごとにAPIコールをトリガーし、ユーザーが急速にタイプした場合、多くの不要なリクエストを行うことになります。デバウウンスは、指定された期間ユーザーがタイピングを一時停止した後でのみ、検索リクエストが送信されることを保証します。
useDebounceの実装
useDebounce Hookは通常、値と遅延を入力として受け取り、その値のデバウンスされたバージョンを返します。
import { useState, useEffect } from 'react'; /** * 値のデバウンスを行うカスタムHook。 * * @param {any} value デバウンスする値。 * @param {number} delay デバウンスされた値を更新するまでのミリ秒単位の遅延。 * @returns {any} デバウンスされた値。 */ function useDebounce(value, delay) { // デバウンスされた値を格納するステート const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { // 指定された遅延後にデバウンスされた値を更新するタイムアウトを設定 const handler = setTimeout(() => { setDebouncedValue(value); }, delay); // タイムアウトが実行される前、またはコンポーネントがアンマウントされる前に // 値または遅延が変更された場合は、タイムアウトをクリアする。 return () => { clearTimeout(handler); }; }, [value, delay]); // 値または遅延が変更された場合にのみ再実行 return debouncedValue; } export default useDebounce;
useDebounceの仕組み
- デバウンスされた値のための
useState:debouncedValueは、valueの安定した、デバウンスされたバージョンを保持します。初期valueで初期化されます。 - デバウンスロジックのための
useEffect:useEffect内で、delayミリ秒後にdebouncedValueを更新するsetTimeoutが設定されます。- クリーンアップ関数:
useEffectはclearTimeoutを呼び出すクリーンアップ関数を返します。これは非常に重要です。valueまたはdelayが変更されるたびに、前のタイムアウトがクリアされ、新しいタイムアウトが設定されます。これにより、valueがdelay期間変更されなかった場合にのみsetDebouncedValueが実行されることが保証されます。
- 依存配列:
[value, delay]は、エフェクトがvalueまたはdelayが変更された場合にのみ再実行されることを保証します。
useDebounceの応用
検索コンポーネントでこのHookをどのように使用できるか見てみましょう:
import React, { useState } from 'react'; import useDebounce from './useDebounce'; // Hookをファイルに保存したと仮定 function SearchInput() { const [searchTerm, setSearchTerm] = useState(''); // デバウンスされた検索用語を使用 const debouncedSearchTerm = useDebounce(searchTerm, 500); // 500msのデバウンス遅延 // debouncedSearchTermが変更されたときに検索を実行するエフェクト useEffect(() => { if (debouncedSearchTerm) { console.log('Performing search for:', debouncedSearchTerm); // 実際のアプリケーションでは、ここでAPI呼び出しを行います // 例: fetch(`/api/search?q=${debouncedSearchTerm}`).then(...) } else { console.log('Search term cleared.'); } }, [debouncedSearchTerm]); // debouncedSearchTermが変更された場合にのみ実行 const handleChange = (event) => { setSearchTerm(event.target.value); }; return ( <div> <input type="text" placeholder="Type to search..." value={searchTerm} onChange={handleChange} style={{ padding: '8px', width: '300px' }} /> <p>Current search term: {searchTerm}</p> <p>Debounced search term: {debouncedSearchTerm}</p> </div> ); } export default SearchInput;
この例では、SearchInputは内部のsearchTermを即座に更新しますが、仮想的な検索操作をトリガーするuseEffectはdebouncedSearchTermにのみ反応し、効率を保証します。
カスタムHookパターン2:useLocalStorageによるステートの永続化
多くのWebアプリケーションでは、ページのリロード間でユーザー設定やデータを永続化する必要があります。localStorageは、この目的のための便利なブラウザAPIです。しかし、コンポーネント内で直接localStorageを操作すると、コードの繰り返しや初期ステートのハイドレーションに関する問題が発生する可能性があります。useLocalStorage Hookは、このプロセスを合理化します。
useLocalStorageが解決する問題
localStorageからの値の保存と取得には、多くの場合、ボイラープレートコードが必要です。JSON.parseとJSON.stringify、エラー処理、そしてコンポーネントの初期ステートが保存された値を正しく反映していることを確認することです。useLocalStorageは、この複雑さを抽象化し、永続的なステート管理を容易にします。
useLocalStorageの実装
このHookは、localStorageのキーと初期値を受け取ります。これは、useStateと同様に、現在の値とそれを更新する関数を返します。
import { useState, useEffect } from 'react'; /** * localStorageでステートを永続化するためのカスタムHook。 * * @param {string} key localStorageに値が格納されるキー。 * @param {any} initialValue localStorageに見つからなかった場合に使用する初期値。 * @returns {[any, (value: any) => void]} 現在のステートとセッター関数を含むタプル。 */ function useLocalStorage(key, initialValue) { // 遅延初期化のために`useState`に関数型アップデートを使用。 // これにより、`localStorage.getItem`が一度だけ呼び出されることが保証されます。 const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); // 格納されたJSONを解析するか、沒有の場合はinitialValueを返す return item ? JSON.parse(item) : initialValue; } catch (error) { // エラーが発生した場合(例:localStorageが利用できない場合)、 // initialValueを返し、エラーをログに記録する。 console.error('Error reading from localStorage:', error); return initialValue; } }); // storedValueが変更されるたびにlocalStorageを更新するためにuseEffectを使用 useEffect(() => { try { window.localStorage.setItem(key, JSON.stringify(storedValue)); } catch (error) { console.error('Error writing to localStorage:', error); } }, [key, storedValue]); // キーまたはstoredValueが変更された場合にエフェクトを再実行 return [storedValue, setStoredValue]; } export default useLocalStorage;
useLocalStorageの仕組み
useStateによる遅延初期化:useStateHookは関数で初期化されます。この関数は、コンポーネントの初期レンダリング中に一度だけ実行されます。- この関数内で、提供された
keyを使用してlocalStorageからアイテムを取得しようとします。 - 取得された文字列を
JSON.parseで解析します。アイテムが見つからない場合やエラーが発生した場合は、initialValueにフォールバックします。これにより、すべてのレンダリングでのlocalStorageアクセスが防止されます。
- 永続化のための
useEffect:- この
useEffectHookは、keyまたはstoredValueが変更されるたびに実行されます。 - 現在の
storedValueを取得し、JSON.stringifyを使用してJSON文字列に変換し、指定されたkeyの下にlocalStorageに保存します。 - 堅牢な操作のためにエラー処理が含まれています。
- この
- 戻り値: Hookは
[storedValue, setStoredValue]を返し、useStateのAPIを模倣します。
useLocalStorageの応用
ダークモードのプリファレンスを切り替えることができるコンポーネントを考えてみましょう:
import React from 'react'; import useLocalStorage from './useLocalStorage'; // Hookを保存したと仮定 function ThemeSwitcher() { // 'isDarkMode'ステートを管理するためにカスタムHookを使用し、 // localStorageに見つからない場合はデフォルトでfalseにする。 const [isDarkMode, setIsDarkMode] = useLocalStorage('isDarkMode', false); const toggleDarkMode = () => { setIsDarkMode(!isDarkMode); // 通常はbodyまたはルート要素にCSSクラスを適用します // document.body.classList.toggle('dark-mode', !isDarkMode); }; useEffect(() => { // 現在のダークモードステートに基づいてスタイルを適用 document.body.style.backgroundColor = isDarkMode ? '#333' : '#FFF'; document.body.style.color = isDarkMode ? '#FFF' : '#333'; }, [isDarkMode]); return ( <div style={{ padding: '20px', border: '1px solid #ccc', margin: '20px' }}> <h2>Theme Switcher</h2> <p>Current theme: {isDarkMode ? 'Dark' : 'Light'}</p> <button onClick={toggleDarkMode}> Toggle to {isDarkMode ? 'Light' : 'Dark'} Mode </button> <p>Refresh the page to see the state persisted!</p> </div> ); } export default ThemeSwitcher;
useLocalStorageを使用すると、useStateを使用するのと同じくらい簡単にisDarkModeステートを管理できますが、その値はブラウザセッション間で自動的に永続化されます。
結論
カスタムReact Hooksは、アプリケーション全体でステートフルなロジックを抽象化、再利用、および整理するためのエレガントで強力なメカニズムを提供します。デバウンスされた制御フローやステートの永続化のようなコアパターンを理解することで、開発者はより効率的で、堅牢で、保守しやすいReactアプリケーションを構築できます。useDebounceとuseLocalStorageの例は、これらのパターンの実用的な応用を示しており、一般的な開発上の課題を大幅に簡素化します。カスタムHooksを採用することは、よりクリーンなコンポーネント、強化された再利用性、そしてより快適な開発者体験につながります。

