レンガの暖炉のイラスト

Storybook v8 の React Server Components サポート

Storybook v8 では `experimentalNextRSC` オプションにより React Server Components をサポートしています。しかし、このオプションは React Server Components としての動作を再現しているわけではありません。サーバーサイドで Storybook が動作してるわけではなく、非同期コンポーネントをクライアントでレンダリングしているに過ぎないことに留意すべきです。

Storybook v8 より、experimentalNextRSC というオプションが追加されました。このオプションは true に設定することで、実験的に React Server Components をサポートします。

Storybook v8 での React Server Components サポートを試す

実際に Storybook v8 における React Server Components のサポートを試してみます。以下のコマンドで Storybook をインストールします。

npx storybook@v8.0.0-alpha.1 init

.storybook/main.tsfeatures.experimentalNextRSC: true を追加します。

import type { StorybookConfig } from "@storybook/nextjs";
 
const config: StorybookConfig = {
  features: {
    experimentalNextRSC: true,
  },
};
export default config;

Storybook に表示するためのコンポーネントを作成しましょう。TODO のリストを API から取得して表示する簡単なコンポーネントです。

app/TodoList/TodoList.tsx
export async function TodoList() {
  const res = await fetch("https://jsonplaceholder.typicode.com/todos");
  const todoList = (await res.json()) as { id: number; title: string }[];
 
  if (todoList.length === 0) {
    return <p>There are no todos</p>;
  }
 
  return (
    <ul>
      {todoList.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

TodoList コンポーネントを Storybook に表示するためのストーリーを作成します。TodoList コンポーネントでは fetch API により外部の API からデータを取得しているため、サーバーのレスポンスをモックする必要があります。

サーバーをモックするために msw を使用します。msw をインストールしましょう。

npm install msw@1 --save-dev

なお、Storybook において msw を使用する場合には msw-storybook-addon がよく使われていますが、現時点では正しく動作しないため、msw を直接使用します。また、msw の v2 では Cannot find module ‘msw/node’ というエラーが発生するため、v1 を使用します。

app/TodoList/TodoList.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { TodoList } from "./TodoList";
 
const server = setupServer();
 
const meta = {
  component: TodoList,
  tags: ["autodocs"],
  decorators: [
    (Story) => {
      server.listen();
      return <Story />;
    },
  ],
} satisfies Meta<typeof TodoList>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Default: Story = {
  decorators: [
    (Story) => {
      server.use(
        rest.get(
          "https://jsonplaceholder.typicode.com/todos",
          (req, res, ctx) => {
            return res(
              ctx.json([
                { id: 1, title: "Do the dishes" },
                { id: 2, title: "Take out the trash" },
              ])
            );
          }
        )
      );
      return <Story />;
    },
  ],
};
 
export const Empty: Story = {
  decorators: [
    (Story) => {
      server.use(
        rest.get(
          "https://jsonplaceholder.typicode.com/todos",
          (req, res, ctx) => {
            return res(ctx.json([]));
          }
        )
      );
      return <Story />;
    },
  ],
};

以下のように、TodoList コンポーネントが Storybook に表示されることを確認できます。

どのように動いているのか

Storybook は基本的にブラウザで動くはずなのですが、なぜサーバーサイドでのみ動作する React Server Components が動いているのでしょうか?

experimentalNextRSC オプションによる React Server Components のサポートは実際にサーバーサイドで動作しているわけではなく、<Suspense> でコンポーネントをラップすることで動かしているようです。

export default {
  component: MyServerComponent,
  decorators: [(Story) => <Suspense><Story /><Suspense />]
}

そのため、例えば fs.readFile などのサーバーサイドでのみ動作する API をコンポーネント内で使用している場合には、Storybook での表示時にエラーが発生します。

(0 , fs__WEBPACK_IMPORTED_MODULE_2__.readFile) is not a function

完全に React Server Components の動作を再現しているわけではなく、非同期コンポーネントをクライアントコンポーネントとしてレンダリングしているに過ぎない、ということに留意する必要があるでしょう。

またクライアントの非同期コンポーネントは、将来動作が変更される可能性があります。

We strongly considered supporting not only async Server Components, but async Client Components, too. It's technically possible, but there are enough pitfalls and caveats involved that, as of now, we aren't comfortable with the pattern as a general recommendation. The plan is to implement support for async Client Components in the runtime, but log a warning during development. The documentation will also discourage their use.

Why can't Client Components be async functions?

このように現時点では動作が不安定であると考えられるため、Container/Presenter パターンを使用してデータ取得処理と表示処理を分離するなど、別の方法を検討することをおすすめします。

まとめ

  • Storybook v8 では experimentalNextRSC オプションにより React Server Components をサポートしている
  • 実際には <Suspense> でコンポーネントをラップすることで動作している。そのため React Server Components としての動作を再現してるとは言えず、単に非同期コンポーネントをクライアントコンポーネントとしてレンダリングしているに過ぎない
  • まだ experimentalNextRSC オプションによる React Server Components のサポートは実験的なものであり、動作が不安定であるため、べつの方法を検討することをおすすめする

参考


Contributors

> GitHub で修正を提案する
この記事をシェアする
はてなブックマークに追加

関連記事