← Notes

React 19.3 と SolidJS 2.0 RC(2.0.0-rc.14)を公式ドキュメントから徹底比較する

React と SolidJS はどちらも JSX でコンポーネントを書くため、コードの見た目はよく似ています。ところが内側の実行モデルはほぼ正反対で、状態更新の伝わり方、非同期の扱い、サーバーとの境界の引き方まで設計思想が分かれています。この記事では、2026年10月時点の React 最新安定版と、リリース候補(RC)段階にある Solid 2.0(2.0.0-rc.14)を、公式ドキュメントとリリースノートをもとに観点ごとに比べます。

比較対象のバージョン

まず前提をそろえておきます。バージョンと日付は react.dev の Versions ページ、各リリースノート、npm レジストリの公開日時で確認しました。

  • React: 19.3.0(2026年9月9日リリース、npm の latest)。19.3 では <ViewTransition> と Fragment Refs が安定版になり、react-dom に browser() が追加されました。
  • React Compiler: 1.0(2025年10月7日、パッケージは babel-plugin-react-compiler)。React 本体とは別にリリースされるビルド時ツールです。
  • Solid 2.0: 2.0.0-rc.14(GitHub のリリースは 2026年10月8日、npm の next タグ)。npm の latest は 1.x 系の 1.9.17 のままです。
  • Solid 2.0 の経緯: 2.0.0-beta.0 が 2026年3月3日、RC の最初の版 2.0.0-rc.0("The Big <Reveal>")が 2026年8月13日(npm 上は 8月12日 UTC)に出ています。
Solid 2.0 はまだ正式リリースではありません。RC の発表では API は凍結されたとされていますが、rc.14 でも小文字の onclick がイベントではなく属性として扱われるようになるなど、破壊的変更が入っています。本番に投入する前に、使う RC 番号での挙動を確認してください。この記事の Solid に関する記述は、すべて 2.0.0-rc.14 時点のものです。

Solid 1.x と SolidStart のドキュメントは引き続き docs.solidjs.com にあり、2.0 のドキュメントは別サイトの v2.solidjs.com で公開されています。本文で「Solid」と書くときは、特に断りがなければ 2.0.0-rc.14 を指します。

レンダリングモデル:再レンダリングか、fine-grained reactivityか

React:コンポーネント関数を何度も呼ぶ

React では、状態が変わるとそのコンポーネント関数がもう一度実行されます。返ってきた要素ツリーを前回と比較し、差分だけを DOM に反映するのが基本の流れです。コンポーネントは「状態から UI を求める純粋な関数」として扱われ、関数本体のローカル変数はレンダーごとに作り直されます。

import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);
  const doubled = count * 2; // レンダーのたびに再計算される

  console.log("render"); // クリックのたびに出力される
  return (
    <button onClick={() => setCount((c) => c + 1)}>
      {count} × 2 = {doubled}
    </button>
  );
}

この方式はわかりやすい一方、親が再レンダリングされると子もまとめて再実行されがちです。そのコストを抑えるのが memo、useMemo、useCallback で、現在はその役目の大半を React Compiler が引き受けます。

Solid:コンポーネントは一度だけ実行される

Solid のコンポーネント関数はマウント時に一度だけ実行されます。状態はシグナルやストアに入れておき、値が変わると、その値を読んでいる JSX の式や computation だけが更新されます。仮想 DOM はなく、更新は該当する DOM ノードに直接届きます。

import { createSignal } from "solid-js";

export function Counter() {
  const [count, setCount] = createSignal(0);
  const doubled = () => count() * 2; // 関数にしておくと読まれる場所で評価される

  console.log("setup"); // 最初の一回だけ出力される
  return (
    <button onClick={() => setCount((c) => c + 1)}>
      {count()} × 2 = {doubled()}
    </button>
  );
}

見た目の差は count と count() くらいですが、意味はまったく違います。Solid では count() という読み取りが依存関係の登録を兼ねていて、どこで呼んだかによってリアクティブになるかどうかが決まります。

コンパイラの役割の違い

両者ともコンパイラを持っていますが、目的が異なります。

  • React Compiler は、Rules of React に従ったコードを解析して、値や関数、JSX の自動メモ化を差し込む最適化ツールです。使わなくても React は動きます。公式ドキュメントでは React 17・18・19 が対象とされ、Expo SDK 54 以降では標準で有効、Vite と Next.js のテンプレートではオプトインです。
  • Solid のコンパイラは JSX を実際の DOM 生成コードとfine-grained な更新処理に変換するもので、これがないと Solid の JSX は成り立ちません。2.0 RC では OXC ベースの Rust 製コンパイラが @solidjs/vite-plugin の既定になり、Babel プリセットは選択肢として残っています。

状態管理:Hooks とシグナル

基本の状態

