こんにちは、Ryohei(@ityryohei)です!

モダンなJavaScript開発、特にコンポーネントベースのアーキテクチャでは、UI(表示)のロジックとデータ処理や副作用(ロジック)のロジックを分離することが極めて重要です。この分離を実現し、アプリケーション全体でロジックを再利用可能にする設計パターンが「カスタムフック(Custom Hooks)」の考え方です。

カスタムフックの概念は特定のフレームワークで標準化されましたが、その設計思想は純粋なJavaScriptにおける高度な関数とクロージャに基づいています。複雑な状態管理や外部APIとの通信ロジックをカプセル化し、どのコンポーネントからも簡単に利用できる形に変換します。

本記事では、この設計パターンの概念と利点、再利用可能なロジック設計に不可欠なクロージャの役割、そして実務で頻出するロジックにおける副作用の管理(クリーンアップ)までを徹底解説し、その設計術をご紹介しています。

毎回同じ入力チェックやデータ取得の処理をコンポーネント内に書くのは非効率だけど、ロジックを切り出して再利用するにはどうすればいいんだろう?

上記の疑問にお答えします。

では、解説していきます。


なぜロジックを分離する必要があるのか

コンポーネントベースのアプリケーションが複雑化すると、以下の2つの問題が発生します。

  1. ロジックの重複(Duplication):
    複数のコンポーネントで、フォームの入力値検証、ウィンドウサイズの監視、データフェッチングといった同じ処理ロジックを何度も記述する必要が生じます。
  2. コンポーネントの肥大化(Bloating):
    複雑なロジックがUIの記述と混ざり合い、コンポーネントのコードが長大化し、テストや保守が困難になります。

ステートフル関数(カスタムフック的パターン)は、このロジックをコンポーネントの外に出し、「再利用可能なロジックの単位」としてカプセル化するための設計パターンです。

1. ステートフル関数の定義と特徴

このパターンで利用する関数は、特定の機能を持つプレーンなJavaScript関数であり、以下の特徴を持ちます。

  • 関数であること:
    単なるJavaScript関数として定義されます。
  • 状態とロジックの提供:
    内部で状態(変数)を持ち、その状態を操作する関数や、現在の状態値を外部に提供します。

2. クロージャが独立した状態を創り出す

ステートフル関数の核となる仕組みは、クロージャ(Closure)です。

この関数を呼び出すたびに、そのフック内にある変数や関数は新しい実行コンテキストを持ちます。この実行コンテキストこそが、フックが持つ「状態(State)」の役割を果たします。

  • 関数が実行されるたびに、関数内のローカル変数は初期化されます。
  • 関数が返す内部関数(例: increment())は、関数内で宣言されたローカル変数を記憶し続けます(クロージャ)。

このクロージャの特性により、各コンポーネントが同じステートフル関数を呼び出しても、それぞれが独立した状態を維持できるのです。

ステップ1:シンプルなステートフル関数の設計と再利用

最も基本的な、カウンター機能を例に、ステートフル関数の構造と、コンポーネント間での状態の独立性を見てみましょう。

