T-CREATOR

SolidJS のリアクティブライフサイクルフック入門

SolidJS のリアクティブライフサイクルフック入門

フロントエンド開発者の皆さん、「なぜこのコンポーネントが無限ループしてしまうのか」「メモリリークが発生しているのはなぜか」といった悩みを抱えたことはありませんか?

SolidJS の魅力の一つが、シンプルで直感的なライフサイクルフック。React とは異なるアプローチで、より効率的なリアクティブプログラミングを実現できます。この記事では、SolidJS のライフサイクルフックを実際のコードとエラー事例を交えながら、実践的に学んでいきましょう。

ライフサイクルフックとは?SolidJS の基本概念

SolidJS のライフサイクルフックの特徴

SolidJS におけるライフサイクルフックは、コンポーネントの生成から破棄までの各段階で実行される関数です。最も重要なのは、SolidJS のライフサイクルフックは真のリアクティブシステムに基づいていることです。

従来のフレームワークでは、状態が変更されるたびに仮想 DOM の比較(diff)が行われ、必要な部分だけが再レンダリングされます。しかし、SolidJS ではSignalという仕組みを使って、変更された箇所だけを直接更新します。

typescriptimport { createSignal } from 'solid-js';

function Counter() {
  const [count, setCount] = createSignal(0);

  // この関数は初回のみ実行され、以降は自動的に追跡される
  return (
    <div>
      <p>カウント: {count()}</p>
      <button onClick={() => setCount(count() + 1)}>
        増やす
      </button>
    </div>
  );
}

上記のコードでは、count()が変更されるたびに、その Signal に依存する部分だけが自動的に更新されます。これにより、パフォーマンスが大幅に向上し、開発者はより直感的にコードを書くことができるのです。

React との違いを理解する

React を使っている開発者の多くが最初に戸惑うのが、SolidJS の「実行タイミング」です。

React の場合:

javascript// React - 毎回実行される
function ReactComponent() {
  const [count, setCount] = useState(0);

  // このコンポーネント関数は状態変更のたびに実行される
  console.log('コンポーネントが実行されました');

  useEffect(() => {
    console.log('useEffectが実行されました');
  }, [count]);

  return <div>{count}</div>;
}

SolidJS の場合:

typescript// SolidJS - 初回のみ実行される
function SolidComponent() {
  const [count, setCount] = createSignal(0);

  // このコンポーネント関数は初回のみ実行される
  console.log('コンポーネントが実行されました');

  createEffect(() => {
    console.log('createEffectが実行されました');
    console.log('現在のcount:', count());
  });

  return <div>{count()}</div>;
}

この違いを理解することで、SolidJS の一度実行されたコンポーネント関数は、以降は実行されないという特徴を活用できるようになります。

必須のライフサイクルフック 4 選

onMount:コンポーネントの初期化を担う

onMountは、コンポーネントが DOM に追加された直後に実行される関数です。データの初期化、API 呼び出し、外部ライブラリの初期化などに使用します。

まず、基本的な使い方を見てみましょう:

typescriptimport { onMount } from 'solid-js';

function UserProfile() {
  const [user, setUser] = createSignal(null);

  onMount(() => {
    console.log('コンポーネントがマウントされました');
    // DOM要素にアクセス可能
    const element = document.getElementById('user-profile');
    if (element) {
      element.focus();
    }
  });

  return (
    <div id='user-profile' tabIndex={0}>
      ユーザープロフィール
    </div>
  );
}

よくある間違い: 初心者の方が陥りがちなのは、onMountの外で DOM 要素にアクセスしようとすることです。

typescriptfunction WrongExample() {
  // ❌ エラー: Cannot read properties of null (reading 'focus')
  const element = document.getElementById('my-input');
  element.focus(); // この時点ではまだDOM要素が存在しない

  return <input id='my-input' />;
}

正しい実装:

typescriptfunction CorrectExample() {
  onMount(() => {
    // ✅ 正しい: DOM要素が確実に存在する
    const element = document.getElementById('my-input');
    if (element) {
      element.focus();
    }
  });

  return <input id='my-input' />;
}

onCleanup:リソースの適切な解放

onCleanupは、コンポーネントが破棄される際やエフェクトが再実行される前に呼び出される関数です。メモリリークを防ぐために非常に重要です。

