solid-js の使い方

A declarative JavaScript library for building user interfaces.

v1.9.121.8M/週MITフレームワーク
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

solid-js の使い方 — 高速リアクティブUIライブラリ完全ガイド

一言でいうと

solid-js は、仮想DOMを使わずにきめ細かいリアクティビティ(Fine-Grained Reactivity)で直接DOMを更新する、高パフォーマンスな宣言的UIライブラリです。JSX構文を使いながらもReactとは根本的に異なるアプローチで、コンパイル時最適化により圧倒的な実行速度を実現します。

どんな時に使う?

  • パフォーマンスが最重要なSPA/ダッシュボード — 大量のデータ更新が頻繁に発生するリアルタイムアプリケーションで、仮想DOMの差分計算コストを排除したい場合
  • Reactライクな開発体験を維持しつつ軽量化したい場合 — JSX・コンポーネントベースの開発に慣れたチームが、バンドルサイズと実行速度を大幅に改善したい場合
  • 既存のバニラJS/Web Components資産と統合したい場合 — 仮想DOMの抽象化レイヤーがなく実DOMを直接操作するため、サードパーティのDOM操作ライブラリとの相性が良い

インストール

# npm
npm install solid-js

# yarn
yarn add solid-js

# pnpm
pnpm add solid-js

プロジェクトの初期セットアップ(推奨)

実際の開発では、SolidJSのコンパイラ(Babel/Viteプラグイン)が必要です。テンプレートから始めるのが最も簡単です。

# Viteテンプレート(TypeScript)
npx degit solidjs/templates/ts my-solid-app
cd my-solid-app
npm install
npm run dev

注意: solid-js単体ではJSXのコンパイルができません。vite-plugin-solidbabel-preset-solid と組み合わせて使用します。

基本的な使い方

SolidJSの最も基本的なカウンターアプリの例です。

// src/App.tsx
import { createSignal } from "solid-js";
import { render } from "solid-js/web";

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

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

render(() => <Counter />, document.getElementById("root")!);

Reactとの決定的な違い

// ❌ Reactの感覚で書くと間違える
<p>{count}</p>

// ✅ SolidJSではSignalは関数呼び出しで値を取得する
<p>{count()}</p>

SolidJSのコンポーネントは一度だけ実行されます。Reactのように再レンダリングされることはありません。リアクティブな値(Signal)が変更されると、その値を参照しているDOM部分だけがピンポイントで更新されます。

よく使うAPI

1. createSignal — リアクティブな状態管理

最も基本的なプリミティブです。getter関数とsetter関数のタプルを返します。

import { createSignal } from "solid-js";

function UserProfile() {
  const [name, setName] = createSignal("太郎");
  const [age, setAge] = createSignal(30);

  // 派生値はただの関数として定義できる
  const greeting = () => `こんにちは、${name()}さん(${age()}歳)`;

  return (
    <div>
      <p>{greeting()}</p>
      <input
        value={name()}
        onInput={(e) => setName(e.currentTarget.value)}
      />
      <button onClick={() => setAge((a) => a + 1)}>年齢+1</button>
    </div>
  );
}

2. createEffect — 副作用の実行

Signal の変更を自動追跡し、依存する値が変わるたびに再実行されます。

import { createSignal, createEffect } from "solid-js";

