T-CREATOR

SolidJS フック相当 API 速見表:createSignal/createMemo/createEffect… 一覧

SolidJS フック相当 API 速見表:createSignal/createMemo/createEffect… 一覧

React から SolidJS への移行を検討している方、あるいは SolidJS を始めたばかりの方にとって、React のフックに相当する API を素早く把握することは非常に重要です。

SolidJS は React と似た構文を持ちながら、リアクティビティの仕組みが根本的に異なります。そのため、React の useStateuseEffect といったフックに対応する SolidJS の API を理解することが、効率的な開発の第一歩となるでしょう。

本記事では、SolidJS の主要な Primitives(プリミティブ)を一覧表形式で整理し、それぞれの使い方を具体的なコード例とともに解説します。

SolidJS フック相当 API 速見表

以下の表は、React のフックと SolidJS の対応する API を比較したものです。

#React フックSolidJS API用途リアクティブ再実行
1useStatecreateSignal状態管理×
2useMemocreateMemo派生値のメモ化
3useEffectcreateEffect副作用の実行
4useCallback関数定義コールバックのメモ化××
5useRef変数宣言可変参照の保持××
6useContextuseContextコンテキストの利用×
7useReducercreateStore複雑な状態管理×
8-createResource非同期データ取得
9-onMountマウント時の処理××
10-onCleanupクリーンアップ処理××

背景

React と SolidJS のリアクティビティの違い

React は仮想 DOM を利用し、コンポーネント全体を再レンダリングすることで UI を更新します。一方、SolidJS は細かい粒度のリアクティビティを採用しており、変更された部分だけを効率的に更新する仕組みです。

この根本的な違いにより、同じような機能を実現する API でも、内部動作や使い方が異なる場合があります。

以下の図は、React と SolidJS のリアクティビティの違いを示しています。

mermaidflowchart TB
  subgraph react["React のアプローチ"]
    react_state["状態変更"] --> react_vdom["仮想 DOM 再構築"]
    react_vdom --> react_diff["差分計算"]
    react_diff --> react_update["実 DOM 更新"]
  end

  subgraph solid["SolidJS のアプローチ"]
    solid_signal["Signal 変更"] --> solid_direct["依存関係を直接更新"]
    solid_direct --> solid_dom["実 DOM のみ更新"]
  end

React は状態変更のたびにコンポーネント関数を再実行しますが、SolidJS のコンポーネント関数は初回の 1 回のみ実行され、以降はリアクティブな依存関係によって必要な部分だけが更新されます。

このため、React のフックの概念をそのまま SolidJS に適用しようとすると、期待通りに動作しないことがあります。

課題

React 開発者が SolidJS で直面する課題

React から SolidJS に移行する際、多くの開発者が以下のような課題に直面します。

  1. 再レンダリングの概念がない: React の useEffect の依存配列のような考え方が通用しません。
  2. フック名が異なる: useState ではなく createSignal という名前で、直感的に対応が分かりにくいです。
  3. メモ化の必要性が異なる: useCallbackuseMemo が必須だった場面が、SolidJS では不要になることがあります。
  4. クリーンアップの方法: useEffect の return 関数ではなく、onCleanup を使います。

以下の図は、React 開発者が SolidJS で直面する主な課題を示しています。

mermaidflowchart TD
  start["React 開発者"] --> q1{"useState の<br/>代わりは?"}
  q1 -->|正解| s1["createSignal"]
  q1 -->|誤解| e1["useState を探す"]

  start --> q2{"useEffect の<br/>代わりは?"}
  q2 -->|正解| s2["createEffect"]
  q2 -->|誤解| e2["依存配列を探す"]

  start --> q3{"useMemo の<br/>代わりは?"}
  q3 -->|正解| s3["createMemo"]
  q3 -->|誤解| e3["再レンダリングを<br/>期待する"]

  e1 --> confusion["混乱"]
  e2 --> confusion
  e3 --> confusion

  s1 --> success["正しい理解"]
  s2 --> success
  s3 --> success

これらの課題を解決するには、React のフックと SolidJS の Primitives の対応関係を正しく理解することが不可欠です。

解決策

SolidJS の主要 API とその使い方

SolidJS では、React のフックに相当する機能を「Primitives」と呼ばれる API で提供しています。それぞれの Primitives の特徴と使い方を理解することで、スムーズに SolidJS を習得できるでしょう。

以下、主要な Primitives について詳しく解説します。

createSignal - 状態管理の基本

createSignal は React の useState に相当する API です。状態の読み取りと更新を別々の関数として提供します。

typescriptimport { createSignal } from 'solid-js';
typescript// createSignal の基本的な使い方
const [count, setCount] = createSignal(0);
typescript// 値の読み取り(関数として呼び出す)
console.log(count()); // 0
typescript// 値の更新
setCount(1);
setCount((c) => c + 1); // 前の値を使う場合

