SolidJSは、JSXで書いたUIを仮想DOMを介さずに実DOMの操作へコンパイルし、値が変わった箇所だけを更新するJavaScriptのUIライブラリです。書き味はReactに近い一方、コンポーネント関数は1回しか実行されません。この違いがReact経験者のつまずきどころにもなります。
2026年10月時点の安定版は solid-js 1.9.17 で、Solid Router 1.0.0 と SolidStart 2.1.0 が組み合わせの最新です。次の大型版 Solid 2.0 は RC(2.0.0-rc.19)の段階にあり、更新の反映タイミングや非同期APIが変わります。本記事ではReactとの違いをビルド結果で比べたうえで、Signalの使い方、propsと制御フローの注意点、SolidStartでの始め方、2.0で変わる点を順に説明します。
まとめ:SolidJSの要点と2026年10月時点の版
- 仕組み:JSXをコンパイル時に実DOM操作へ変換し、Signalを読んだ式だけを再実行する。コンポーネント関数の再実行(Reactの再レンダリング)は起きない。
- Reactとの差:Vite 8.3.5の公式テンプレートをそのままビルドすると、JSはSolidが15.00kB(gzip 5.72kB)、Reactが222.44kB(gzip 69.28kB)。依存配列とメモ化の指定も要らない。
- つまずく2点:propsを分割代入すると更新が止まる。配列の描画は
mapではなく<For>を使う。 - 今から始めるなら:
npm create solid@latestで SolidStart 2(安定版・Node.js 24以上・Vite 8)を選び、solid-js 1.9系で組む。ただしSolid 2.0ではSolidStartは退役し、Viteプラグインのstart modeが後継になる。 - 2.0 RC:setterの直後に読んでも値は変わらず、
createResource・Suspense・Index・onMountは別のAPIに置き換わる。本番投入は正式版を待つ。 - 避ける場面:React前提の既製UI部品や人材に依存するプロジェクト、React Nativeでモバイルも共通化したいプロジェクト。
SolidJSとは:仮想DOMを使わないリアクティブUIライブラリ
SolidJSはRyan Carniato氏が開発し、MITライセンスで公開されているUIライブラリです。リポジトリは GitHubのsolidjs/solid です。npmのダウンロード統計API で2026年10月20〜26日の1週間を見ると、solid-js は7,038,726回、react は165,732,882回でした。ダウンロード数にはCIでの再取得や他パッケージの依存としての取得も含まれるため、開発者数の比ではありませんが、エコシステムの規模の差を見る目安にはなります。
コンパイル後のコード:JSXが実DOMの操作に変わる
Solidのコンパイラ(babel-preset-solid)は、JSXを静的なHTMLテンプレートと、値が変わる場所への差し込み処理に分けます。次のカウンターを例にします。
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
return <button onClick={() => setCount(count() + 1)}>{count()}</button>;
}
babel-preset-solid 1.9.16で変換すると、要点は次のとおりです(読みやすさのため変数名を整理)。
const tmpl = template(`<button>`); // 再利用するDOMテンプレートを用意する
function Counter() {
const [count, setCount] = createSignal(0);
const el = tmpl();
el.$$click = () => setCount(count() + 1);
insert(el, count); // count() が変わったらこの位置だけ書き換える
return el;
}
仮想DOMの差分計算はありません。count を読んでいるのは insert の1か所だけなので、クリックで更新されるのはボタン内のテキストノードだけです。
コンポーネント関数の初期化と更新時の実行範囲
Reactではstateが変わるたびにコンポーネント関数が再実行され、その中の変数や関数も作り直されます。Solidのコンポーネント関数はDOMを組み立てるために1回呼ばれて終わりで、以後に動くのはSignalを読んだ式だけです。関数本体に console.log を置いても1回しか出力されません。後述するpropsの罠も、この性質から生まれます。
SolidJSとReactの違い:更新単位・API・ビルドサイズ
再実行の単位と依存関係の扱い
| 比較項目 | SolidJS 1.9 | React 19 |
|---|---|---|
| 更新の単位 | Signalを読んだ式 | コンポーネント関数 |
| DOM反映 | 実DOMを直接更新 | 仮想DOMの差分を反映 |
| 依存関係 | 実行時に自動で追跡 | 依存配列で宣言 |
| メモ化 | 原則不要 | useMemo等かReact Compiler |
| 状態の読み取り | count()(関数呼び出し) |
count(値) |
| モバイル | 公式の対応なし | React Native |
Reactは再レンダリングの範囲を絞るために useMemo・useCallback を必要に応じて使い、対象のHookでは依存配列を指定します。React Compilerを導入した構成では、こうした手動メモ化の一部を、React Compilerとは?自動メモ化の仕組みと導入方法・useMemoとの違いを解説 で扱うコンパイラが自動化しています。Solidは、どのSignalをどの式が読んだかを実行時に記録するため、そもそも依存配列がありません。
フックとAPIの対応
| React | SolidJS 1.9 | 違い |
|---|---|---|
| useState | createSignal | getter関数を返す |
| useMemo | createMemo | 依存配列なし |
| useEffect | createEffect | 依存配列なし |
| useRef(DOM参照) | ref属性 | 変数へ直接代入 |
| Context | createContext | ほぼ同じ |
| Suspense | Suspense | createResourceと組む |
バンドルサイズの実測:Vite公式テンプレートで15.00kB対222.44kB
Vite 8.3.5の npm create vite@latest で solid-ts と react-ts のテンプレートを作り、手を加えずに npm run build した結果です。どちらもカウンター1つとロゴ画像の初期画面で、CSSの表示サイズはともに4.10kBです。ただし、フレームワークのロゴ画像とそのバンドルへの取り込み方は異なります。
| テンプレート | ライブラリの版 | JS | JS(gzip) |
|---|---|---|---|
| solid-ts | solid-js 1.9.17 | 15.00kB | 5.72kB |
| react-ts | react・react-dom 19.3.1 | 222.44kB | 69.28kB |
この数値は初期テンプレート全体のビルド結果であり、ライブラリ単体のサイズではありません。実アプリでの比率は、画面の実装や追加する依存パッケージによって変わります。初回読み込みのJSを減らしたい埋め込みウィジェットや低速回線向けの画面では効く差です。描画性能の比較は、主要フレームワークを同一条件で測る js-framework-benchmark で版ごとの結果を確認できます。Vite自体の設定は Vite + Reactの環境構築手順|Viteとは・Vite 8.3の変更点とTypeScript 7対応 と共通です。
Signal・Memo・Storeの使い方
createSignalとcreateMemo:状態と派生値
createSignal は「読み取り関数」と「更新関数」の組を返します。値は count() と関数として呼んで読み、この呼び出しが依存関係の記録になります。派生値は createMemo でキャッシュし、元のSignalが変わったときだけ再計算させます。
import { createSignal, createMemo, createEffect } from "solid-js";
const [count, setCount] = createSignal(0);
const double = createMemo(() => count() * 2); // count が変わったときだけ再計算
createEffect(() => {
console.log("double:", double()); // double を読んだので自動で購読される
});
setCount(1); // 1.9系では直後に count() を読むと 1 が返る
APIの詳細は公式の createSignalリファレンス にあります。テンプレート内で count() * 2 と書けば済む軽い計算は、Memoにしなくても問題ありません。重い計算や複数箇所で使う値だけをMemoにします。
createStore:入れ子の状態をパス指定とproduceで更新
オブジェクトや配列を丸ごとSignalに入れると、1か所の変更で全体が更新扱いになります。createStore はプロパティ単位で追跡するため、読んだプロパティが変わった箇所だけが更新されます。
import { createStore, produce } from "solid-js/store";
const [state, setState] = createStore({
user: { name: "佐藤", tags: ["a"] },
todos: [{ id: 1, done: false }],
});
setState("todos", 0, "done", true); // パス指定で1プロパティだけ更新
setState(produce((s) => { // ミュータブルな書き方でまとめて更新
s.user.name = "鈴木";
s.user.tags.push("b");
}));
solid-js 1.9.17で実行すると、state.todos[0].done を読む処理と state.user.name を読む処理は、それぞれ自分の読んだプロパティが変わったときだけ再実行されました。複数のコンポーネントで使う状態は、クライアントだけで動くアプリならモジュールのトップレベルで作ったストアをimportして共有できます。SSRでユーザーごとの状態を扱う場合は、モジュール単位のストアがリクエスト間で共有されないよう、リクエストごとにストアを作ってContextで配ります(公式のStores解説)。
propsの分割代入と制御フロー:React経験者がつまずく2点
propsの分割代入によるリアクティブ性の喪失
Reactでは function Child({ count }) と書くのが普通ですが、Solidでは更新が止まります。コンパイラは親から渡す値を get count() { return count(); } というgetterにしており、分割代入はそのgetterをコンポーネント実行時の1回だけ呼んで値を固定するからです。
function Good(props) {
return <p>{props.count}</p>; // 毎回 props.count を読む=更新される
}
function Bad({ count }) {
return <p>{count}</p>; // 実行時の値で固定=更新されない
}
solid-js 1.9.17で親のSignalを0→2と更新すると、Good は2に追従し、Bad は0のままでした。公式ドキュメントの Props も「分割代入はリアクティブ性を壊すので推奨しない」と明記しています。一部だけ取り出したいときは splitProps、既定値を与えるときは mergeProps を使います。
配列描画の <For> と条件分岐の <Show>
Solidでも items().map(...) は動きますが、配列が変わるたびに全要素のDOMを作り直します。<For> は要素ごとにDOMを保持し、追加・削除・並べ替えの分だけを反映します。条件分岐は三項演算子でも、コンパイラが条件式をメモ化するため、真偽が切り替わらない限り中身は作り直されません(babel-preset-solid 1.9.16の変換結果で確認)。<Show> は、fallback を添えたい場合や、条件に使った値を子に渡したい場合に読みやすく書けます。
import { For, Show } from "solid-js";
<Show when={todos().length > 0} fallback={<p>タスクはありません</p>}>
<ul>
<For each={todos()}>
{(todo, i) => <li>{i() + 1}. {todo.title}</li>}
</For>
</ul>
</Show>
要素の位置は変わらず中身だけが頻繁に変わる配列(入力欄の一覧など)には <Index> を使います。使い分けは公式の List rendering にまとまっています。
非同期データの読み込み:createResourceとSuspense
1.9系でAPIからデータを取るときは createResource を使います。引数のSignalが変わると自動で再取得し、読み込み中かどうかは user.loading、エラーは user.error で分かります。
import { createSignal, createResource, Suspense } from "solid-js";
const fetchUser = async (id) => {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
};
function Profile() {
const [userId, setUserId] = createSignal(1);
const [user] = createResource(userId, fetchUser); // userId が変わると再取得
return (
<Suspense fallback={<p>読み込み中…</p>}>
<p>{user()?.name}</p>
</Suspense>
);
}
取得中は Suspense の fallback が表示されます。キャッシュや再検証まで任せたい場合は TanStack Query のSolid版(@tanstack/solid-query 5.105.0)も選べます。SolidのcreateResourceとSuspenseはSolid 2.0で置き換わるため、後述の移行章も確認してください。
SolidStartで始める:プロジェクト作成とSolidStart 2の変更点
npm create solidによるプロジェクト作成
SolidStartはSolid公式のフルスタックフレームワークで、ファイルベースルーティング、SSR、サーバー関数を備えます。プロジェクトは公式のcreate-solid(0.12.0)で作ります。
npm create solid@latest
# 対話で「SolidStart」→「2 (Stable)」→テンプレート(basic等)を選ぶ
cd solid-project
npm install
npm run dev
basicテンプレートで作ると、依存は @solidjs/start 2.1.0・@solidjs/router 1.0.0・solid-js 1.9.17・vite 8.3.5 になり、package.json の engines は node >=24 でした。src/routes/about.tsx のように src/routes 配下へファイルを置くと /about のルートになり、[id].tsx は動的ルート、[...404].tsx は該当なしのページになります。手順の全体は公式の SolidStart Getting started にあります。
SolidStart 2で変わった点:Vinxi廃止とvite.config.ts
SolidStart 2.0.0は2026年8月4日に安定版になりました。リリースノート によると、ビルド基盤だったVinxiをやめてViteのEnvironment APIを直接使う構成に変わり、Node.js 24以上とVite 8が必須です。設定ファイルは app.config.ts から vite.config.ts へ移り、作成直後の中身は次のとおりです。
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import { solidStart } from "@solidjs/start/config";
export default defineConfig({
plugins: [solidStart(), nitro()],
});
名前が紛らわしいのですが、SolidStart 2は Solid 1.x向け のフレームワークです(peerDependencies は solid-js ^1.9.15)。Solid 2.0向けのSolidStartは出ない予定で、詳しくは次章で説明します。1系の既存プロジェクトは v1からの移行ガイド に沿って移します。2.1.0(10月22日)では、ハイドレーションせずにサーバーでデータだけ先読みする ssr: "data-only" と、103 Early Hintsを送る earlyHints オプションが加わりました。
Solid 2.0 RCの変更点:更新タイミングとAPIの移行
版の組み合わせ:Router 1.0とSolidStart 2はSolid 1系用
| パッケージ | 安定版(Solid 1系) | Solid 2系(プレリリース) |
|---|---|---|
| solid-js | 1.9.17 | 2.0.0-rc.19(next) |
| @solidjs/router | 1.0.0 | 2.0.0-next.43 |
| @solidjs/start | 2.1.0 | 退役(保守リリースのみ) |
| Viteプラグイン | vite-plugin-solid 2.11.14 | @solidjs/vite-plugin 3.0.0-next.52 |
| DOM向けの描画 | solid-js/web | @solidjs/web |
| ストア | solid-js/store | solid-js |
2026年10月28日時点のnpmの値です。Solid RouterのCHANGELOG は、1.0.0を「0.16系と機能は同じで、破壊的変更ではなく版番号の整理」と説明しています。router 1.xはSolid 1.x(とSolidStart 2.0)と組み、Solid 2用のルーターは 2.0.0-next 系に移りました。2.0.0-rc.0は2026年8月12日に出て、その後も数日おきにRCが更新されています。
フレームワーク側の扱いも変わります。2026年8月13日の 2.0 RCの告知 は、SolidStartを3.0として出さずに退役させ、Viteプラグインの start mode(solid({ start: true }))を後継にすると発表しました。サーバー関数はコアへ、ファイルベースルーティングはルーターに依存しない別パッケージへ移ります。告知は同時に、本番で動いているSolidStartは壊れず保守リリースが続くとも書いています。1系で作るならSolidStart 2、2.0の正式版を待って作るならstart modeが入り口になります。
Solid 2.0の更新タイミングとsetter直後の読み取り値
公式の Solid 2.0移行ガイド で最も影響が大きいのは、更新が既定でマイクロタスク単位にまとめられ、setterを呼んでも次のマイクロタスクまで読み取り値が変わらない点です。1.9.17と2.0.0-rc.19で同じ処理を実行すると、結果は次のように分かれました。
setCount(1);
console.log(count()); // 1.9.17 → 1 / 2.0.0-rc.19 → 0
flush(); // 2.0 で今すぐ反映させる関数
console.log(count()); // 2.0.0-rc.19 → 1
setterの直後に値を読んで分岐するコードは、2.0で黙って挙動が変わります。移行ガイドは flush() をテストなど限られた場面だけで使うよう求めています。createEffect も「値を読む関数」と「副作用を実行する関数」の2引数に分かれ、1.x流に1引数で呼ぶと rc.19 では実行時エラーになりました。
Solid 1.xから2.0へのAPI置換一覧
| 1.x | 2.0 |
|---|---|
| createResource | 非同期のcreateMemo+Loading |
| Suspense / ErrorBoundary | Loading / Errored |
| Index | For keyed={false} |
| onMount | onSettled |
| mergeProps / splitProps | merge / omit |
| unwrap | snapshot |
| import元 solid-js/store | import元 solid-js |
| batch | 既定でまとめて反映(flush) |
rc.19のエクスポート一覧を確認すると、createResource・Suspense・Index・batch は実際に無くなっています。2.0ではコンポーネント本体でpropsを分割代入するとトップレベルでの読み取りとして開発時に警告が出るため、前章の罠は2.0で検出しやすくなります。非同期の扱いはReact側の Async Reactとは?Transition・Suspense・useOptimisticで非同期UIを組む実装【React 19.3】 と発想が近づいており、action と createOptimistic による楽観的更新も入りました。
Signalsの広がり:Svelte 5・Angular・TC39提案
Solidが広めたSignalという設計は、ほかのフレームワークにも取り込まれています。Svelteは Svelte 5のRunes発表 で「Svelte 5のリアクティビティはsignalsで動く」「signalsはSolidによって広まった」と書いています。Angularも Angular Signals を中核の状態管理に据え、v21ではゾーンレスが既定になりました(Angular 21の変更点|ゾーンレス既定化とv20からの移行手順・サポート期限)。
JavaScript言語そのものにSignalを入れる TC39のSignals提案 はStage 1です。提案文書は、Angular・Preact・Qwik・Solid・Svelte・Vueなどの作者や保守者の意見をもとに草案を作ったと記しています。
SolidJSを採用すべきでない場面とUIライブラリの選択肢
次の条件に当てはまるなら、Reactを選ぶほうが安全です。
- 既製のUI部品に依存する:MUIやshadcn/uiなどReact専用の部品や、ReactのSDKしか提供していないSaaSを前提にしている。
- モバイルアプリも共通化したい:React Nativeに当たる公式の選択肢がない。
- 採用と引き継ぎ:Solid経験者を確保できる見込みがない、または保守の引き継ぎ先がSolidに対応できない。
- 2.0への移行工数を見込めない:1.9系で作って将来2.0へ移るなら、前章のAPI置き換えが必要になる。正式版が出ても既存アプリの即時移行が必須になるわけではないが、長く保守する前提なら工数を見積もっておく。
UI部品は、Solid向けのヘッドレス(見た目を持たない)ライブラリで組むのが現実的です。npmの最新版はKobalte(@kobalte/core 0.13.14)、React・Vue・Svelte版もあるArk UI(@ark-ui/solid 5.39.3)で、Material UIを移植したSUID(@suid/material 0.19.0)は2025年6月から更新が止まっています。フレームワーク選定から任せたい場合は、要件に合わせて構成を組む フルスクラッチ開発 の支援もあります。
よくある質問
SolidJSとReactはどちらを使うべきですか?
React専用のUI部品やReact Nativeを使う予定があるならReactです。表示の速さと初回読み込みのJSを優先し、UI部品をヘッドレスライブラリで組めるならSolidが向きます。Vite 8の公式テンプレートでは、ビルド後のJSがSolid 15.00kB、React 222.44kBでした。
仮想DOMを使わないのに、なぜ速いのですか?
JSXをコンパイル時に実DOMの操作へ変換し、Signalを読んだ式だけを再実行するからです。コンポーネント関数の再実行と仮想DOMの差分計算が起きないため、更新時の処理が値の変わった箇所に限られます。
SolidJSの最新バージョンは?2.0はもう使えますか?
2026年10月28日時点の安定版は solid-js 1.9.17 です。2.0は 2.0.0-rc.19 がnpmの next タグで公開されています。RCのまま数日おきに更新が続き、SolidStartやUIライブラリの多くもまだ1.9系を前提にしているため、本番は1.9系で組むのが無難です。2.0の解説は公式の Solid 2ドキュメント にまとまっています。
SolidJSでルーティングはどう実装しますか?
SPAなら @solidjs/router 1.0.0 の Router と Route で定義します。SSRやファイルベースルーティングが必要なら SolidStart 2 を使い、src/routes 配下にファイルを置きます。
SolidJS向けのUIライブラリはありますか?
ヘッドレスのKobalte(0.13.14)とArk UI(5.39.3)が更新を続けています。Material UI風の見た目が欲しい場合はSUIDがありますが、2025年6月の0.19.0以降は新しい版が出ていません。