React の useState は、そのレンダー時点の値(スナップショット)を返します。setter を呼んでも、同じイベントハンドラ内の count 変数は変わらず、次のレンダーで新しい値が見えます。

Solid 2.0 では、書き込みはマイクロタスク単位でまとめて反映されます。setter を呼んだ直後に読むと古い値が返り、キューが flush されてから新しい値になります。1.x の batch は廃止され、すべての書き込みが既定でバッチされるようになりました。同期的に確定させたいときは flush() を使いますが、用途はテストや命令的なコードに限るよう案内されています。

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

const [count, setCount] = createSignal(0);
setCount(1);
count(); // まだ 0
flush();
count(); // 1

「書いた直後には読めない」という点は React のスナップショットと似ていますが、仕組みはまったく異なります。React はコンポーネントの再実行によって新しい値を渡し、Solid はリアクティブグラフの flush によって購読者へ通知します。

派生値:useMemo / React Compiler と createMemo

React で派生値を作るときは、レンダー中に普通に計算するのが基本です。計算が重い場合は useMemo で包みます。React Compiler を導入すると、この手作業のメモ化はほとんど不要になります。公式ドキュメントは、新しいコードではコンパイラに任せ、useMemo / useCallback は Effect の依存配列に渡す値など精密な制御が要る場面だけで使うよう勧めています。既存のメモ化は、消すとコンパイル結果が変わりうるので残しておくのが無難とされています。

Solid では、軽い派生値は () => count() * 2 のような関数で十分です。複数の場所から読まれる値や計算が重い値は createMemo でキャッシュします。2.0 では createMemo の第2引数がオプションオブジェクトになり、initialValue は廃止されました。前回値は compute 関数の引数 prev で受け取り、初回は undefined です。

2.0 では、関数を渡す形の createSignal(fn) も加わりました。これは「書き込み可能な派生値」で、props に追従しつつユーザーが上書きできる入力欄のような状態を、同期用の effect なしで表せます。

function NameField(props: { name: string }) {
  // props.name に追従するが、ユーザーの入力で一時的に上書きできる
  const [name, setName] = createSignal(() => props.name);
  return <input value={name()} onInput={(e) => setName(e.currentTarget.value)} />;
}

React で同じことをするなら、react.dev の「You Might Not Need an Effect」が紹介しているように、key を変えて state ごとリセットするか、レンダー中に前回の props と比べて state を調整します。

Effect

React の useEffect は、レンダー後に実行される副作用を依存配列とともに宣言します。19.2 で追加された useEffectEvent を使うと、Effect 内で使う値のうち「最新値を読みたいが、変化しても Effect を再実行したくない」ものを切り出せます。

import { useEffect, useEffectEvent } from "react";

function ChatRoom({ roomId, theme }: { roomId: string; theme: string }) {
  const onConnected = useEffectEvent(() => {
    showNotification("Connected!", theme);
  });

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on("connected", () => onConnected());
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // onConnected は依存に含めない
}

Solid 2.0 の createEffect は、依存を追跡する compute 関数と、追跡しない effect 関数の2段に分かれました。依存配列を手で書く代わりに、compute 側で読んだものが依存になります。effect 側の読み取りは追跡されないので、React で useEffectEvent が担う「読むけれど依存にしない」役割は、構造そのものに組み込まれています。クリーンアップは effect 関数の戻り値で返します。

import { createEffect } from "solid-js";

function ChatRoom(props: { roomId: string; theme: string }) {
  createEffect(
    () => props.roomId, // compute: ここで読んだ値だけが依存になる
    (roomId) => {
      // effect: 追跡されない。props.theme を読んでも再実行されない
      const connection = createConnection(roomId);
      connection.on("connected", () => showNotification("Connected!", props.theme));
      connection.connect();
      return () => connection.disconnect();
    }
  );
  return null;
}

どちらの公式ドキュメントも「Effect は外部システムとの同期のためのもの」という立場で、他の状態から計算できる値を Effect で同期することを戒めています。React には "You Might Not Need an Effect"、Solid 2.0 には "Avoid unnecessary effects" というガイドがあります。

非同期とデータ取得

ここは両者の差がいちばん大きく、Solid 2.0 の目玉でもあります。

React:Suspense、use、Transition、Actions

React では、Promise を use に渡して読むと、解決するまでコンポーネントがサスペンドし、最寄りの <Suspense> のフォールバックが表示されます。reject された Promise は最寄りの Error Boundary に伝わります。

import { Suspense, use } from "react";

function Profile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

export function Page({ userPromise }: { userPromise: Promise<User> }) {
  return (
    <Suspense fallback={<Skeleton />}>
      <Profile userPromise={userPromise} />
    </Suspense>
  );
}