重要な違い: React の useState と異なり、値を取得するには関数として呼び出す必要があります。これにより、SolidJS はどこで値が使われているかを追跡できます。

createMemo - 派生値のメモ化

createMemo は React の useMemo に相当しますが、依存配列を指定する必要がありません。

typescriptimport { createMemo } from 'solid-js';
typescriptconst [count, setCount] = createSignal(0);

// createMemo は依存する Signal を自動的に追跡
const doubled = createMemo(() => count() * 2);
typescript// メモ化された値を取得(これも関数として呼び出す)
console.log(doubled()); // 0
setCount(5);
console.log(doubled()); // 10

createMemo は依存する Signal が変更されたときだけ再計算されます。依存配列を手動で管理する必要がないため、バグを減らせるでしょう。

createEffect - 副作用の実行

createEffect は React の useEffect に相当しますが、動作が大きく異なります。

typescriptimport { createEffect } from 'solid-js';
typescriptconst [count, setCount] = createSignal(0);

// createEffect は依存する Signal を自動追跡
createEffect(() => {
  console.log('Count changed:', count());
});
typescript// count が変更されると自動的に再実行される
setCount(1); // "Count changed: 1" がログ出力される
setCount(2); // "Count changed: 2" がログ出力される

重要な違い: React の useEffect と異なり、依存配列は不要です。関数内で使用した Signal が自動的に追跡されます。

onCleanup - クリーンアップ処理

onCleanup は、エフェクトが再実行される前やコンポーネントが破棄される前に実行される処理を登録します。

typescriptimport { createEffect, onCleanup } from 'solid-js';
typescriptcreateEffect(() => {
  const timer = setInterval(() => {
    console.log('Tick');
  }, 1000);

  // クリーンアップ処理を登録
  onCleanup(() => {
    clearInterval(timer);
    console.log('Timer cleared');
  });
});

React の useEffect の return 関数と同じ役割ですが、より明示的に意図を表現できます。

createResource - 非同期データ取得

createResource は、非同期データ取得を簡単に扱うための SolidJS 独自の API です。React には直接対応するフックがありません。

typescriptimport { createResource } from 'solid-js';
typescript// データ取得関数を定義
const fetchUser = async (id: number) => {
  const response = await fetch(
    `https://api.example.com/users/${id}`
  );
  return response.json();
};
typescriptconst [userId, setUserId] = createSignal(1);

// createResource でデータ取得を管理
const [user] = createResource(userId, fetchUser);
typescript// リソースの状態を確認
console.log(user.loading); // データ取得中かどうか
console.log(user.error); // エラー情報
console.log(user()); // 取得したデータ

createResource は、ローディング状態やエラー処理を自動的に管理してくれるため、非同期処理を簡潔に記述できます。

onMount - マウント時の処理

onMount は、コンポーネントが DOM にマウントされた直後に 1 回だけ実行される処理を登録します。

typescriptimport { onMount } from 'solid-js';
typescriptonMount(() => {
  console.log('Component mounted');
  // DOM 操作や初期化処理を実行
});

React の useEffect(() => {}, []) に相当しますが、より意図が明確になります。

createStore - 複雑な状態管理

createStore は、ネストしたオブジェクトや配列などの複雑な状態を管理するための API です。

typescriptimport { createStore } from 'solid-js/store';
typescript// ストアの作成
const [store, setStore] = createStore({
  user: {
    name: 'Alice',
    age: 30,
  },
  todos: [],
});
typescript// ネストしたプロパティの更新
setStore('user', 'name', 'Bob');
setStore('user', { age: 31 }); // 部分的な更新
typescript// 配列の操作
setStore('todos', (todos) => [
  ...todos,
  { id: 1, text: 'New task' },
]);

createStore は、React の useReducer や状態管理ライブラリに近い役割を果たします。細かい粒度でリアクティビティを保持できるため、パフォーマンスに優れています。

以下の図は、SolidJS の主要 API の関係性を示しています。

mermaidflowchart LR
  signal["createSignal<br/>(状態)"] --> memo["createMemo<br/>(派生値)"]
  signal --> effect["createEffect<br/>(副作用)"]
  memo --> effect

  signal --> resource["createResource<br/>(非同期)"]

  effect --> cleanup["onCleanup<br/>(クリーンアップ)"]

  store["createStore<br/>(複雑な状態)"] --> memo
  store --> effect

各 API は互いに連携して動作し、効率的なリアクティブシステムを構築します。

具体例

カウンターアプリで比較する React と SolidJS

React と SolidJS で同じカウンターアプリを実装し、コードの違いを確認してみましょう。

React 版のカウンター

