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-solidやbabel-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-js | React | Svelte | Vue 3 |
|---|---|---|---|---|
| レンダリング方式 | コンパイル + Fine-Grained Reactivity | 仮想DOM差分 | コンパイル | 仮想DOM + Reactivity |
| 仮想DOM | なし | あり | なし | あり |
| バンドルサイズ(min+gzip) | ~7KB | ~42KB | ~2KB(ランタイム) | ~33KB |
| JSX | ✅(コンパイル時変換) | ✅(ランタイム) | ❌(独自構文) | ✅(オプション) |
| TypeScript対応 | ✅ ファーストクラス | ✅ | ✅ | ✅ |
| コンポーネント再実行 | しない(一度だけ) | 毎回再実行 | しない | setup一度だけ |
| 学習コスト | React経験者なら低い | 標準 | 低い | 中程度 |
| エコシステム規模 | 成長中 | 最大 | 大きい | 大きい |
| SSRフレームワーク | SolidStart | Next.js | SvelteKit | Nuxt |
注意点・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 ? "多い"