注意したいのは、Client Component のレンダー中に Promise を作ってはいけない点です。サスペンドしたレンダーの状態は保持されないため、毎回新しい Promise ができてフォールバックが出続けます。公式ドキュメントは、Promise をイベントハンドラ、ルートローダー、Server Component などレンダーの外で作るか、キャッシュしてから渡すよう求めています。実際には、フレームワークやデータ取得ライブラリに任せることが多いでしょう。

表示中の画面を保ったまま裏で更新したいときは、startTransition / useTransition で更新を Transition として扱います。React 19 では、Transition に渡す非同期関数を Actions と呼び、次のフックが追加されました。

  • useActionState: Action の最新の結果、ラップされた Action、pending 状態を返す。
  • useOptimistic: リクエスト中だけ楽観的な値を表示し、完了や失敗で元に戻す。
  • useFormStatus(react-dom): 親の <form> の送信状態を読む。
  • <form action={fn}>: フォームに関数を直接渡すと送信が Action になり、成功時に非制御フォームはリセットされる。

19.3 では、遅い Transition が無関係な Transition を待たせないよう、Transition が独立してレンダーされるようになりました。

Solid 2.0:非同期をリアクティブグラフに組み込む

Solid 2.0 では、computation が Promise や async iterable を返せます。createMemo が Promise を返すと、読む側には解決済みの値が渡され、型も Promise<User> ではなく User になります。値がまだない間の読み取りは「未準備」として扱われ、最寄りの <Loading> がフォールバックを出します。1.x の createResource は廃止され、この仕組みに置き換わりました。

import { createMemo, isPending, Loading } from "solid-js";

function Profile(props: { id: string }) {
  const user = createMemo(() => fetchUser(props.id));

  return (
    <Loading fallback={<Skeleton />}>
      <h1 class={{ stale: isPending(user) }}>{user().name}</h1>
    </Loading>
  );
}

一度表示した後に props.id が変わっても、画面は空になりません。古い内容を表示したまま新しいリクエストを進め、同じ更新に含まれる他の書き込みも一緒に待たせて、準備ができたらまとめて反映します。公式ドキュメントでは "held updates" と呼ばれ、オプトインの API なしで常に有効です。React で Transition を使って得る挙動が、Solid では既定になっています。そのため 2.0 では startTransition と useTransition が削除されました。

保留中の状態は、次の手段で扱います。

  • isPending(fn): 新しい答えが届く途中かどうかを返す。古い内容を薄く表示するなどのフィードバックに使う。
  • latest(fn): コミット済みの値ではなく、更新が向かっている先の値を読む。選択中のタブのハイライトなど、入力側の UI をすぐ動かしたいときに使う。データ側に使うと画面の各部分が食い違うので避けるよう案内されている。
  • <Loading on={key}>: 対象が切り替わったときにはフォールバックを出し直したい場合に、追跡するキーを指定する。
  • <Errored>: 1.x の ErrorBoundary の後継。フォールバックは (error, reset) を受け取り、データの変化や refresh でコンテンツが復帰しうる。
  • <Reveal>: 1.x の SuspenseList の後継で、兄弟の境界の表示順を調整する。

注意点は、依存の追跡が同期的に行われることです。async 関数の中で最初の await より後に読んだシグナルは追跡されないので、リアクティブな入力は関数の冒頭で読み切っておく必要があります。

書き込み:Actions と action

React の Actions に相当する Solid 2.0 の仕組みが action です。ジェネレーター関数(または async ジェネレーター)を受け取り、1回の呼び出しを1つのトランザクションとして扱います。createOptimistic / createOptimisticStore と組み合わせると、Action の中で行った書き込みは仮の値としてすぐ画面に反映され、Action が落ち着いた時点で自動的に巻き戻されるか、確定した値に置き換わります。

import { action, createOptimisticStore, refresh } from "solid-js";

const [todos, setTodos] = createOptimisticStore(() => api.getTodos(), []);

const addTodo = action(function* (todo: Todo) {
  setTodos((t) => {
    t.push(todo); // 楽観的な更新
  });
  yield api.addTodo(todo); // サーバーへの書き込みを待つ
  refresh(todos); // 派生した読み取りを再計算する
});

yield が待機点で、yield した Promise の完了後にトランザクションへ戻ります。素の await ではトランザクションに戻らないため、await の後に書き込むなら、その前に引数なしの yield を置くよう公式リファレンスは指示しています。refresh は対象を強制的に再実行させますが、それだけでは isPending が立ちません。保留表示が必要なときは、先に affects を呼ぶ必要があります。

対応関係をまとめると、useActionState / useTransition の pending が isPending、useOptimistic が createOptimistic / createOptimisticStore、Transition でラップした非同期関数が action にあたります。React はフックとコンポーネントの単位で、Solid はリアクティブグラフの単位でこれらを設計している、という違いがあります。

制御フローと JSX の違い

条件分岐とリスト

