SolidStartによるSolidのサーバーレンダリング
SolidStart v2でアプリを構築。サーバーレンダリング、queryとcreateAsync、ルートの先読み、サーバー関数、描画設定、デプロイ方法を解説します。
SolidStart v2 (@solidjs/start 2.x) · Solid v1 · Node.js 24+ · Vite 8
SolidStartは、SolidJS向けのフルスタックフレームワークです。Solidコンポーネントにファイルベースルーティング、サーバーレンダリング、サーバー関数を追加します。ReactにおけるNext.js、SvelteにおけるSvelteKitに相当する存在です。
すでにSolidコンポーネントを書いている方にとって、フルスタック化で最も厄介なのはチュートリアルです。その多くはいまだにapp.config.tsとVinxiを使っていますが、どちらも現行のメジャーバージョンには存在しません。本記事では、サーバーでデータを読み込む小さなSolidStart v2アプリを作成し、プリロード付きの詳細ページを実装します。続いて、サーバー関数、レンダリング設定、デプロイを解説し、Reactスタックではなくこちらを選べるほどエコシステムが成熟しているかについても検討します。なお、SolidJSのベストプラクティスで取り上げたコンポーネントレベルのパターンを理解していることを前提とします。
重要なポイント
- SolidStart v2は2026年8月4日に安定版となりました。Solid v1を対象とし、VinxiをViteのEnvironment APIに置き換えています。Node.js 24+とVite 8が必要で、設定は
vite.config.tsで行います。 - SolidStart v2のルートデータは、非同期関数を
@solidjs/routerのqueryでラップし、createAsyncで読み取ることで読み込みます。 - queryの関数本体の先頭に
"use server"ディレクティブを記述すると、その関数はサーバー上で実行され、シークレットやセッションデータを読み取れるようになります。 - ルートモジュールから
route.preloadをエクスポートすると、ページコンポーネントのレンダリング前にそのルートのqueryが開始されます。 - SolidStart v2はViteプラグイン(Nitro v3、Cloudflare、Netlify)を通じてデプロイされ、独自のホスティングアダプターは保守していません。
SolidStartとは何か、v2の現状は?
SolidStartは、Solidの上に位置するメタフレームワーク層です。Solidはコンポーネントをレンダリングし、SolidStartはその周囲にルーティング、サーバーレンダリング、データ読み込み、そしてビルドとデプロイのパイプラインを追加します。
SolidStart v2は2026年8月4日に安定版になりました。Vinxiは廃止され、v2は代わりにViteのEnvironment APIの上に構築されています。Node.js 24以降とVite 8が必要で、フレームワークの設定はapp.config.tsではなくvite.config.tsに記述するようになりました。v2はSolid v1向けに作られています。Solid 2.0 Release Candidateの発表によると、SolidStartは今後メンテナンスリリースのみとなり、Solid 2.0ではSolidコアに組み込まれたStartモードがその役割を引き継ぎます。すでに本番環境でSolidStartを運用しているアプリは引き続き動作します。以下のコードはSolid v1とSolidStart v2を対象としています。既存のv1アプリをアップグレードする場合は、v1移行ガイドに従ってください。
SolidStartプロジェクトの作成
プロジェクトの雛形を作成するには、Solidのイニシャライザーを実行し、プロンプトでSolidStartテンプレートを選択します。
npm init solid
# or: pnpm create solid
cd my-app
npm install
アプリの設定はvite.config.tsで行います。入門ガイドに掲載されている最小構成は次のとおりです。
import { defineConfig } from "vite";
import { solidStart } from "@solidjs/start/config";
export default defineConfig({
plugins: [solidStart()],
});
本番環境では、solidStart()をNitro v3などのサーバープラグインと組み合わせます。solidStart設定リファレンスでは、Nitroの設定はsolidStart()には含めず、別のトップレベルのnitroキーに記述するよう定められています。
src/routes配下のすべてのファイルは、ページまたはAPIエンドポイントのいずれかになります。v2のAPIルートのページで説明されているとおり、エンドポイントもページと同じファイルとパスの対応規則に従います。組み込みの~エイリアスはアプリのルートを指すため、デフォルト設定では~/lib/dbはsrc/lib/dbに対応します。
queryとcreateAsyncによるサーバーでのデータ読み込み
SolidStartでは、ルートデータは@solidjs/routerのqueryでラップした非同期関数から取得します。コンポーネントはそれをcreateAsyncを通じて読み取ります。これはv2のデータフェッチのドキュメントで使われているパターンです。同ドキュメントには、SolidStart v2がSolid Router v1とTanStack Solid Router v1のどちらでも動作するとも記載されています。本記事の例ではSolid Routerを使用します。
まず、src/lib/posts.tsにデータモジュールを作成します。データベースなしで例を動かせるよう、インメモリの配列を使っています。
export type Post = { id: string; title: string; body: string };
const posts: Post[] = [
{ id: "1", title: "Hello SolidStart", body: "First post." },
{ id: "2", title: "Server functions", body: "Second post." },
];
export async function listPosts() {
return posts.map(({ id, title }) => ({ id, title }));
}
export async function findPost(id: string) {
return posts.find((p) => p.id === id) ?? null;
}
投稿一覧のルートは次のとおりです。
import { For } from "solid-js";
import { createAsync, query, type RouteDefinition } from "@solidjs/router";
import { listPosts } from "~/lib/posts";
const getPostList = query(async () => {
"use server";
return listPosts();
}, "post-list");
export const route = {
preload: () => getPostList(),
} satisfies RouteDefinition;
export default function PostsPage() {
const list = createAsync(() => getPostList());
return (
<ul>
<For each={list()}>
{(post) => (
<li>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
)}
</For>
</ul>
);
}
詳細ページは、idパラメーターを受け取る動的ルートです。動的セグメントのファイル名の記法については、v2のルーティングのドキュメントを参照してください。
import { Show } from "solid-js";
import { createAsync, query, type RouteDefinition } from "@solidjs/router";
import { findPost } from "~/lib/posts";
const getPostById = query(async (id: string) => {
"use server";
return findPost(id);
}, "post-by-id");
export const route = {
preload: ({ params }) => getPostById(params.id),
} satisfies RouteDefinition;
export default function PostPage(props: { params: { id: string } }) {
const post = createAsync(() => getPostById(props.params.id));
return (
<Show when={post()} fallback={<p>Post not found.</p>}>
{(p) => (
<article>
<h1>{p().title}</h1>
<p>{p().body}</p>
</article>
)}
</Show>
);
}
queryの第2引数はキャッシュキーなので、queryごとに固有のキーを指定してください。route.preloadのエクスポートにより、ページコンポーネントのレンダリング前にqueryが開始されます。つまり、ナビゲーションと同時にデータの読み込みが始まります。
フォーム、ミューテーション、再検証
サーバーへのデータの書き込みは、読み取りとは仕組みが異なります。ミューテーションでは、サーバー上の状態を変更したうえで、ページ側のキャッシュ済みの読み取り結果が古くなったことを通知する必要があります。v2では、書き込みはSolid Routerのactionを通じて行い、処理をサーバー上で実行する必要がある場合はaction本体の中に"use server"ディレクティブを記述します。最新のフォームの例、保留状態とエラー状態、.with()による追加引数の渡し方については、v2のデータミューテーションのページを参照してください。
再検証の読み取り側は、前のセクションの内容がそのまま当てはまります。各queryはそのキー(上記の"post-list"と"post-by-id")でキャッシュされ、createAsyncはそのキャッシュエントリが再検証されると再読み込みを行います。actionが成功すると、Solid Routerはデフォルトでページ上のアクティブなqueryをすべて再検証します。一部だけを更新したい場合は、revalidateにキーを渡します。getPostList.keyはqueryのすべてのインスタンスを対象とし、getPostById.keyFor(id)は特定の1つを対象とします。データフェッチのドキュメントではキャッシュの挙動の説明をSolid Routerに委ねており、詳細はルーターの再検証のページで解説されています。
再検証の対象を絞り込む際には、明確で安定したqueryキー名が役立ちます。投稿を追加するミューテーションは"post-list"だけを更新すれば十分ですが、編集の場合は"post-by-id"も更新する必要があります。
“use server”は何をするのか?
関数本体の先頭に"use server"ディレクティブを記述すると、その関数はコンポーネントから呼び出された場合でもサーバー上で実行されます。このように指定したqueryがprocess.envのシークレットやセッションデータを読み取れるのはそのためです。v2のデータフェッチのドキュメントでは、サーバー専用の例の中で@solidjs/start/httpのuseSessionを使ってこれを示しています。
"use server"の中に置くもの | 外に置くもの |
|---|---|
環境変数のシークレット(process.env.*) | ブラウザAPI(window、localStorage、DOM) |
| データベースおよびセッションへのアクセス | コンポーネント内に存在するUI状態 |
@solidjs/start/httpのヘルパー | リクエストを伴わずクリック時に実行する必要があるもの |
SolidStartアプリでは、UI状態はクライアント側に置きます。サーバーはデータを保持し、シグナルとストアがインタラクションの状態を保持します。この役割分担については、SolidJSにおける状態管理で解説しています。
SolidStart v2は、server-onlyおよびclient-onlyというマーカーインポートも認識します。モジュールにこれを追加すると、誤った側からインポートした場合(クライアントでサーバーコードを使う、またはその逆)に開発サーバーやビルドがエラーになるため、ミスがユーザーに届くことはありません。設定リファレンスには、@solidjs/start/httpと@solidjs/start/middlewareがあらかじめserver-onlyとしてマークされていると記載されています。また開発中は、アプリのエラーやサーバー関数の呼び出しを表示するツールバーも利用できます。これが本番ビルドに含まれることはありません。
SolidStartのレンダリングモード
SolidStart v2では、レンダリングの挙動はsolidStart()プラグインと、それと組み合わせるデプロイプラグインによって設定します。どちらもvite.config.tsで宣言します。サーバーレンダリング、ストリーミング、静的生成の概念的なトレードオフについては、レンダリングパターン トップ10を参照してください。このセクションでは、SolidStartにおいてそれらの選択をどこで行うかのみを扱います。
app.config.tsはもう存在しないため、古い記事で見かけるv1時代のレンダリングフラグはそのままでは適用できません。v2で利用できるオプションについては、solidStart()の設定リファレンスとデプロイプラグインのドキュメントを確認してください。
ページがサーバーでレンダリングされると、クライアントのJavaScriptによるハイドレーションが完了する前にユーザーにマークアップが表示されます。SSRアプリのセッションリプレイを見ると、このタイムラグがはっきりわかります。正しく表示されているのに反応しないボタンをユーザーがタップし、ハンドラーが登録された後にもう一度タップする、といった様子です。
SolidStart v2のデプロイ
SolidStart v2は独自のホスティングアダプターを持ちません。Viteプラグインを通じてデプロイされ、v2の発表ではサポート対象として3つが挙げられています。Nitro v3 Viteプラグイン(同発表時点ではまだベータ版)、Cloudflare Viteプラグイン、Netlify Viteプラグインです。入門ガイドからリンクされているデプロイプラグインのページで、それぞれの本番ランタイム設定が解説されています。
ReactスタックではなくSolidStartを選ぶ際の実質的なコストはエコシステムです。Reactにはサードパーティのコンポーネントライブラリ、認証連携、ホスティングテンプレートがはるかに多く存在し、Solidのチームはそうした連携部分のコードを自分たちで書くことが多くなります。SolidJSとReactの比較では、この差とSolidのレンダリングモデルの利点を比較しています。また、Solid 2.0がStartモードへ移行しつつある中で、SolidStart v2はメンテナンスモードにあることも念頭に置いておいてください。
まとめ
SolidStart v2は、Solid v1アプリにサーバーレンダリングを追加するための、安定したViteネイティブな手段です。データ読み込みはquery、createAsync、"use server"ディレクティブの3つの要素に集約され、早期フェッチにはroute.preloadを使います。次のステップとして、npm init solidでプロジェクトを作成し、上記の2つの投稿ルートを追加したうえで、v2ドキュメントのデータミューテーションのセクションを参考にミューテーションを実装してみてください。
よくある質問
SolidStart v2はSolid 2.0で動作しますか?
いいえ。SolidStart v2はSolid 1.x向けに作られており、Solid 2.0を対象とするSolidStartのリリースはありません。Solid 2.0 Release CandidateではSolidStartは役目を終え、その構成要素は別の場所に移されます。サーバー関数はSolidコアが担い、サーバーとしての提供はStartモードが行い、ファイルシステムルーティングはルーターに依存しない別パッケージが担当します。既存のSolidStartアプリは引き続き動作し、SolidStartは今後もメンテナンスリリースを受け取ります。Startモードへの移行については公式の移行ガイドが用意されています。
TanStack Startと、TanStack Routerを使ったSolidStartの違いは何ですか?
これらは別々の選択肢です。SolidStart v2はルーターに依存せず、公式のSolid Router v1とTanStack Solid Router v1のどちらでも動作するため、SolidStartを離れることなくTanStackのルーティングを利用できます。一方のTanStack Startは独立したメタフレームワークで、Solid版が@tanstack/solid-startとして公開されており、すでにSolid 2.0のベータリリースもあります。Solid 2.0向けには、StartモードとTanStack Router、TanStack Queryを組み合わせたfullstack-tanstackスターターが用意されています。
SolidStartのシングルフライトミューテーションとは何ですか?
シングルフライトミューテーションは、ミューテーション自体と同じサーバーレスポンスで更新後のデータを返す仕組みです。サーバー側のactionが現在のビューでプリロード済みのデータを変更した場合、またはデータがプリロードされているページへリダイレクトした場合、SolidStartはそのデータをサーバー上で再検証し、同じラウンドトリップで返します。クライアントは2回目のリクエストを発行する必要がないため、書き込み後にリクエストのウォーターフォールが発生しません。
SolidStart v1アプリをv2にアップグレードすると何が変わりますか?
アップグレードにはNode.js 24とVite 8が必要です。フレームワークの設定はapp.config.tsからvite.config.tsに移り、そこでsolidStart()をViteプラグインとして登録します。また、VinxiはViteのEnvironment APIに置き換えられます。公式の移行ガイドでは、インポートパス、TypeScriptの環境型、サーバーランタイム、ミドルウェアの変更点についても順を追って説明しています。v1のインポートパスやミドルウェアのコードはそのままコピーせず、ガイドと照らし合わせて見直してください。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k