function Logger() {
  const [query, setQuery] = createSignal("");

  // queryが変わるたびに自動実行される(依存配列の指定は不要)
  createEffect(() => {
    console.log("検索クエリが変更されました:", query());
  });

  // クリーンアップが必要な場合は onCleanup を使う
  createEffect(() => {
    const term = query();
    const timer = setTimeout(() => {
      console.log("デバウンス後の検索:", term);
    }, 300);

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

  return (
    <input
      placeholder="検索..."
      value={query()}
      onInput={(e) => setQuery(e.currentTarget.value)}
    />
  );
}

3. createMemo — メモ化された派生値

計算コストの高い派生値をキャッシュします。依存するSignalが変わった時だけ再計算されます。

import { createSignal, createMemo } from "solid-js";

function ExpensiveList() {
  const [items, setItems] = createSignal<number[]>([1, 2, 3, 4, 5]);
  const [multiplier, setMultiplier] = createSignal(1);

  // itemsまたはmultiplierが変わった時だけ再計算
  const processedItems = createMemo(() => {
    console.log("再計算実行"); // 不要な再計算が起きないことを確認できる
    return items().map((item) => item * multiplier());
  });

  const total = createMemo(() =>
    processedItems().reduce((sum, n) => sum + n, 0)
  );

  return (
    <div>
      <p>合計: {total()}</p>
      <button onClick={() => setMultiplier((m) => m + 1)}>
        倍率: {multiplier()}
      </button>
    </div>
  );
}

4. createResource — 非同期データフェッチ

非同期処理をリアクティブに扱うための専用プリミティブです。

import { createSignal, createResource, Show, Suspense } from "solid-js";

interface User {
  id: number;
  name: string;
  email: string;
}

const fetchUser = async (id: number): Promise<User> => {
  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  if (!res.ok) throw new Error("ユーザーが見つかりません");
  return res.json();
};

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

  // userIdが変わるたびに自動的にfetchUserが再実行される
  const [user, { refetch, mutate }] = createResource(userId, fetchUser);

  return (
    <div>
      <button onClick={() => setUserId((id) => id + 1)}>次のユーザー</button>
      <button onClick={refetch}>再取得</button>

      <Show when={user.loading}>
        <p>読み込み中...</p>
      </Show>

      <Show when={user.error}>
        <p>エラー: {user.error.message}</p>
      </Show>

      <Show when={user()}>
        {(userData) => (
          <div>
            <h2>{userData().name}</h2>
            <p>{userData().email}</p>
          </div>
        )}
      </Show>
    </div>
  );
}

5. createStore — ネストしたオブジェクトのリアクティブ管理

複雑なネスト構造を持つ状態を効率的に管理します。solid-js/store からインポートします。

import { createStore, produce } from "solid-js/store";
import { For } from "solid-js";

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface AppState {
  todos: Todo[];
  filter: "all" | "active" | "completed";
}

function TodoApp() {
  const [state, setState] = createStore<AppState>({
    todos: [],
    filter: "all",
  });

  let nextId = 0;

  const addTodo = (text: string) => {
    setState("todos", (todos) => [
      ...todos,
      { id: nextId++, text, completed: false },
    ]);
  };

  const toggleTodo = (id: number) => {
    setState("todos", (todo) => todo.id === id, "completed", (c) => !c);
  };

  // produce を使うと Immer ライクな記法も可能
  const removeTodo = (id: number) => {
    setState(
      produce((s) => {
        const index = s.todos.findIndex((t) => t.id === id);
        if (index !== -1) s.todos.splice(index, 1);
      })
    );
  };

  const filteredTodos = () => {
    switch (state.filter) {
      case "active":
        return state.todos.filter((t) => !t.completed);
      case "completed":
        return state.todos.filter((t) => t.completed);
      default:
        return state.todos;
    }
  };

  let inputRef!: HTMLInputElement;

  return (
    <div>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (inputRef.value.trim()) {
            addTodo(inputRef.value.trim());
            inputRef.value = "";
          }
        }}
      >
        <input ref={inputRef} placeholder="新しいTodo" />
        <button type="submit">追加</button>
      </form>

      <For each={filteredTodos()}>
        {(todo) => (
          <div>
            <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)}>削除</button>
          </div>
        )}
      </For>
    </div>
  );
}

補足: 制御フローコンポーネント

SolidJSでは条件分岐やリスト描画に専用コンポーネントを使います。

import { Show, For, Switch, Match, Index } from "solid-js";

// 条件分岐
<Show when={isLoggedIn()} fallback={<p>ログインしてください</p>}>
  <Dashboard />
</Show>

// リスト描画(キーベース — 要素の追加・削除に最適)
<For each={items()}>
  {(item, index) => <div>{index()}: {item.name}</div>}
</For>

// リスト描画(インデックスベース — 固定長配列の値更新に最適)
<Index each={items()}>
  {(item, index) => <div>{index}: {item().name}</div>}
</Index>

// 複数条件の分岐
<Switch fallback={<p>不明なステータス</p>}>
  <Match when={status() === "loading"}>読み込み中...</Match>
  <Match when={status() === "error"}>エラーが発生しました</Match>
  <Match when={status() === "success"}>完了</Match>
</Switch>

類似パッケージとの比較

特徴solid-jsReactSvelteVue 3
レンダリング方式コンパイル + Fine-Grained Reactivity仮想DOM差分コンパイル仮想DOM + Reactivity
仮想DOMなしありなしあり
バンドルサイズ(min+gzip)~7KB~42KB~2KB(ランタイム)~33KB
JSX✅(コンパイル時変換)✅(ランタイム)❌(独自構文)✅(オプション)
TypeScript対応✅ ファーストクラス
コンポーネント再実行しない(一度だけ)毎回再実行しないsetup一度だけ
学習コストReact経験者なら低い標準低い中程度
エコシステム規模成長中最大大きい大きい
SSRフレームワークSolidStartNext.jsSvelteKitNuxt

注意点・Tips

1. コンポーネントは関数として一度だけ実行される

これがSolidJSを理解する上で最も重要なポイントです。

// ❌ これはReactの考え方 — SolidJSでは動かない
function BadComponent() {
  const [count, setCount] = createSignal(0);
  
  // ここのログは初回の1回しか出力されない
  console.log("レンダリング:", count());
  
  // 条件分岐をJSで書くとリアクティブにならない
  const message = count() > 5 ? "多い" : "少ない"; // 初回の値で固定される
  
  return <p>{message}</p>;
}

// ✅ リアクティブにするには関数やJSX内に書く
function GoodComponent() {
  const [count, setCount] = createSignal(0);
  
  // 派生値は関数にする
  const message = () => (count() > 5 ? "多い"

比較記事