React の JSX はただの式なので、条件分岐には三項演算子や &&、リストには array.map() と key を使います。毎レンダー全体を再評価して差分を取る前提だからこそ成り立つ書き方です。

Solid でも三項演算子や && はそのまま動きますが、<Show> と <For> を使うとフォールバックや型の絞り込みが得られ、DOM の再利用も適切に制御できます。

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

function UserList(props: { user?: User; todos: Todo[] }) {
  return (
    <>
      <Show when={props.user} fallback={<SignIn />}>
        {(u) => <Greeting name={u().name} />}
      </Show>

      <ul>
        <For each={props.todos} keyed={(todo) => todo.id} fallback={<li>No todos</li>}>
          {(todo, index) => (
            <li>
              {index() + 1}. {todo().text}
            </li>
          )}
        </For>
      </ul>
    </>
  );
}

2.0 で For は大きく変わりました。1.x の Index は廃止され、keyed プロパティで行の同一性を切り替えます。子関数が受け取る引数の形はモードごとに異なるので注意が必要です。

  • 既定(keyed 省略または true): 要素の参照で行を照合する。item は生の値、index はアクセサ。
  • keyed={false}: 位置で照合する(1.x の Index 相当)。item がアクセサ、index は固定の数値。
  • keyed={(item) => key}: 返したキーで照合する。item も index もアクセサになる。サーバーから同じ id で新しいオブジェクトが返ってきても DOM が保たれる。

ほかにも、<Show> の関数の子は(keyed なしの場合)アクセサを受け取ること、件数だけで位置的に描画する <Repeat> が追加されたこと、<Dynamic> が非推奨になり dynamic(() => src) で作ったコンポーネントを使う形になったことなどが変わっています。

props の分割代入に注意

Solid の props は getter を持つオブジェクトで、親で動的に渡された値は読み取ったタイミングで評価されます。コンポーネントは一度しか実行されないため、関数本体で分割代入すると、その時点の値を一度読んだだけになりリアクティビティが失われます。

// NG: 分割代入した時点の値で固定される
function Price({ amount }: { amount: number }) {
  return <span>{amount}</span>;
}

// OK: JSX の中で props.amount を読む
function Price2(props: { amount: number }) {
  return <span>{props.amount}</span>;
}

React では分割代入が当たり前なので、移行時にいちばん引っかかりやすい点です。2.0 の開発ビルドでは、コンポーネント本体のトップレベルでのリアクティブな読み取りに警告が出ます。props の加工には、mergeProps から改名された merge と、splitProps の後継の omit を使います。

DOM 属性の扱い

React は className、キャメルケースの style キー、数値への自動 px 付与など、DOM プロパティ寄りの独自の約束事を持っています。Solid 2.0 は HTML に近い方向へ寄せていて、次のように扱います。

  • 属性は class。文字列のほか、条件付きのオブジェクトや配列を渡せる(1.x の classList はここに統合)。
  • style のキーは "background-color" のような CSS 名で、数値に単位は自動で付かない。
  • 1文字ごとの入力を拾うには、React の onChange ではなく onInput を使う。
  • 1.x の use: ディレクティブは廃止され、ref に渡すディレクティブファクトリ(配列で合成可能)に置き換わった。

ストア:イミュータブルとミュータブル

React の状態はイミュータブルに扱うのが原則です。オブジェクトや配列を更新するときは、スプレッド構文などで新しい値を作って setter に渡します。参照が変わることが「変わった」の合図で、memo や React Compiler によるメモ化もこの前提の上に成り立っています。入れ子が深くなると、useReducer や Immer のようなライブラリに頼る場面が増えます。

setTodos((prev) =>
  prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
);

Solid の createStore はプロキシベースのストアで、プロパティ単位で購読されます。2.0 では setter が「ドラフトを受け取って直接書き換える」形が標準になりました。1.x のパス指定形式や produce は廃止され、createMutable もドラフト setter に統合されています。

import { createStore } from "solid-js";

const [state, setState] = createStore({ todos: [] as Todo[] });

setState((s) => {
  const todo = s.todos.find((t) => t.id === id);
  if (todo) todo.done = !todo.done; // 変更したプロパティを読んでいる箇所だけが更新される
});

// 新しい値を返すこともできる(配列はインデックスごとに置き換え)
setState((s) => s.todos.filter((t) => !t.done));

書き方は Immer に似ていますが、Solid ではドラフトへの書き込みがそのままfine-grained な通知になります。2.0 では次の API も加わったり、名前が変わったりしています。

  • createStore(fn, seed): 関数形式の派生ストア(プロジェクション)。async 関数を渡してサーバーのデータを取り込み、キーで照合させることもできる。
  • createProjection: 1.x の createSelector の後継として案内されている。
  • reconcile: 選択したドラフトに対して適用する形になった。
  • snapshot: unwrap の後継。購読せずに素の値を取り出す。
  • deep: effect の中で入れ子のプロパティすべてを購読する。
  • storePath: 1.x のパス形式 setter からの移行用ヘルパー。

