mcpField
B
bun-react-ssr

bun-react-ssr

github.com/secondsky開発240399 回インストール5 週間前に更新
リポジトリ

bun-react-ssr について

Bun でサーバーサイドレンダリングを行う React アプリを構築するためのスキルです。独自の SSR アプリを作る開発者向けに、ストリーミング、データ取得、ルーティング、本番ビルドを扱います。

主な機能

  1. 01サーバーとクライアントのエントリー設定
  2. 02ストリーミング SSR によるページ描画
  3. 03サーバー側でのデータ取得
  4. 04クライアントバンドルのビルドとハイドレーション
  5. 05ルーティングと CSS の設定

活用シーン

  1. 01独自の React SSR アプリ作成
  2. 02Suspense を使ったページのストリーミング
  3. 03サーバー取得データのクライアントへの受け渡し
  4. 04最適化したブラウザー向けアセットのビルド

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/secondsky/claude-skills --skill bun-react-ssr

インストール後、Claude Code で/bun-react-ssrを入力して読み込まれたか確認します。

詳細

Bun React SSR

Bun でカスタムのサーバーサイドレンダリング React アプリケーションを構築します。

クイックスタート

# Initialize project
mkdir my-ssr-app && cd my-ssr-app
bun init

# Install dependencies
bun add react react-dom
bun add -D @types/react @types/react-dom

基本的な SSR の設定

サーバーエントリーポイント
// src/server.tsx
import { renderToString } from "react-dom/server";
import App from "./App";

Bun.serve({
  port: 3000,
  async fetch(req) {
    const url = new URL(req.url);

    // Serve static files
    if (url.pathname.startsWith("/static/")) {
      const file = Bun.file(`./public${url.pathname}`);
      if (await file.exists()) {
        return new Response(file);
      }
    }

    // Render React app
    const html = renderToString(<App url={url.pathname} />);

    return new Response(
      `<!DOCTYPE html>
      <html>
        <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1">
          <title>React SSR</title>
        </head>
        <body>
          <div id="root">${html}</div>
          <script src="/static/client.js"></script>
        </body>
      </html>`,
      {
        headers: { "Content-Type": "text/html" },
      }
    );
  },
});

console.log("Server running on http://localhost:3000");
クライアントエントリーポイント
// src/client.tsx
import { hydrateRoot } from "react-dom/client";
import App from "./App";

hydrateRoot(
  document.getElementById("root")!,
  <App url={window.location.pathname} />
);
React アプリ
// src/App.tsx
interface AppProps {
  url: string;
}

export default function App({ url }: AppProps) {
  return (
    <div>
      <h1>React SSR with Bun</h1>
      <p>Current path: {url}</p>
      <button onClick={() => alert("Hydrated!")}>Click me</button>
    </div>
  );
}

クライアントバンドルのビルド

// build.ts
await Bun.build({
  entrypoints: ["./src/client.tsx"],
  outdir: "./public/static",
  target: "browser",
  minify: true,
  splitting: true,
});
# Build client
bun run build.ts

# Start server
bun run src/server.tsx

ストリーミング SSR

// src/server-streaming.tsx
import { renderToReadableStream } from "react-dom/server";
import App from "./App";

Bun.serve({
  port: 3000,
  async fetch(req) {
    const url = new URL(req.url);

    const stream = await renderToReadableStream(
      <App url={url.pathname} />,
      {
        bootstrapScripts: ["/static/client.js"],
        onError(error) {
          console.error(error);
        },
      }
    );

    // Wait for shell to be ready (Suspense boundaries)
    await stream.allReady;

    return new Response(stream, {
      headers: { "Content-Type": "text/html" },
    });
  },
});
Suspense を使う場合
// src/App.tsx
import { Suspense } from "react";

function SlowComponent() {
  // This would be a data fetching component
  return <div>Loaded!</div>;
}

export default function App({ url }: { url: string }) {
  return (
    <html>
      <head>
        <title>Streaming SSR</title>
      </head>
      <body>
        <div id="root">
          <h1>Fast Shell</h1>
          <Suspense fallback={<div>Loading...</div>}>
            <SlowComponent />
          </Suspense>
        </div>
      </body>
    </html>
  );
}

データの取得

サーバーサイドのデータ
// src/server.tsx
import { renderToString } from "react-dom/server";
import { Database } from "bun:sqlite";
import App from "./App";

const db = new Database("data.sqlite");