実際のタイマー実装を見てみましょう:

typescriptimport { onMount, onCleanup } from 'solid-js';

function Timer() {
  const [seconds, setSeconds] = createSignal(0);

  onMount(() => {
    const interval = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);

    // クリーンアップ関数を登録
    onCleanup(() => {
      clearInterval(interval);
      console.log('タイマーをクリーンアップしました');
    });
  });

  return <div>経過時間: {seconds()}秒</div>;
}

メモリリークの実例: onCleanupを忘れると、以下のようなメモリリークが発生します:

typescriptfunction LeakyTimer() {
  const [seconds, setSeconds] = createSignal(0);

  onMount(() => {
    const interval = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);

    // ❌ onCleanupを忘れると、コンポーネントが破棄されても
    // setIntervalが動き続ける
  });

  return <div>経過時間: {seconds()}秒</div>;
}

このようなコードは、開発者ツールのメモリタブで確認すると、メモリ使用量が増加し続けることがわかります。

createEffect:副作用を管理する

createEffectは、依存する Signal が変更されたときに実行される関数です。React のuseEffectに似ていますが、依存配列を明示的に指定する必要がありません。

基本的な使い方:

typescriptimport { createSignal, createEffect } from 'solid-js';

function SearchComponent() {
  const [query, setQuery] = createSignal('');
  const [results, setResults] = createSignal([]);

  // queryが変更されるたびに自動実行
  createEffect(() => {
    const searchTerm = query();

    if (searchTerm.length > 2) {
      // 検索API呼び出し
      searchAPI(searchTerm).then((data) => {
        setResults(data);
      });
    }
  });

  return (
    <div>
      <input
        type='text'
        value={query()}
        onInput={(e) => setQuery(e.target.value)}
        placeholder='検索キーワードを入力...'
      />
      <div>検索結果: {results().length}件</div>
    </div>
  );
}

無限ループの原因と対策: createEffect内で Signal を更新すると、無限ループが発生することがあります:

typescriptfunction InfiniteLoopExample() {
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    // ❌ 無限ループ発生
    // count()を読み取り → エフェクト実行 → count更新 → 再実行...
    setCount(count() + 1);
  });

  return <div>{count()}</div>;
}

エラーメッセージ:

vbnetError: Potential infinite loop detected in createEffect

解決策:

typescriptfunction FixedExample() {
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    const currentCount = count();

    // 条件を追加して無限ループを防ぐ
    if (currentCount < 10) {
      setCount(currentCount + 1);
    }
  });

  return <div>{count()}</div>;
}

createMemo:計算結果をメモ化する

createMemoは、依存する Signal が変更されない限り、計算結果をキャッシュしてくれる関数です。重い計算処理の最適化に使用します。

重い計算処理の例:

typescriptimport { createSignal, createMemo } from 'solid-js';

function ExpensiveCalculation() {
  const [numbers, setNumbers] = createSignal([
    1, 2, 3, 4, 5,
  ]);
  const [multiplier, setMultiplier] = createSignal(1);

  // 重い計算処理をメモ化
  const expensiveResult = createMemo(() => {
    console.log('重い計算を実行中...');

    return numbers().reduce((acc, num) => {
      // 意図的に重い処理をシミュレート
      for (let i = 0; i < 1000000; i++) {
        // 空のループ
      }
      return acc + num * multiplier();
    }, 0);
  });

  return (
    <div>
      <button
        onClick={() => setMultiplier(multiplier() * 2)}
      >
        倍率を2倍にする
      </button>
      <div>結果: {expensiveResult()}</div>
    </div>
  );
}

メモ化なしの場合:

typescriptfunction WithoutMemo() {
  const [numbers, setNumbers] = createSignal([
    1, 2, 3, 4, 5,
  ]);
  const [multiplier, setMultiplier] = createSignal(1);

  // ❌ 毎回計算が実行される
  const expensiveResult = () => {
    console.log('重い計算を実行中...');

    return numbers().reduce((acc, num) => {
      for (let i = 0; i < 1000000; i++) {
        // 空のループ
      }
      return acc + num * multiplier();
    }, 0);
  };

  return (
    <div>
      <button
        onClick={() => setMultiplier(multiplier() * 2)}
      >
        倍率を2倍にする
      </button>
      <div>結果: {expensiveResult()}</div>
    </div>
  );
}