Context とライフサイクル

Context

Context の書き方はかなり近づいています。React 19 以降は <ThemeContext value="dark"> のように Context 自体をプロバイダーとして書け、<Context.Provider> は将来非推奨になる予定です。読み取りは useContext のほか、条件分岐の後でも呼べる use(ThemeContext) も使えます。19.3 では、'use client' モジュールで定義した Context を Server Component から直接レンダーできるようになりました。

Solid 2.0 でも createContext が返す Context がそのままプロバイダーになり、<ThemeContext value={...}> と書きます。デフォルト値を省略した Context をプロバイダーなしで useContext すると ContextNotFoundError を投げるので、「必ずプロバイダーの内側で使う」という前提を型と実行時の両方で表せます。

import { createContext, useContext } from "solid-js";

const ThemeContext = createContext<"light" | "dark">();

export function App() {
  return (
    <ThemeContext value="dark">
      <Toolbar />
    </ThemeContext>
  );
}

function Toolbar() {
  const theme = useContext(ThemeContext); // プロバイダーがなければ ContextNotFoundError
  return <div class={theme}>...</div>;
}

モジュールレベルのシグナルやストアで共有状態を持つのは避けるよう、Solid の移行ガイドは注意しています。所有者(owner)がいないうえに、サーバーでは全リクエストで同じインスタンスが共有されてしまうからです。

ライフサイクル

React には独立したライフサイクル API はなく、マウント時の処理は useEffect(() => { ...; return cleanup; }, []) で書きます。Strict Mode の開発ビルドでは Effect が2回実行され、クリーンアップの漏れが表に出るようになっています。19.3 からはハイドレーション時にも同じように2回実行されます。19.2 で入った <Activity mode="hidden"> を使うと、状態を残したまま子の Effect をアンマウントして画面を隠せます。

Solid 2.0 では onMount が onSettled に置き換わりました。onSettled はリアクティブグラフが落ち着いた後に一度だけ実行され、コンポーネント本体のような所有者のあるスコープで呼んだ場合は、戻り値のクリーンアップ関数が破棄時に呼ばれます。コールバックの中で onCleanup や flush() は使えません。

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

export function useViewportWidth() {
  const [width, setWidth] = createSignal(window.innerWidth);
  onSettled(() => {
    const onResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  });
  return width;
}

Solid では、コンポーネントが一度だけ実行されるので「マウント時」と「関数本体」がほぼ同じタイミングになります。ライフサイクル API が必要になるのは DOM が整った後の処理と後片付けくらいです。

SSR、ストリーミング、サーバーとの境界

React:Server Components と Server Functions

React のサーバー側の機能は大きく3つの層に分かれます。

  • ストリーミング SSR: react-dom/server の renderToPipeableStream(Node)や renderToReadableStream(Web Streams)で、Suspense 境界ごとに HTML を流す。
  • 静的生成と Partial Pre-rendering: react-dom/static の prerender で静的部分を事前に作り、19.2 で入った resume / resumeAndPrerender 系 API で後から動的部分を埋める。
  • React Server Components(RSC): ビルド時やリクエストごとに、クライアントとは別の環境でレンダーされるコンポーネント。'use client' で境界を示し、"use server" を付けた Server Functions をクライアントから呼べる。

RSC は React の機能ですが、フレームワーク側の実装コストが大きく、react.dev は現時点で最も完全な実装として Next.js の App Router を挙げています。19.3 では、Server Component から Context を直接レンダーできるようになったほか、use(browser()) でコンポーネントをサーバーレンダリングの対象から外せるようになりました(サーバーではサスペンドして最寄りの Suspense のフォールバックが HTML に入ります)。

2025年12月には、RSC に関するセキュリティ脆弱性の告知が react.dev のブログに2回掲載されました(12月3日の Critical な脆弱性と、12月11日のサービス拒否・ソースコード露出)。Server Functions は外部から到達できるエンドポイントだという前提を忘れないことが大切です。

Solid 2.0:start モードとコアに入ったサーバー関数

Solid 2.0 の SSR API は @solidjs/web にまとまっています。

  • renderToString: 同期的に HTML を返す。Loading 境界の中で保留中の読み取りはフォールバックとして出力され、後から届くことはない。
  • renderToStream: 同期的なシェルを先に送り、各 Loading 境界の中身が落ち着くたびにストリームする。1.x の renderToStringAsync の代わりに、これを await して完全な HTML を得る使い方も案内されている。
  • hydrate: サーバーで描画済みの HTML にイベントとリアクティブな束縛を結び付ける。NoHydration / Hydration で範囲を制御できる。

