SolidJS 2.0は、コンポーネントがPromiseを他のリアクティブな値と同じように扱えるネイティブな非同期データモデルを搭載しています。これは、React開発者がSuspenseやフックで見かけるような、再レンダリングの連鎖を引き起こすことなく実現されています。この変更が重要な理由は、データの更新中もUIを画面上に保持し、ボイラープレートを削減し、Reactチームがデータ取得コードをSolidへ移行するための具体的な道筋を提供できるからです。

なぜSolidの非同期モデルは違って感じるのか

Reactでは、データを必要とするコンポーネントは通常、フック(多くの場合カスタムフック)を呼び出してPromiseのようなオブジェクトを返し、Promiseが解決(resolve)するまでのフォールバックを表示するためにUIを<Suspense>でラップします。Promiseが解決するたびに、Reactは再レンダリングをスケジュールします。もし同じコンポーネントが後でデータを再取得した場合、すでに表示されているコンテンツの上にフォールバックが一瞬表示されてしまう(フラッシュする)ことがあります。

Solidは、その仕組みを根本から変えています。Promiseは、リアクティブグラフが監視する単なる一つの値です。計算(computation)がその値を読み取るとき、グラフはその読み取りをPromiseが解決するまで一時停止しますが、UIの他の部分はレンダリングされたままになります。コンポーネントが最初にマウントされるとき、<Loading>境界がフォールバックを表示できます。それ以降の再取得(refetch)では、新しいデータが届くまで既存のDOMはそのまま維持されます。コンポーネント内での明示的な「await」も、createResourceも、手動のステート切り替えも必要ありません。

コア・プリミティブ

  • <Loading>境界 – Reactの<Suspense>に代わるものです。初回ロード時のみフォールバックを表示します。最初の描画後は、保留中の読み取りによってUIが置き換わることはありません。新しい値が解決されるまで、古いコンテンツが保持されます。特定の再取得時にスピナーを強制的に表示するには、onプロップを渡します。
  • <Reveal>コンポーネント – 複数の<Loading>境界がどのように一緒に表示されるかを制御します。以下から選択できます:
    • Sequential: DOMの順序に従って、境界が一つずつ順番に表示されます。
    • Together: すべてのデータが準備できたときに、すべてが一斉に表示されます。
    • Natural: 各データが届き次第、個別に表示されます。
  • isPendingシグナル – 特定の読み取りが進行中(in flight)の間、trueを返します。メインコンテンツを表示したまま、細いローディングバーや控えめなアニメーションをオーバーレイとして表示するのに使用でき、「stale-while-revalidate(古いデータを表示しつつ、裏で更新する)」を簡単に実現できます。
  • actionジェネレーター – Reactのミュータブルなステート更新に代わるものです。action(function*…)を使用すると、UIを楽観的に(optimistically)更新し、yieldでサーバーのレスポンスを待ち、Promiseが解決したときに結果を反映(reconcile)させることができます。UIのレスポンスは即座に感じられ、反映ステップはプリミティブに組み込まれています。

各要素とReactの概念の対応関係

機能 Reactのアプローチ Solid 2.0のアプローチ
データ取得 use()(実験的)またはサードパーティ製フック。結果は<Suspense>でラップされる Promiseを返すcreateMemo(または類似のもの)。JSX内で直接読み取る
ローディングUI 再取得のたびに<Suspense>がフォールバックを再トリガーする可能性がある <Loading>は初回ロード時のみ。再取得時は古いUIを維持
更新(リフレッシュ) UIの更新を遅延させるためのuseTransition isPendingにより、UIの切り替えなしに保留中の読み取りを通知
ミューテーション useState/useReducer + 非同期呼び出し。多くの場合、カスタムアクションでラップされる 楽観的更新の処理が組み込まれたaction(function*…)

実用的な利点は、Reactでは個別のフックとして扱われる機能を、Solidがリアクティビティエンジンのコアに組み込んでいることです。

ステップバイステップの移行ガイド

  1. データソースを特定する – Reactでは const data = useMyFetch(url) のようになっていることが多いでしょう。Solidでは、Promiseを返すmemoに置き換えます: const data = createMemo(() => fetch(url).then(r => r.json())).
  2. トップレベルのコンポーネントをラップする – Promiseが解決される前にコンポーネントがレンダリングされる場合は、<Loading fallback={<Spinner/>}>…</Loading> で囲みます。fallbackは初回マウント時にのみ表示されます。
  3. 再取得ごとのスピナーを置き換える – 以前はloadingフラグを切り替えていた箇所で、isPending(data) を読み取ります。そのboolean値を使用して、既存のUIを画面に表示したまま、控えめなインジケーターを表示します。
  4. オプティミスティック・アップデートを変換するsetState(prev => ({...prev, optimisticValue})) の後に非同期呼び出しを行っていた場合は、const update = action(function* (newValue) { state = newValue; const server = yield fetch(...); state = reconcile(server); }); と書き換えます。ジェネレーターはサーバーが応答するまで制御を yield し、その後、リアクティブ・グラフを自動的に更新します。
  5. 複数の非同期処理を扱う – 必要に応じて <Loading> の境界をネストし、<Reveal> ラッパーを追加して、それらを同時に表示するか、一つずつ表示するかを決定します。これは、React開発者が複雑な状態チェックを用いて複数の <Suspense> コンポーネントを段階的に表示させていたパターンに代わるものです。
  6. フローをテストする – SolidはPromiseの解決時に再レンダリングを行わないため、期待通りにUIの更新が行われるか確認してください。リアクティブ・グラフが変更を自動的に伝播させるため、追加の useEffect を呼び出す必要はありません。

現在進行中の変更点

Solid 2.0のasync APIは現在ベータ版です。<Loading>action といった名称は、安定版のリリース前に変更される可能性があり、ドキュメントもまだ進化しています。Promiseをリアクティブな値として扱うという核心的な考え方は変わりませんが、早期採用者はライブラリが安定するまでの間に、軽微な破壊的変更が発生することを想定しておく必要があります。

メリットを受ける対象

  • データフェッチを多用するReactチーム – 外部の状態管理ライブラリへの依存を減らし、組み込みのstale-while-revalidateパターンを利用することで、バンドルサイズを縮小し、コードベースを簡素化できます。
  • パフォーマンス重視のアプリ – フェッチのたびにコンポーネント全体を再レンダリングすることを避けることで、Solidは、特に低スペックのデバイスにおいて、よりスムーズな視覚的更新を実現します。
  • 「スピナーのちらつき」に疲れた開発者<Loading> 境界の「初回ロード時のみ」という動作により、すでに表示されているコンテンツの上にスピナーが一瞬表示されてしまうという、よくある煩わしさが解消されます。

考えられるデメリット

  • ベータ版であること – APIが安定するまでは、長期的なプロジェクトでは将来の移行作業のための工数を予算に組み込んでおく必要があるかもしれません。

次に注目すべき点

<Loading>action の名称変更がないか、公式の変更履歴(changelog)を注視してください。

結論: SolidJS 2.0では、Promiseをファーストクラスのリアクティブな値として扱うことができ、データ更新中もUIを安定させ、Reactスタイルのフック群を必要としなくなります。再レンダリング中心のモデルから脱却する準備ができているチームにとって、移行パスは明確であり、パフォーマンスの向上は具体的で、唯一の真のリスクはベータ版特有の不確実性だけです。