typescriptimport React, { useState, useEffect } from 'react';
typescriptfunction Counter() {
  const [count, setCount] = useState(0);
  const [doubled, setDoubled] = useState(0);

  // 派生値の計算
  useEffect(() => {
    setDoubled(count * 2);
  }, [count]);

  // ログ出力
  useEffect(() => {
    console.log('Count:', count);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

React では、派生値を計算するために useEffect を使い、依存配列を指定する必要があります。

SolidJS 版のカウンター

typescriptimport {
  createSignal,
  createMemo,
  createEffect,
} from 'solid-js';
typescriptfunction Counter() {
  const [count, setCount] = createSignal(0);

  // 派生値の計算(依存配列不要)
  const doubled = createMemo(() => count() * 2);

  // ログ出力(依存配列不要)
  createEffect(() => {
    console.log('Count:', count());
  });

  return (
    <div>
      <p>Count: {count()}</p>
      <p>Doubled: {doubled()}</p>
      <button onClick={() => setCount((c) => c + 1)}>
        Increment
      </button>
    </div>
  );
}

SolidJS では、依存配列を指定する必要がなく、より簡潔に記述できます。また、値を取得する際は関数として呼び出す点に注意してください。

データ取得を含む実践的な例

次に、API からデータを取得して表示する実践的な例を見てみましょう。

データ取得用の API 関数

typescript// ユーザー情報を取得する非同期関数
const fetchUserData = async (userId: number) => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${userId}`
  );

  if (!response.ok) {
    throw new Error('Failed to fetch user data');
  }

  return response.json();
};

SolidJS でのデータ取得と表示

typescriptimport {
  createSignal,
  createResource,
  Show,
} from 'solid-js';
typescriptfunction UserProfile() {
  const [userId, setUserId] = createSignal(1);

  // createResource でデータ取得を管理
  const [user] = createResource(userId, fetchUserData);

  return (
    <div>
      <h2>User Profile</h2>

      <div>
        <button onClick={() => setUserId((id) => id - 1)}>
          Previous User
        </button>
        <button onClick={() => setUserId((id) => id + 1)}>
          Next User
        </button>
      </div>

      {/* ローディング状態の表示 */}
      <Show
        when={!user.loading}
        fallback={<p>Loading...</p>}
      >
        {/* エラー処理 */}
        <Show
          when={!user.error}
          fallback={<p>Error: {user.error}</p>}
        >
          <div>
            <p>Name: {user()?.name}</p>
            <p>Email: {user()?.email}</p>
            <p>City: {user()?.address?.city}</p>
          </div>
        </Show>
      </Show>
    </div>
  );
}

createResource を使うことで、ローディング状態やエラー処理を簡潔に記述できます。userId が変更されると自動的に再取得が行われるのもポイントです。

タイマーとクリーンアップの例

副作用とクリーンアップを含む例として、タイマーアプリを実装してみましょう。

タイマーコンポーネント

typescriptimport {
  createSignal,
  createEffect,
  onCleanup,
  onMount,
} from 'solid-js';
typescriptfunction Timer() {
  const [seconds, setSeconds] = createSignal(0);
  const [isRunning, setIsRunning] = createSignal(false);

  // タイマーのエフェクト
  createEffect(() => {
    if (isRunning()) {
      const interval = setInterval(() => {
        setSeconds((s) => s + 1);
      }, 1000);

      // クリーンアップ処理
      onCleanup(() => {
        clearInterval(interval);
      });
    }
  });

  return (
    <div>
      <h2>Timer: {seconds()}s</h2>
      <button onClick={() => setIsRunning(!isRunning())}>
        {isRunning() ? 'Stop' : 'Start'}
      </button>
      <button onClick={() => setSeconds(0)}>Reset</button>
    </div>
  );
}

onCleanup を使うことで、タイマーのクリーンアップ処理を適切に管理できます。isRunning の値が変わるたびにエフェクトが再実行され、古いタイマーは自動的にクリアされます。

createStore を使った Todo アプリ

複雑な状態管理が必要な Todo アプリを createStore で実装してみましょう。

Todo の型定義

typescripttype Todo = {
  id: number;
  text: string;
  completed: boolean;
};

Todo アプリのストア

typescriptimport { createStore } from 'solid-js/store';
import { createSignal, For } from 'solid-js';
typescriptfunction TodoApp() {
  const [store, setStore] = createStore<{ todos: Todo[] }>({
    todos: [],
  });

  const [input, setInput] = createSignal('');
  let nextId = 1;

  // Todo を追加
  const addTodo = () => {
    const text = input().trim();
    if (!text) return;

    setStore('todos', (todos) => [
      ...todos,
      { id: nextId++, text, completed: false },
    ]);
    setInput('');
  };

  // Todo の完了状態を切り替え
  const toggleTodo = (id: number) => {
    setStore(
      'todos',
      (todo) => todo.id === id,
      'completed',
      (completed) => !completed
    );
  };

  // Todo を削除
  const removeTodo = (id: number) => {
    setStore('todos', (todos) =>
      todos.filter((t) => t.id !== id)
    );
  };

  return (
    <div>
      <h2>Todo List</h2>

      <div>
        <input
          value={input()}
          onInput={(e) => setInput(e.currentTarget.value)}
          placeholder='Enter a task'
        />
        <button onClick={addTodo}>Add</button>
      </div>

      <ul>
        <For each={store.todos}>
          {(todo) => (
            <li>
              <input
                type='checkbox'
                checked={todo.completed}
                onChange={() => toggleTodo(todo.id)}
              />
              <span
                style={{
                  'text-decoration': todo.completed
                    ? 'line-through'
                    : 'none',
                }}
              >
                {todo.text}
              </span>
              <button onClick={() => removeTodo(todo.id)}>
                Delete
              </button>
            </li>
          )}
        </For>
      </ul>
    </div>
  );
}

createStore を使うことで、配列やオブジェクトの深い階層の変更も効率的に追跡できます。特定の Todo の completed フィールドだけを更新しても、他の Todo には影響を与えません。

以下の図は、Todo アプリでの状態更新フローを示しています。

mermaidflowchart TD
  user_input["ユーザー入力"] --> add["addTodo 実行"]
  add --> update_store["setStore で<br/>todos 配列に追加"]
  update_store --> reactive["リアクティブ<br/>更新"]
  reactive --> ui["UI が自動更新"]

  user_toggle["チェックボックス<br/>クリック"] --> toggle["toggleTodo 実行"]
  toggle --> update_completed["setStore で<br/>completed を更新"]
  update_completed --> reactive

  user_delete["削除ボタン<br/>クリック"] --> remove["removeTodo 実行"]
  remove --> filter_todos["setStore で<br/>todos をフィルタ"]
  filter_todos --> reactive

ユーザーのアクションに応じて、適切な関数が実行され、ストアが更新されることで UI が自動的に反映されます。

API の組み合わせパターン

実際のアプリケーションでは、複数の Primitives を組み合わせて使います。

typescriptimport {
  createSignal,
  createMemo,
  createEffect,
  createResource,
} from 'solid-js';
typescriptfunction DataDashboard() {
  // フィルター条件
  const [searchQuery, setSearchQuery] = createSignal('');
  const [sortOrder, setSortOrder] = createSignal<
    'asc' | 'desc'
  >('asc');

  // データ取得
  const [data] = createResource(() => fetchData());

  // フィルタリングとソート
  const filteredData = createMemo(() => {
    const query = searchQuery().toLowerCase();
    const items = data() || [];

    return items
      .filter((item) =>
        item.name.toLowerCase().includes(query)
      )
      .sort((a, b) => {
        const order = sortOrder() === 'asc' ? 1 : -1;
        return a.name.localeCompare(b.name) * order;
      });
  });

  // データが変更されたらログ出力
  createEffect(() => {
    console.log(
      'Filtered data count:',
      filteredData().length
    );
  });

  return <div>{/* UI コンポーネント */}</div>;
}

このように、createResource でデータを取得し、createSignal でフィルター条件を管理し、createMemo で派生値を計算し、createEffect で副作用を実行するという組み合わせが一般的です。

まとめ

SolidJS のフック相当 API(Primitives)は、React のフックと似た機能を提供しながら、より効率的なリアクティビティシステムを実現しています。

主なポイントを振り返りましょう。

状態管理の基本: createSignaluseState に相当しますが、値を取得する際は関数として呼び出す必要があります。これにより、SolidJS は依存関係を自動的に追跡できます。

派生値のメモ化: createMemouseMemo に相当しますが、依存配列を指定する必要がありません。使用した Signal が自動的に追跡されるため、バグを減らせるでしょう。

副作用の実行: createEffectuseEffect に相当しますが、こちらも依存配列は不要です。関数内で参照した Signal が変更されると自動的に再実行されます。

非同期データ取得: createResource は SolidJS 独自の API で、非同期データ取得を簡単に扱えます。ローディング状態やエラー処理が自動的に管理されるため、コードが簡潔になります。

複雑な状態管理: createStore を使えば、ネストしたオブジェクトや配列などの複雑な状態を効率的に管理できます。細かい粒度でリアクティビティを保持できるため、パフォーマンスに優れています。

React から SolidJS への移行は、フックから Primitives への対応関係を理解することから始まります。本記事で紹介した速見表と具体例を参考に、SolidJS の効率的なリアクティビティシステムを活用してください。

関連リンク