大きく変わったのはフルスタック構成です。2.0 RC の発表で、SolidStart は 3.0 を出さずに役目を終えることが告げられました。その機能は次のように分かれています。

  • サーバー関数("use server")はコアの @solidjs/web/server-functions に移った。
  • サービング層は @solidjs/vite-plugin の start モードになった。
  • ファイルシステムルーティングはルーター非依存の別パッケージになった。

SolidStart にはメンテナンスリリースが続きます。ただし、リリース済みの SolidStart 2(npm の @solidjs/start 2.0.x)は Solid 1 系で動き、Solid 2 には対応していません。公式の移行ガイドは、既存の SolidStart アプリに Solid 2 を入れず、start モードで新しいアプリを作ってコードを移すよう案内しています。

import { defineConfig } from "vite";
import solid from "@solidjs/vite-plugin";

export default defineConfig({
  // start: true だけなら CSR の SPA。ssr: true で SSR とハイドレーションになる
  plugins: [solid({ start: true, ssr: true })],
});
import { reload } from "@solidjs/web";

export async function addTodo(title: string) {
  "use server";
  await db.insert(title);
  return reload({ revalidate: "todos" });
}

start モードの本番ビルドは、静的な dist/client と、Web 標準の fetch ハンドラを default export するサーバーモジュールを出力します。RC の発表では、Cloudflare、Netlify、Nitro の Vite プラグインがこのサーバー環境をそのまま使えるため、Solid 専用のアダプターは要らないとされています。サーバーの秘密情報がクライアントのチャンクに漏れていないかをビルド時に調べる仕組みもあります。

Server Components にあたる機能は、Solid 2.0 RC では serverFunctions: { components: true } の背後にある実験的なプレビューにとどまり、正式な発表は 2.0 安定版の後に予定されています。この領域では、React のほうが本番での実績もエコシステムの対応も大きく先行しています。

なお、どちらの公式ドキュメントも、サーバー関数は誰でもリクエストを送れる公開エンドポイントであり、引数を信用せず関数の中で認可を確認するよう強調しています。

パフォーマンス特性とバンドルサイズ

まず、出典のある数字だけを挙げます。

  • React Compiler 1.0 の告知記事は、Meta の Quest Store での結果として、初回ロードとページ間遷移が最大12%改善し、一部のインタラクションは2.5倍以上速くなり、メモリ使用量は変わらなかったと紹介しています(React Conf の講演が出典。結果はアプリによって異なると注記あり)。
  • Solid 2.0 RC の発表は、新しい OXC コンパイラと従来の babel-plugin-jsx のコンパイル時間を比べています。88ファイル・175KB のフィクスチャ群で 440ms → 19ms(23倍)、1MB の単一モジュールで 24,975ms → 70ms(355倍)です。これはビルド速度の数字で、実行時の速度ではありません。

一方で、Solid 2.0 のパフォーマンスガイドは「速度についての主張はしない」と明言していて、ベンチマークも計測済みのバンドルサイズも載せていません。react.dev にも公式のバンドルサイズの数字は見当たりませんでした。そこでこの記事でも、ランタイム速度やバンドルサイズを数字で比べることはしません。気になる場合は、自分のアプリの構成で vite build の出力サイズを測るか、計測方法の公開されている第三者ベンチマークを、条件を確認したうえで参照するのがよいでしょう。

数字を抜きにしても、設計から読み取れる特性の違いはあります。

  • React はコンポーネントの再実行がコストの単位です。重くなるのは「再レンダリングが広く波及する」ときで、React Compiler の自動メモ化はまさにこれを抑えるためのものです。Transition や <Activity> による優先度の制御もそろっています。
  • Solid はコンポーネントを再実行しないので、コストの単位は個々の computation と購読です。公式ガイドが挙げるボトルネックも、重い派生値、多数の購読者に届く書き込み、値を中継するだけの effect、作り直されるリストなど、グラフの形に関わるものです。開発ビルドでは costs() や feedback() で、どのスコープがなぜ再実行されたかを追えます。

エコシステムとメタフレームワーク

React

react.dev は新しいアプリをフレームワークで始めることを推奨しています。挙げられているのは、Next.js(App Router)、React Router、Expo(ネイティブアプリ)で、新興の選択肢として TanStack Start と RedwoodSDK も紹介されています。フレームワークを使わない場合は、Vite、Parcel、Rsbuild で一から組む手順が用意されています。2025年2月には Create React App の終了が告知されました。

UI ライブラリ、フォーム、データ取得、テスト、React Native まで含めたエコシステムの厚みは、比べるまでもなく React が上です。2026年2月には React Foundation が Linux Foundation 傘下で発足し、プロジェクトの運営体制も変わりました。

Solid 2.0