Bun.serve({
  async fetch(req) {
    const url = new URL(req.url);

    // Fetch data server-side
    const users = db.query("SELECT * FROM users").all();

    const html = renderToString(
      <App url={url.pathname} initialData={{ users }} />
    );

    return new Response(
      `<!DOCTYPE html>
      <html>
        <head><title>SSR</title></head>
        <body>
          <div id="root">${html}</div>
          <script>
            window.__INITIAL_DATA__ = ${JSON.stringify({ users })};
          </script>
          <script src="/static/client.js"></script>
        </body>
      </html>`,
      { headers: { "Content-Type": "text/html" } }
    );
  },
});
クライアントのハイドレーション
// src/client.tsx
import { hydrateRoot } from "react-dom/client";
import App from "./App";

const initialData = (window as any).__INITIAL_DATA__;

hydrateRoot(
  document.getElementById("root")!,
  <App url={window.location.pathname} initialData={initialData} />
);

ルーティング

シンプルなルーター
// src/Router.tsx
import { useState, useEffect } from "react";

interface Route {
  path: string;
  component: React.ComponentType;
}

interface RouterProps {
  routes: Route[];
  initialPath: string;
}

export function Router({ routes, initialPath }: RouterProps) {
  const [path, setPath] = useState(initialPath);

  useEffect(() => {
    const handlePopState = () => setPath(window.location.pathname);
    window.addEventListener("popstate", handlePopState);
    return () => window.removeEventListener("popstate", handlePopState);
  }, []);

  const route = routes.find((r) => r.path === path);
  const Component = route?.component || NotFound;

  return <Component />;
}

export function Link({ href, children }: { href: string; children: React.ReactNode }) {
  const handleClick = (e: React.MouseEvent) => {
    e.preventDefault();
    window.history.pushState({}, "", href);
    window.dispatchEvent(new PopStateEvent("popstate"));
  };

  return <a href={href} onClick={handleClick}>{children}</a>;
}

function NotFound() {
  return <h1>404 - Not Found</h1>;
}

CSS の処理

インラインスタイル
const html = renderToString(<App />);

return new Response(
  `<!DOCTYPE html>
  <html>
    <head>
      <style>${await Bun.file("./src/styles.css").text()}</style>
    </head>
    <body>
      <div id="root">${html}</div>
    </body>
  </html>`,
  { headers: { "Content-Type": "text/html" } }
);
外部スタイルシート
// Build CSS
await Bun.build({
  entrypoints: ["./src/styles.css"],
  outdir: "./public/static",
});

// Link in HTML
`<link rel="stylesheet" href="/static/styles.css">`

開発環境の設定

ホットリロードでの開発
// dev.ts
import { watch } from "fs";

const srcDir = "./src";
let serverProcess: Subprocess | null = null;

async function startServer() {
  serverProcess?.kill();
  serverProcess = Bun.spawn(["bun", "run", "src/server.tsx"], {
    stdout: "inherit",
    stderr: "inherit",
  });
}

// Watch for changes
watch(srcDir, { recursive: true }, async (event, filename) => {
  console.log(`Change detected: ${filename}`);
  await startServer();
});

await startServer();
console.log("Dev server watching...");

本番ビルド

// build-prod.ts

// Build client
await Bun.build({
  entrypoints: ["./src/client.tsx"],
  outdir: "./dist/public/static",
  target: "browser",
  minify: true,
  splitting: true,
  sourcemap: "external",
});

// Build server
await Bun.build({
  entrypoints: ["./src/server.tsx"],
  outdir: "./dist",
  target: "bun",
  minify: true,
});

console.log("Build complete!");

よくあるエラー

エラー原因修正方法
Hydration mismatchサーバーとクライアントの HTML が異なります初期状態を確認します
document is not definedSSR で DOM にアクセスしていますtypeof window でガードします
Cannot use hooksコンポーネントの外でフックを使用していますコンポーネントの構造を確認します
Flash of unstyled contentCSS が読み込まれていませんクリティカル CSS をインライン化します

パフォーマンスのヒント

  1. ストリーミング SSR を使用すると TTFB を短縮できます
  2. ファーストビューのクリティカル CSS をインライン化する
  3. クライアントバンドルをコード分割する
  4. 静的ページではレンダリング済み HTML をキャッシュする
  5. 段階的な読み込みにはSuspense を使用する

リファレンスを読み込むタイミング

次の場合は references/streaming-patterns.md を読み込みます:

  • 複雑な Suspense 境界
  • 選択的ハイドレーション
  • プログレッシブエンハンスメント

次の場合は references/caching.md を読み込みます:

  • HTML のキャッシュ戦略
  • CDN の統合
  • エッジレンダリング

コメント