この違いは、開発者ツールのパフォーマンスタブで確認できます。メモ化ありの場合は、依存する値が変更されない限り計算が実行されないため、大幅なパフォーマンス向上が期待できます。

実践的なライフサイクルフック

createResource:非同期データの取得

createResourceは、非同期データの取得と管理を簡単にしてくれる強力なフックです。ローディング状態、エラー状態、成功状態を自動的に管理してくれます。

基本的な使い方:

typescriptimport { createResource } from 'solid-js';

// API呼び出し関数
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error('ユーザーの取得に失敗しました');
  }
  return response.json();
}

function UserProfile() {
  const [userId, setUserId] = createSignal(1);

  // リソースの作成
  const [user, { refetch }] = createResource(
    userId,
    fetchUser
  );

  return (
    <div>
      <button onClick={() => setUserId(userId() + 1)}>
        次のユーザー
      </button>

      {user.loading && <div>読み込み中...</div>}
      {user.error && (
        <div>エラー: {user.error.message}</div>
      )}
      {user() && (
        <div>
          <h2>{user().name}</h2>
          <p>{user().email}</p>
        </div>
      )}
    </div>
  );
}

エラーハンドリングの実例:

typescriptfunction RobustUserProfile() {
  const [userId, setUserId] = createSignal(1);

  const [user, { refetch }] = createResource(
    userId,
    async (id) => {
      try {
        const response = await fetch(`/api/users/${id}`);

        if (!response.ok) {
          if (response.status === 404) {
            throw new Error('ユーザーが見つかりません');
          }
          if (response.status === 500) {
            throw new Error('サーバーエラーが発生しました');
          }
          throw new Error('不明なエラーが発生しました');
        }

        return response.json();
      } catch (error) {
        console.error('ユーザー取得エラー:', error);
        throw error;
      }
    }
  );

  return (
    <div>
      <button onClick={() => setUserId(userId() + 1)}>
        次のユーザー
      </button>

      <button onClick={refetch} disabled={user.loading}>
        再読み込み
      </button>

      {user.loading && <div>読み込み中...</div>}
      {user.error && (
        <div style={{ color: 'red' }}>
          エラー: {user.error.message}
        </div>
      )}
      {user() && (
        <div>
          <h2>{user().name}</h2>
          <p>{user().email}</p>
        </div>
      )}
    </div>
  );
}

createSignal:状態管理の基本

createSignalは SolidJS の状態管理の基本となる関数です。React の useState に似ていますが、より効率的で直感的です。

基本的な使い方:

typescriptimport { createSignal } from 'solid-js';

function Counter() {
  const [count, setCount] = createSignal(0);

  // 現在の値を取得する場合は関数として呼び出す
  const handleIncrement = () => {
    setCount(count() + 1);
  };

  // 関数を使った更新も可能
  const handleDecrement = () => {
    setCount((prev) => prev - 1);
  };

  return (
    <div>
      <p>カウント: {count()}</p>
      <button onClick={handleIncrement}>+</button>
      <button onClick={handleDecrement}>-</button>
    </div>
  );
}

配列や オブジェクトの状態管理:

typescriptfunction TodoList() {
  const [todos, setTodos] = createSignal([]);
  const [inputValue, setInputValue] = createSignal('');

  const addTodo = () => {
    if (inputValue().trim()) {
      setTodos((prev) => [
        ...prev,
        {
          id: Date.now(),
          text: inputValue(),
          completed: false,
        },
      ]);
      setInputValue('');
    }
  };

  const toggleTodo = (id) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    );
  };

  return (
    <div>
      <input
        value={inputValue()}
        onInput={(e) => setInputValue(e.target.value)}
        placeholder='新しいTODOを入力...'
      />
      <button onClick={addTodo}>追加</button>

      <ul>
        {todos().map((todo) => (
          <li
            key={todo.id}
            onClick={() => toggleTodo(todo.id)}
            style={{
              'text-decoration': todo.completed
                ? 'line-through'
                : 'none',
            }}
          >
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

createStore:複雑な状態を扱う

createStoreは、ネストしたオブジェクトや配列を効率的に管理するためのフックです。部分的な更新でも適切にリアクティブになります。

基本的な使い方:

typescriptimport { createStore } from 'solid-js/store';

function UserSettings() {
  const [settings, setSettings] = createStore({
    profile: {
      name: '太郎',
      email: 'taro@example.com',
    },
    preferences: {
      theme: 'light',
      notifications: true,
    },
  });

  const updateName = (newName) => {
    setSettings('profile', 'name', newName);
  };

  const toggleTheme = () => {
    setSettings(
      'preferences',
      'theme',
      settings.preferences.theme === 'light'
        ? 'dark'
        : 'light'
    );
  };

  return (
    <div>
      <h2>ユーザー設定</h2>
      <input
        value={settings.profile.name}
        onInput={(e) => updateName(e.target.value)}
      />

      <button onClick={toggleTheme}>
        テーマ: {settings.preferences.theme}
      </button>

      <div>
        <p>名前: {settings.profile.name}</p>
        <p>メール: {settings.profile.email}</p>
      </div>
    </div>
  );
}

複雑な配列操作:

typescriptfunction ShoppingCart() {
  const [cart, setCart] = createStore({
    items: [],
    total: 0,
  });

  const addItem = (product) => {
    setCart('items', (items) => [
      ...items,
      {
        id: product.id,
        name: product.name,
        price: product.price,
        quantity: 1,
      },
    ]);
    updateTotal();
  };

  const updateQuantity = (id, newQuantity) => {
    setCart(
      'items',
      (item) => item.id === id,
      'quantity',
      newQuantity
    );
    updateTotal();
  };

  const updateTotal = () => {
    const total = cart.items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
    setCart('total', total);
  };

  return (
    <div>
      <h2>ショッピングカート</h2>
      {cart.items.map((item) => (
        <div key={item.id}>
          <span>{item.name}</span>
          <input
            type='number'
            value={item.quantity}
            onInput={(e) =>
              updateQuantity(
                item.id,
                parseInt(e.target.value)
              )
            }
          />
          <span>¥{item.price * item.quantity}</span>
        </div>
      ))}
      <div>合計: ¥{cart.total}</div>
    </div>
  );
}

よくある落とし穴とその対策

無限ループを避ける方法

SolidJS では、特定の条件下で無限ループが発生することがあります。よくあるパターンと対策を見てみましょう。

パターン 1: createEffect 内での Signal 更新

typescript// ❌ 無限ループの例
function InfiniteLoop() {
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    console.log('現在のカウント:', count());
    setCount(count() + 1); // 無限ループ発生
  });

  return <div>{count()}</div>;
}

エラーメッセージ:

vbnetError: Potential infinite loop detected in createEffect

解決策:

typescript// ✅ 条件付きで更新
function FixedLoop() {
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    console.log('現在のカウント:', count());
    if (count() < 10) {
      // 条件を追加
      setCount(count() + 1);
    }
  });

  return <div>{count()}</div>;
}

パターン 2: 相互依存する Signal

typescript// ❌ 相互依存による無限ループ
function MutualDependency() {
  const [a, setA] = createSignal(0);
  const [b, setB] = createSignal(0);

  createEffect(() => {
    if (a() !== b()) {
      setB(a());
    }
  });

  createEffect(() => {
    if (b() !== a()) {
      setA(b());
    }
  });

  return (
    <div>
      {a()} - {b()}
    </div>
  );
}

解決策:

typescript// ✅ 単方向の依存関係
function OnewayDependency() {
  const [source, setSource] = createSignal(0);
  const [derived, setDerived] = createSignal(0);

  createEffect(() => {
    setDerived(source() * 2);
  });

  return (
    <div>
      <input
        type='number'
        value={source()}
        onInput={(e) => setSource(parseInt(e.target.value))}
      />
      <div>結果: {derived()}</div>
    </div>
  );
}

メモリリークを防ぐベストプラクティス

メモリリークは、アプリケーションのパフォーマンスに深刻な影響を与えます。SolidJS でよくあるメモリリークのパターンと対策を説明します。

パターン 1: イベントリスナーの削除忘れ