1. カウンターロジックの分離(useCounter.js

この関数は、内部でカウンターのvalue(状態)を持ち、それを増やすincrement関数(ロジック)を返します。ここでは、慣例として「use」から始める名称とします。

JavaScript

// ステートフル関数の慣例として'use'から始める
function useCounter(initialValue = 0) {
  let value = initialValue; // 内部の状態

  // カウンターを増やすロジック
  const increment = () => {
    // 状態を更新
    value += 1; 
    console.log(`現在の値: ${value}`);
  };

  // 外部に、状態の値とその操作関数を提供する
  return { value, increment };
}

2. 複数のコンポーネントでの利用と状態の独立

この関数を二つの異なるコンポーネントで利用すると、それぞれのコンポーネントが独立したvalueの状態を保持します。

JavaScript

// コンポーネントA
function ComponentA() {
  const { value, increment } = useCounter(100); // 独立した状態1

  // コンポーネントAの処理...
  increment(); // A: 101

  // 戻り値でUIをレンダリング... (レンダリングの詳細は省略)
}

// コンポーネントB
function ComponentB() {
  const { value, increment } = useCounter(50); // 独立した状態2

  // コンポーネントBの処理...
  increment(); // B: 51

  // 戻り値でUIをレンダリング... (レンダリングの詳細は省略)
}

ComponentAComponentBは、同じ関数を呼び出していますが、クロージャの性質により、valueincrementは互いに影響を与えません。

ステップ2:実務で使える「ステートフル関数」設計術と副作用の管理

実務では、ブラウザAPIやデータとの連携ロジックをステートフル関数にカプセル化し、副作用の管理を行います。

1. 外部API連携:ローカルストレージの同期ロジック

ローカルストレージ(localStorage)に値を保存し、その値を自動的に取得・更新するロジックを切り出します。

JavaScript

// ローカルストレージと状態を同期するステートフル関数
function useLocalStorage(key, initialValue) {
  let storedValue;
  
  // 1. 状態の初期値をlocalStorageから読み込む副作用
  try {
    const item = localStorage.getItem(key);
    storedValue = item ? JSON.parse(item) : initialValue;
  } catch (error) {
    console.error("ローカルストレージの読み込みエラー:", error);
    storedValue = initialValue;
  }
  
  let state = storedValue; // 内部状態の保持(クロージャで保持される)

  const setState = (newValue) => {
    // 2. 状態の更新
    state = newValue;
    
    // 3. localStorageに書き込む副作用も実行
    try {
      localStorage.setItem(key, JSON.stringify(newValue));
    } catch (error) {
      console.error("ローカルストレージの書き込みエラー:", error);
    }
  };

  // [現在の値, 更新関数] を返す
  return [state, setState];
}

2. イベント監視と副作用のクリーンアップ

ブラウザイベントの監視は、ロジックを分離する上で非常に有用ですが、副作用(イベントリスナーの登録)を伴います。登録したリスナーを解除せずに放置すると、メモリリークを引き起こすため、後処理(クリーンアップ)を設計に含める必要があります。

JavaScript

// ネットワーク接続状態を監視するステートフル関数
function useNetworkStatus() {
  let isOnline = navigator.onLine; // 内部状態の保持

  const updateStatus = () => {
    isOnline = navigator.onLine;
    console.log(`接続状態が更新されました: ${isOnline ? 'オンライン' : 'オフライン'}`);
  };

  // 1. 副作用の登録
  window.addEventListener('online', updateStatus);
  window.addEventListener('offline', updateStatus);
  
  // 2. 戻り値:現在の状態とクリーンアップ関数
  return {
    isOnline,
    // クリーンアップ関数(ロジックの後処理)をセットで提供する
    cleanup: () => {
      window.removeEventListener('online', updateStatus);
      window.removeEventListener('offline', updateStatus);
      console.log('ネットワークリスナーを解除しました。');
    }
  };
}

クリーンアップ設計の真髄

利用側は、コンポーネントが不要になったタイミング(例えば、コンポーネントが画面から削除される際)で、フックが提供するcleanup関数を呼び出す責務を持ちます。

この「副作用の登録と後処理をセットで提供する」という考え方こそが、メモリリークを防ぎ、アプリケーションの安定性を保つための高品質なステートフル関数設計の真髄です。

最後に

ステートフル関数の設計術は、状態管理副作用を、クロージャの力を借りてカプセル化し、再利用可能な関数として切り出すことにあります。

  • 分離の原則:
    複雑なロジックを関数内に閉じ込め、コンポーネントから分離します。
  • 再利用性:
    一度作った関数は、ロジックを重複させることなく、どのコンポーネントでも利用できます。
  • 堅牢な設計:
    副作用を伴うロジックには、必ずクリーンアップ関数をセットで提供し、メモリリークや意図しない動作を防ぎます。

このパターンをマスターすることで、あなたは大規模アプリケーションでも破綻しない、保守性が高く、テストしやすいモダンなJavaScriptコンポーネントを設計できるようになるでしょう。

以上、ステートフル関数の設計術についてのご紹介でした!

この記事を書いた人

Ryohei

Webエンジニア / ブロガー

福岡のWeb制作会社に務めるWebエンジニアです。エンジニア歴は10年程で、好きな言語はPHPとJavaScriptです。本サイトは私がインプットしたWebに関する知識を整理し、共有することを目的に2015年から運営しています。Webに関するご相談があれば気軽にお問い合わせください。