Solid 2.0 RC は、コア、DOM レンダラー、ルーター、head 管理、Vite プラグインが連携して動く前提で設計されています。

  • Solid Router 2.0: RC と同時に公開。ルート、パラメーター、ナビゲーションまで型が付く。query() と action() でサーバー関数をラップし、JavaScript の読み込み前のフォーム送信や single-flight mutation に対応する。
  • TanStack Router / Start: 2026年4月10日に Solid 2.0 ベータへの対応が告知され、v2 ドキュメントにも TanStack Router を使う構成が載っている。執筆時点で npm の @tanstack/solid-start は latest が 1.x 系で、Solid 2 向けは beta / rc タグのプレリリース。
  • Solid Meta 1.0: 2.0 に組み込まれた head レジストリの上の薄い層になった(@solidjs/web にも useHead がある)。
  • RC の発表では、Solid Primitives、Kobalte、Solid Testing Library、Storybook、AG Grid が 2.0 対応を表明している。

ただし、@solidjs/vite-plugin はまだ 3.0.0-next.x のプレリリースです。サードパーティのライブラリもすべてが 2.0 に追いついているわけではないので、依存ライブラリごとに対応状況を確かめる必要があります。

開発体験とツール

React 側で押さえておきたいものは次のとおりです。

  • eslint-plugin-react-hooks の recommended プリセットに React Compiler 由来のルール(set-state-in-render、set-state-in-effect、refs など)が入った。コンパイラを導入していなくても使える。
  • 19.2 で Chrome DevTools のパフォーマンスプロファイルに Scheduler と Components のトラックが追加された。
  • React DevTools、Strict Mode の二重実行、Fast Refresh といった長く使われてきた道具がそろっている。
  • 一方で、依存配列、メモ化、「レンダー中に Promise を作らない」といった Rules of React を理解しておく必要がある。コンパイラやリンターが助けてくれるとはいえ、学ぶことは少なくない。

Solid 2.0 側で押さえておきたいものは次のとおりです。

  • OXC ベースのコンパイラで、JSX の変換が大幅に速くなった(数字は前の節を参照)。
  • 開発ビルドの警告が充実している。コンポーネント本体のトップレベルでの読み取り、リアクティブなスコープ内での書き込み、非同期のウォーターフォール(ASYNC_WATERFALL)、フィードバックのない保留(SILENT_HOLD)などを検出する。
  • enablePerformanceTracks() でブラウザのパフォーマンスパネルに Solid のトラックを出せる(rc.14 では、帰属コストの計測は既定でオフになった)。
  • 1.x からの移行には npx solid-migration-assistant があり、古い import、引数1つの createEffect、onMount、Suspense / Index / classList などを洗い出してくれる。
  • 一方で、「どこで読んだか」によってリアクティブかどうかが決まるモデルには慣れが要る。分割代入、await 後の読み取り、when={user} と when={user()} の違いなど、React にはない種類の落とし穴がある。

移行のポイント

Solid 1.x から 2.0 へ

公式の移行ガイドは長いので、影響の大きいものを抜き出します。

  • パッケージ: solid-js/web → @solidjs/web、solid-js/store → solid-js、vite-plugin-solid → @solidjs/vite-plugin。jsxImportSource は @solidjs/web に変える。
  • 書き込みはマイクロタスクでバッチされる。batch は削除。テストではアサーションの前に flush() を呼ぶ。
  • createEffect は compute と effect の2段に分かれた。on(...) は削除。クリーンアップは戻り値で返す。
  • onMount → onSettled。createComputed は削除。
  • createResource → async な createMemo。refetch() → refresh()、mutate() → action と createOptimistic / createOptimisticStore、resource.latest → latest()。
  • startTransition / useTransition は削除。保留中の更新は自動で保持され、isPending で表現する。
  • Suspense → Loading、ErrorBoundary → Errored、SuspenseList → Reveal、Index → <For keyed={false}>、Context.Provider → Context そのもの。
  • ストア: ドラフト setter が標準。produce と createMutable は削除。unwrap → snapshot、mergeProps → merge、splitProps → omit。
  • JSX: classList → class のオブジェクト・配列形式、use: → ref のディレクティブファクトリ、on: / attr: / bool: などの名前空間も整理された。
  • SSR: renderToString は同期になり、renderToStringAsync は await renderToStream(...) に置き換わる。

React から Solid へ

Solid 2.0 のドキュメントには React からの移行ガイドもあり、対応表と段階的な移行手順が載っています。考え方の切り替えとして大事なのは次の点です。

  • コンポーネントは一度しか実行されない。本体での読み取りはスナップショットになるので、リアクティブにしたい読み取りは JSX か computation の中で行う。
  • useState はスカラーなら createSignal、オブジェクトやコレクションなら createStore にする。値は count() のように関数呼び出しで読む。
  • props は分割代入しない。props.x のまま読むか、加工するなら createMemo や merge / omit を使う。
  • useMemo → createMemo(単純な派生なら普通の関数で十分)。useEffect → 2段の createEffect。依存配列はなくなる。
  • useRef は、ref コールバックで let 変数に代入する形になる。forwardRef 相当は props.ref を配列で合成する。
  • key を付けた map は <For> の keyed に、位置で描画するなら <Repeat> にする。
  • Suspense → Loading、startTransition → 自動の held updates と isPending、Actions → action。