typescript// ❌ メモリリークの例
function LeakyComponent() {
  const [windowSize, setWindowSize] = createSignal({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  onMount(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener('resize', handleResize);
    // イベントリスナーを削除していない
  });

  return (
    <div>
      {windowSize().width} x {windowSize().height}
    </div>
  );
}

解決策:

typescript// ✅ 適切なクリーンアップ
function ProperComponent() {
  const [windowSize, setWindowSize] = createSignal({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  onMount(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener('resize', handleResize);

    // クリーンアップ関数を登録
    onCleanup(() => {
      window.removeEventListener('resize', handleResize);
    });
  });

  return (
    <div>
      {windowSize().width} x {windowSize().height}
    </div>
  );
}

パターン 2: WebSocket や SSE の接続

typescript// ❌ 接続の切断忘れ
function LeakyWebSocket() {
  const [messages, setMessages] = createSignal([]);

  onMount(() => {
    const ws = new WebSocket('ws://localhost:8080');

    ws.onmessage = (event) => {
      setMessages((prev) => [...prev, event.data]);
    };

    // 接続を切断していない
  });

  return (
    <div>
      {messages().map((msg, index) => (
        <div key={index}>{msg}</div>
      ))}
    </div>
  );
}

解決策:

typescript// ✅ 適切な接続管理
function ProperWebSocket() {
  const [messages, setMessages] = createSignal([]);

  onMount(() => {
    const ws = new WebSocket('ws://localhost:8080');

    ws.onmessage = (event) => {
      setMessages((prev) => [...prev, event.data]);
    };

    ws.onerror = (error) => {
      console.error('WebSocket エラー:', error);
    };

    // 接続を適切に切断
    onCleanup(() => {
      ws.close();
    });
  });

  return (
    <div>
      {messages().map((msg, index) => (
        <div key={index}>{msg}</div>
      ))}
    </div>
  );
}

パフォーマンスを最適化するコツ

SolidJS は既に高速ですが、さらなる最適化のテクニックをご紹介します。

コツ 1: 不要な再計算を避ける

typescript// ❌ 毎回計算される
function SlowComponent() {
  const [items, setItems] = createSignal([]);

  // 毎回フィルタリングが実行される
  const expensiveItems = () => {
    console.log('高価な計算を実行中...');
    return items().filter((item) => item.price > 1000);
  };

  return (
    <div>
      {expensiveItems().map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

解決策:

typescript// ✅ メモ化で最適化
function FastComponent() {
  const [items, setItems] = createSignal([]);

  // 計算結果をメモ化
  const expensiveItems = createMemo(() => {
    console.log('高価な計算を実行中...');
    return items().filter((item) => item.price > 1000);
  });

  return (
    <div>
      {expensiveItems().map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

コツ 2: 適切なコンポーネント分割

typescript// ❌ 大きすぎるコンポーネント
function MonolithicComponent() {
  const [users, setUsers] = createSignal([]);
  const [selectedUser, setSelectedUser] =
    createSignal(null);
  const [editing, setEditing] = createSignal(false);

  return (
    <div>
      {/* 大量のJSX... */}
      {users().map((user) => (
        <div key={user.id}>
          {/* 複雑なユーザー表示ロジック */}
          {selectedUser()?.id === user.id && editing() && (
            <div>{/* 編集フォーム */}</div>
          )}
        </div>
      ))}
    </div>
  );
}

解決策:

typescript// ✅ 適切に分割されたコンポーネント
function UserList() {
  const [users, setUsers] = createSignal([]);

  return (
    <div>
      {users().map((user) => (
        <UserItem key={user.id} user={user} />
      ))}
    </div>
  );
}

function UserItem(props) {
  const [editing, setEditing] = createSignal(false);

  return (
    <div>
      {editing() ? (
        <UserEditForm
          user={props.user}
          onSave={() => setEditing(false)}
        />
      ) : (
        <UserDisplay
          user={props.user}
          onEdit={() => setEditing(true)}
        />
      )}
    </div>
  );
}

実際のアプリケーションでの活用事例

フォームバリデーション

実際のアプリケーションでよく使われるフォームバリデーションの実装例を見てみましょう。

typescriptimport { createSignal, createMemo } from 'solid-js';

function SignupForm() {
  const [formData, setFormData] = createSignal({
    email: '',
    password: '',
    confirmPassword: '',
  });

  const [touched, setTouched] = createSignal({
    email: false,
    password: false,
    confirmPassword: false,
  });

  // バリデーションロジック
  const emailError = createMemo(() => {
    const email = formData().email;
    if (!touched().email) return '';
    if (!email) return 'メールアドレスは必須です';
    if (!/\S+@\S+\.\S+/.test(email)) {
      return '有効なメールアドレスを入力してください';
    }
    return '';
  });

  const passwordError = createMemo(() => {
    const password = formData().password;
    if (!touched().password) return '';
    if (!password) return 'パスワードは必須です';
    if (password.length < 8) {
      return 'パスワードは8文字以上である必要があります';
    }
    return '';
  });

  const confirmPasswordError = createMemo(() => {
    const { password, confirmPassword } = formData();
    if (!touched().confirmPassword) return '';
    if (!confirmPassword)
      return 'パスワードの確認は必須です';
    if (password !== confirmPassword) {
      return 'パスワードが一致しません';
    }
    return '';
  });

  const isValid = createMemo(() => {
    return (
      !emailError() &&
      !passwordError() &&
      !confirmPasswordError() &&
      formData().email &&
      formData().password &&
      formData().confirmPassword
    );
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    if (isValid()) {
      console.log('フォーム送信:', formData());
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type='email'
          placeholder='メールアドレス'
          value={formData().email}
          onInput={(e) =>
            setFormData((prev) => ({
              ...prev,
              email: e.target.value,
            }))
          }
          onBlur={() =>
            setTouched((prev) => ({ ...prev, email: true }))
          }
        />
        {emailError() && (
          <span style={{ color: 'red' }}>
            {emailError()}
          </span>
        )}
      </div>

      <div>
        <input
          type='password'
          placeholder='パスワード'
          value={formData().password}
          onInput={(e) =>
            setFormData((prev) => ({
              ...prev,
              password: e.target.value,
            }))
          }
          onBlur={() =>
            setTouched((prev) => ({
              ...prev,
              password: true,
            }))
          }
        />
        {passwordError() && (
          <span style={{ color: 'red' }}>
            {passwordError()}
          </span>
        )}
      </div>

      <div>
        <input
          type='password'
          placeholder='パスワード確認'
          value={formData().confirmPassword}
          onInput={(e) =>
            setFormData((prev) => ({
              ...prev,
              confirmPassword: e.target.value,
            }))
          }
          onBlur={() =>
            setTouched((prev) => ({
              ...prev,
              confirmPassword: true,
            }))
          }
        />
        {confirmPasswordError() && (
          <span style={{ color: 'red' }}>
            {confirmPasswordError()}
          </span>
        )}
      </div>

      <button type='submit' disabled={!isValid()}>
        アカウント作成
      </button>
    </form>
  );
}

データフェッチング

実際の API との通信を含むデータフェッチングの実装例です。

typescriptimport {
  createResource,
  createSignal,
  createEffect,
} from 'solid-js';

function ProductCatalog() {
  const [category, setCategory] = createSignal('all');
  const [searchTerm, setSearchTerm] = createSignal('');

  // カテゴリ別商品取得
  const [products, { refetch }] = createResource(
    () => ({ category: category(), search: searchTerm() }),
    async (params) => {
      const queryParams = new URLSearchParams();
      if (params.category !== 'all') {
        queryParams.append('category', params.category);
      }
      if (params.search) {
        queryParams.append('search', params.search);
      }

      const response = await fetch(
        `/api/products?${queryParams}`
      );
      if (!response.ok) {
        throw new Error('商品の取得に失敗しました');
      }
      return response.json();
    }
  );

  // 検索のデバウンス処理
  const [debouncedSearch, setDebouncedSearch] =
    createSignal('');

  createEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedSearch(searchTerm());
    }, 300);

    onCleanup(() => clearTimeout(timer));
  });

  createEffect(() => {
    if (debouncedSearch() !== searchTerm()) {
      setSearchTerm(debouncedSearch());
    }
  });

  return (
    <div>
      <div>
        <input
          type='text'
          placeholder='商品を検索...'
          value={searchTerm()}
          onInput={(e) => setSearchTerm(e.target.value)}
        />

        <select
          value={category()}
          onChange={(e) => setCategory(e.target.value)}
        >
          <option value='all'>すべて</option>
          <option value='electronics'>電子機器</option>
          <option value='clothing'>衣類</option>
          <option value='books'>書籍</option>
        </select>

        <button onClick={refetch}>再読み込み</button>
      </div>

      {products.loading && <div>読み込み中...</div>}
      {products.error && (
        <div style={{ color: 'red' }}>
          エラー: {products.error.message}
        </div>
      )}

      {products() && (
        <div>
          {products().map((product) => (
            <div key={product.id}>
              <h3>{product.name}</h3>
              <p>{product.description}</p>
              <span>¥{product.price}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

状態管理パターン

複雑な状態管理が必要なアプリケーションでの実装例です。

typescriptimport { createStore } from 'solid-js/store';
import { createContext, useContext } from 'solid-js';

// グローバル状態管理
const AppStateContext = createContext();

function AppProvider(props) {
  const [state, setState] = createStore({
    user: null,
    cart: {
      items: [],
      total: 0,
    },
    notifications: [],
  });

  const actions = {
    setUser: (user) => {
      setState('user', user);
    },

    addToCart: (product) => {
      setState('cart', 'items', (items) => {
        const existingItem = items.find(
          (item) => item.id === product.id
        );
        if (existingItem) {
          return items.map((item) =>
            item.id === product.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          );
        }
        return [...items, { ...product, quantity: 1 }];
      });

      // 合計金額を再計算
      setState(
        'cart',
        'total',
        state.cart.items.reduce(
          (sum, item) => sum + item.price * item.quantity,
          0
        )
      );
    },

    removeFromCart: (productId) => {
      setState('cart', 'items', (items) =>
        items.filter((item) => item.id !== productId)
      );

      setState(
        'cart',
        'total',
        state.cart.items.reduce(
          (sum, item) => sum + item.price * item.quantity,
          0
        )
      );
    },

    addNotification: (message, type = 'info') => {
      const id = Date.now();
      setState('notifications', (prev) => [
        ...prev,
        {
          id,
          message,
          type,
          timestamp: new Date(),
        },
      ]);

      // 3秒後に自動削除
      setTimeout(() => {
        setState('notifications', (prev) =>
          prev.filter(
            (notification) => notification.id !== id
          )
        );
      }, 3000);
    },
  };

  return (
    <AppStateContext.Provider value={[state, actions]}>
      {props.children}
    </AppStateContext.Provider>
  );
}

function useAppState() {
  const context = useContext(AppStateContext);
  if (!context) {
    throw new Error(
      'useAppState must be used within AppProvider'
    );
  }
  return context;
}

// 使用例
function ShoppingApp() {
  const [state, actions] = useAppState();

  return (
    <div>
      <header>
        <h1>ショッピングアプリ</h1>
        {state.user && (
          <span>こんにちは、{state.user.name}さん</span>
        )}
        <span>カート: {state.cart.items.length}個</span>
      </header>

      <main>
        <ProductList />
        <Cart />
      </main>

      <NotificationArea />
    </div>
  );
}

まとめ

SolidJS のライフサイクルフックは、従来のフレームワークとは異なる革新的なアプローチを提供しています。この記事では、基本的なonMountonCleanupから、高度なcreateResourcecreateStoreまで、実際のコード例とエラーケースを交えながら説明しました。

重要なポイント:

  1. 真のリアクティビティ: SolidJS は仮想 DOM に頼らず、Signal ベースの効率的な更新を実現
  2. シンプルな依存関係: 明示的な依存配列が不要で、より直感的なコードが書ける
  3. パフォーマンス最適化: メモ化やストア機能により、大規模アプリケーションでも高速動作
  4. メモリ安全性: 適切なonCleanupの使用でメモリリークを防止

これらの特徴により、SolidJS は現代の Web アプリケーション開発において、開発効率とパフォーマンスの両立を実現する強力な選択肢となっています。

皆さんも、次のプロジェクトで SolidJS のライフサイクルフックを活用して、より効率的で保守性の高いアプリケーションを構築してみてください。きっと、その直感的な API と高いパフォーマンスに驚かれることでしょう。

関連リンク