こんにちは。エンジニアの吉田です。
本記事では、SolidJS の次期メジャーバージョン Solid 2.0 で検討・実装が進んでいる主な新機能/変更点を、コード例を交えながら紹介します。
本記事は experimental 時点の内容をベースにしているため、正式版までに API 仕様が変わる可能性があります。
背景:SolidJSとは
SolidJS は宣言的な UI を書くための JavaScript ライブラリです。React のような Virtual DOM を採用せず、signal などの fine-grained reactivity(細粒度リアクティビティ)により、影響範囲だけを更新します。
この仕組みにより、不要な再レンダリングを抑えつつ、コンポーネント指向の書き方ができます。
公式サイト:https://www.solidjs.com
ドキュメント:https://docs.solidjs.com
2025年2月、SolidJSのコアチームは公式に 「Solid 2.0」プロジェクトの開始を発表しました。 github.com Solid 2.0 は段階的にリリースが進められており、現時点では experimental リリースが公開されています(experimental → alpha → beta → RC → 正式リリース)。
本記事では、現在実装済みのexperimentalで具体的に触れるAPIと挙動に絞って紹介します。また、2.0で検討されている各APIの実際の挙動については、https://stackblitz.com/@ryansolid/collections/solid-2-0-experiments でコード例がまとめられているためこちらも参照ください。
【新機能】楽観的更新(Optimistic UI)向けプリミティブ
できること:UI を先に更新し、後続の非同期処理が失敗したら変更を取り消す、いわゆる「楽観的更新」を扱いやすくします
SNSの「いいね」ボタン、Todoのチェックトグル、ショッピングカートへの追加など、「操作が失敗する可能性は低いが、毎回サーバー往復が発生する」操作では、応答を待ってからUIを更新すると体感的な遅延が生じます。
楽観的更新(Opimistic UI)は「とりあえず成功した体でUIを先に更新し、失敗時だけ巻き戻す」手法で、この体感遅延を解消します。
しかしこれを手書きすると、
- 巻き戻し用の状態保持
- 複数APIを跨ぐ場合の整合性
- 途中失敗時の一貫性
などが煩雑になり、バグの温床になりがちです。
Solid 2.0 では、この楽観的更新を扱いやすくするためのプリミティブが用意されます。
createOptimisticStore
createOptimisticStore は Store(オブジェクト/配列など)向けの楽観更新プリミティブです。
- ソースに プレーンな値を渡すと、その値を初期値とする楽観的 Store を作れます。Store を書き換えると UI が先に更新され、トランジションが失敗すると取り消されます
- ソースに 関数を渡すと、その関数(非同期でも可)の結果を元にした「派生の楽観的 Store」になります。
refresh(store)を呼ぶとソースを再実行し、取得した値で表示を確定できます
createOptimistic
createOptimistic は同じ考え方を、Store ではなく単一の signal(または派生メモ)向けに使う版です。先に値を変えて見せ、失敗したら元の値に戻します
action と refresh
action と refresh は、楽観的 Store / signal を更新する流れを組み立てるためのヘルパーです。
action:generator(function*)で「UI を先に更新 → API 呼び出し → 成功したら refresh」という流れを 1 本のトランザクションとして実行するためのラッパーです。yieldで Promise を返すたびに一時停止し、resolve 後に再開します。トランザクションが throw すると、その間の楽観的更新は取り消されますconst saveTodo = action(function* () { setTodos(t => [...t, optimisticTodo]); // 先にUI更新 yield apiCall(); // 非同期処理 refresh(todos); // 成功時確定 });refresh:ソース関数を再実行し、得られた値で表示を確定します
stackblitz.com
上記のコード例では、サーバーから取得する Todo リストをソースにした楽観的 Store を作り、各操作(追加・削除・トグル)を action で「先に setTodos で見た目を変える → API を呼ぶ → 成功したら refresh(todos) でサーバーと同期」という 1 本のトランジションにまとめています。
他フレームワークとの比較
- React:
useOptimisticによって、一時的に“仮の値”を表示できます。多くの場合useTransitionやデータ取得層(例:TanStack Query)と組み合わせて、「非同期処理の進行中だけ楽観的な状態を見せる」設計になります。- React はもともと「レンダリングをスケジューラが管理する」モデルを採用しており、楽観的更新もその延長線上にあります。どの更新を急ぎとみなすか(
useTransition)をReactに伝え、最終的な確定タイミングはレンダリングサイクルに委ねる形になります。
- Vue:Vue本体に楽観更新専用APIはありません。Vueは「UI状態のリアクティブ更新」を中心に設計されており、サーバー状態管理のための高度な非同期制御はフレームワークの責務外であることが多いです。
- Solid 1.x:
createResourceのmutate等で値を先に差し替えることは可能でしたが、失敗時のロールバックや一連の制御は自前になりやすいです。Solid 2.0 のcreateOptimisticStore+actionは、ロールバックを含めた流れを組み立てやすい方向に拡張されています。
【新機能】非同期の派生シグナル(Async Derived)対応
できること:createMemo に Promise を返す関数を渡せるようになり、非同期計算をリアクティブな派生として扱えるようになります。
Solid 1.x では、createMemo は基本的に同期的な派生計算を想定していました。非同期処理を扱う場合は createResource を使うのが一般的でした。2.0 では、createMemo に Promise を返す関数を渡せるようになっています。
挙動の概要
createMemo に Promise<T> を返す関数を渡した場合、シグネチャは次のようになります。
createMemo(() => Promise<T>): () => T
ポイントは次の通りです。
createMemoが返すのは() => T- 内部で Promise の解決を待ち、その解決値を派生値として扱う
- 解決を待っている間、その派生と元シグナルはpending状態になる
pending状態を制御するために、isPending と pending を使います 。
isPending:いま pending かどうかを boolean で取る
- 元シグナルを渡す:「そのシグナルに依存する派生のいずれかが pending か?」を判定できる
- 派生シグナル(=
createMemoの戻り)を渡す:「その派生自体が pending か?」を判定できる(例:isPending(derived))
UIのローディング出し分け(スピナー/ボタン無効化など)に利用できます。
pending:pending 中の “元シグナルの読み取り” を制御する
- 元シグナルを更新すると、それに依存する派生が pending になり、その間は元も pending 扱いになる
- ただし
pending(元シグナル)を使うと、派生が pending の間でも元シグナルを即座に読めて、「更新後の値」を表示できる
言い換えると、pending は「pending 伝播で UI が“古い値に張り付く/読めない”状態」を避けて、楽観的に更新後の元値を見せ続けるための読み口を提供します 。
stackblitz.com
コード例:数値型のシグナル n と、createMemo(() => divide(n(), 2)) で作った派生シグナル derived を用意しています。
<Show when={showDerivedVal()}> で派生シグナルの表示をトグルでき、表示するときは derived().toFixed(3) が読まれるため、derived が pending のあいだは isPending(n) は true になります。そのため数字ボタンを押下するとdivideがresolveされるまでボタン内に…が表示されます。また、数字ボタン押下直後のpending(n)とn() (シグナルの直接読み取り)の挙動を比べると、pending(n)は即座に更新後の値が表示されるのに対し、直接読み取りではdevideのresolveが完了するまで更新が遅れることがわかります。
「toggle derived value visibility」で非表示にすると、derived はどこからも読まれないので も元シグナルはpending にならず、isPending(n) は false のままです。この時に数字ボタンを押下しても…は表示されません。また、元シグナルがpendingにならないためpending(n)とn() は同じタイミングで表示が更新されます。
他フレームワークとの比較
React/Vue は派生計算(memo/computed)を同期前提に置く設計が基本で、非同期は effect/watch やデータ取得層に逃がすことが多いのに対し、Solid 2.0 は派生そのものを非同期に拡張する方向に進んでいるようです。
- React:
useMemoは同期前提で、非同期処理はuseEffectやデータ取得ライブラリと組み合わせる形が一般的です。非同期派生を「メモの延長」として直接扱う API は標準ではありません。 - Vue:
computedは同期前提です。非同期はwatchや外部データ取得層で扱うことが多く、派生シグナル自体が Promise を返す設計ではありません。 - Solid 1.x:非同期データは createResource で扱い、loading 状態はリソース自身が持ちます。リソースの値
resource()は初回ロード前はundefined、再取得中はオプションで「前の値」を保持するstorageオプションがありました。つまり「どの非同期が pending か」はリソース単位で、createMemo で Promise を返す非同期派生は 1.x のセマンティクスでは想定されておらず、createMemo は同期的な派生用でした。2.0ではシンプルなデータフェッチなどはcreateResourceの代わりにcreateMemoを使用する形になりそうです。
【仕様変更】デフォルトでシグナルの更新がバッチされるように
できること:同一実行コンテキスト内で発生した複数のシグナル更新が自動的にまとめて処理され、不要な再計算・再実行を抑制できるようになります
Solid はもともと fine-grained reactivity によって「影響範囲だけを更新する」設計でした 。
2.0 ではそれに加えて、更新のフラッシュタイミングをより一貫してまとめる方向に変更が入っています。
これまで(Solid 1.x)
1.x では、effect 内やイベントハンドラ内で setSignal を呼ぶと、その変更は同期的に伝播し、依存するメモ・エフェクトがすぐに再実行されていました。
また、batch() を使えば、複数の更新を1回のリアクティブ再実行にまとめることは可能でした。
Solid 2.0ではどう変わるのか
2.0 では同一同期コンテキスト内でのシグナル更新は自動的にバッチされるようになります。
flush 関数を呼んだタイミングで「ここまでに溜まった更新」が反映されます。デフォルトが「伝播を遅延」に変わり、「伝播を強制するポイント」として flush() を置く形に反転しています。
旧(1.x):
batch(() => { setA(1); setB(2); }); // ここで 1 回だけ伝播
新(2.0):
setA(1); setB(2); flush(); // ここで保留中の更新を反映
Solid はもともと更新の伝播を細かく制御する設計でしたが、2.0 では更新タイミングの一貫性を高める方向に進んでいるようです。
自動バッチングがあるため、普段はflush()を書く必要はありませんが、以下のようなケースでは明示的に必要になるかと思います:
- DOMの計算を直後に行いたい場合
- たとえばアニメーションのスタート位置を取得するために、シグナル更新直後の要素の高さを
getBoundingClientRect()で読みたい場合、更新がバッチ中のままだとDOMに反映されていないため正しい値が取れません。このときflush()で先に反映させる必要があります。
- たとえばアニメーションのスタート位置を取得するために、シグナル更新直後の要素の高さを
- テストコードで同期的にアサーションしたい場合
- 実際にsolid/signalsのテストではflushが使用されています。
- サードパーティライブラリとの統合
- フレームワーク外のコードが「DOMが即座に更新されている」ことを前提に動く場合(例:特定の測定ライブラリなど)、
flush()で明示的に同期させる必要が生じることがあります。
- フレームワーク外のコードが「DOMが即座に更新されている」ことを前提に動く場合(例:特定の測定ライブラリなど)、
他フレームワークとの比較:
- React: もともと同一のイベントハンドラ(合成イベント)内の複数 setState は 1 回の再レンダーにまとめられていました。React 18以降で自動バッチングが導入され、setTimeout や Promise、ネイティブイベント内の更新もまとめて 1 回に処理されるようになりました。Reactの
flushSyncは「このコールバック内の更新だけ同期的にコミットする」点で Solid のflushと役割が似ています。 - Vue: Vue 3はリアクティブ更新をマイクロタスクでまとめてフラッシュする仕組みのため、結果的にバッチ的な挙動をしています
まとめ
本記事では、現時点で experimental として触れられる以下の変更を中心に紹介しました。
- 楽観的更新プリミティブ(
createOptimisticStoreなど) - Async Derived(
createMemoでの非同期関数の利用) - デフォルトバッチングと
flush
一方で、公式ディスカッション「The Road to 2.0」では、他にも次のような方向性が示されています(※2026年2月時点で未確定・未実装を含む):
- リアクティブ所有モデル(ownership)の整理・簡素化
- リソース/非同期モデルのさらなる統合
- トランジション周りの再設計
- エラーハンドリングや Suspense 周辺の改善
- より一貫した非同期リアクティブモデルの構築
(参照: https://github.com/solidjs/solid/discussions/2425)
Solid 2.0 では、個別の機能追加というよりも、「非同期を含めたリアクティブモデルの再整理」が試みられているように見えます。楽観的更新や Async Derived、デフォルトバッチングといった変更も、その一連の流れの中に位置づけられるのかもしれません。
現時点では仕様が固まりきっていない部分も多く、API や挙動が変わる可能性があります。そのため、本記事で紹介した内容も「確定仕様」ではなく、あくまで experimental 時点での観察結果である点には注意が必要です。