移行ガイドが勧める順序は、アプリの外枠とルーティングを用意し、フレームワークに依存しないコード(型、バリデーション、API クライアントなど)を移し、葉のコンポーネント、共有状態と Context、派生値と Effect、非同期処理の順に移して、最後に React のルートを外す、というものです。React のコンポーネントを Solid のツリーの中に混ぜないことも注意点として挙げられています。

どちらを選ぶか

React が向いているのは次のようなケースです。

  • チームの多くが React に慣れていて、採用や引き継ぎのしやすさを重視する。
  • Next.js の App Router のように、Server Components を本番で使い込んだ構成が欲しい。
  • React Native / Expo でモバイルとコードや知識を共有したい。
  • UI コンポーネント、フォーム、データグリッドなど、既製のライブラリを幅広く使いたい。
  • 安定版の API で長期運用したい。React 19.3 は安定版で、React Compiler 1.0 もある。

Solid 2.0 が向いているのは次のようなケースです。

  • 頻繁に更新される UI(ダッシュボード、エディタ、大量のリストなど)で、メモ化を意識せずに更新範囲を細かく絞りたい。
  • 非同期データの読み込み、保留中の表示、楽観的更新を、フックの組み合わせではなく一つのモデルで扱いたい。
  • Vite を中心とした軽い構成で、SSR とサーバー関数を後から足したい。
  • RC のプレリリースを追いかけ、問題を報告しながら使う余裕がある。新規プロジェクトや、影響範囲を限定できる部分から試すのが現実的。

既存の Solid 1.x プロジェクトについては、2.0 の正式リリースと、使っているライブラリの対応を待ってから移行するのが無難です。SolidStart を使っている場合は、Solid 2 への移行が「start モードの新しいアプリへの載せ替え」になる点も計画に入れておく必要があります。

まとめ:比較早見表

観点ごとの比較

  • バージョン(2026年10月11日時点): React は 19.3.0(2026年9月9日、安定版)。Solid は 2.0.0-rc.14(2026年10月8日、リリース候補)。
  • レンダリング: React は仮想 DOM と再レンダリング。Solid はfine-grained reactivityで、コンポーネントは一度だけ実行し、DOM を直接更新する。
  • コンパイラ: React Compiler は任意の自動メモ化ツール。Solid のコンパイラは JSX を DOM 操作に変換する必須の仕組みで、2.0 では OXC ベース。
  • 状態: React は useState / useReducer(スナップショット)。Solid は createSignal / createStore(アクセサで読み、マイクロタスクでバッチ)。
  • 派生値: React は useMemo か React Compiler。Solid は createMemo か普通の関数、書き込み可能な派生は createSignal(fn)。
  • Effect: React は useEffect と依存配列、useEffectEvent。Solid は createEffect(compute, effect) で依存は自動追跡。
  • 非同期の読み取り: React は use と <Suspense>、Promise はレンダーの外で作るかキャッシュする。Solid は async な createMemo と <Loading>。
  • 保留中の更新: React は startTransition / useTransition。Solid は自動の held updates と isPending / latest。
  • 書き込み: React は Actions、useActionState、useOptimistic、form action。Solid は action、createOptimistic(Store)、refresh。
  • エラー: React は Error Boundary。Solid は <Errored>(reset で再試行できる)。
  • リスト: React は map と key。Solid は <For>(keyed で3モード)と <Repeat>。
  • 更新の流儀: React はイミュータブル。Solid はドラフトを書き換えるミュータブルな setter で、プロパティ単位の購読。
  • Context: どちらも <Context value> をプロバイダーとして使える。Solid はデフォルト値がなければプロバイダー不在時に例外を投げる。
  • サーバー: React は RSC と Server Functions(Next.js App Router が最も完全な実装)、PPR。Solid は "use server" がコアに入り、start モードを使う。Server Components は実験的なプレビュー。
  • フルスタック: React は Next.js、React Router、Expo、TanStack Start など。Solid は Vite プラグインの start モード、Solid Router 2.0、TanStack Start(Solid 2 向けはプレリリース)。SolidStart は役目を終える。
  • 公式の性能データ: React は Compiler 1.0 の Meta Quest Store の事例。Solid はコンパイル速度の比較のみで、ランタイム速度の主張はしない。どちらもバンドルサイズの公式な数字は出していない。

参考資料

React:

SolidJS:

関連エコシステム: