🌴

TanStack RouterアプリでStorybookを動かす

2025/01/18に公開

はじめに

Webフロントエンドエンジニアの hi_mochy です。
TanStack Router は2023年12月にリリースされた比較的新しいルーティングライブラリです。
特にクエリパラメータ周りで型安全な開発者体験を提供してくれるため、小規模な管理系アプリと相性が良いと感じています。

課題

React RouterなどはStorybook addon が提供されていますが、TanStackの場合はまだ対応されていません。
TanStack RouterのナビゲーションAPI(useNavigate, useSearch, useParams など)を呼び出しているコンポーネントのstorybookを作ると下記のようなエラーになってしまいます。

Cannot destructure property 'navigate' of 'useRouter(...)' as it is null.

そのため自前でストーリーブック用のデコレータを実装する必要があります。
自分が何度か試行錯誤したので、備忘的に残しておきます。

解決方法

下記のバージョンで実践しました。

node: v22.9.0
@tanstack/react-router: v1.82.2

ストーリーブック用に次のようなデコレータを実装しました。

// withDummyRouter.tsx

import {
  RouterProvider,
  createMemoryHistory,
  createRootRoute,
  createRoute,
  createRouter,
} from "@tanstack/react-router";
import { type ReactNode, createContext, useContext } from "react";

const StoryContext = createContext<(() => ReactNode) | undefined>(
  undefined,
);
const RenderStory = () => {
  const storyFn = useContext(StoryContext);
  if (!storyFn) {
    throw new Error("Storybook root not found");
  }
  return storyFn();
};

// List the paths of your application
const paths = ["/", "/about", "/paths"];
const routes = paths.map((path) => createRoute({
  path,
  getParentRoute: () => rootRoute,
  component: RenderStory,
}));

const rootRoute = createRootRoute();
rootRoute.addChildren(routes);
const storyRouter = createRouter({
  history: createMemoryHistory({ initialEntries: ["/"] }),
  routeTree: rootRoute,
});

/** StoryBook用ダミーRouter */
export const withDummyRouter =
  (initialPath: typeof paths[number]) =>
  (storyFn: () => ReactNode) => {
    storyRouter.history.push(initialPath);
    return (
      <StoryContext.Provider value={storyFn}>
        {/* @ts-expect-error Suppressing type error for dummy usage */}
        <RouterProvider router={storyRouter} />
      </StoryContext.Provider>
    );
  };

作成したデコレータを該当のストーリーブックに登録します。
この時、コンポーネントが求めるパスとダミールートのパスを一致させる必要があることに注意してください。

// Example.ts
const Example = () => {
  const navigate = useNavigate();
  return (
    <button onClick={() => navigate({ from: "/about", to: "/" })}>
      Example
    </button>
  );
};
// Example.stories.tsx
const meta = {
  title: "example",
  component: Example,
  decorators: [
    // CAUTION: コンポーネントが指定する from と一致してる必要がある
    withDummyRouter("/about"),
  ],
} satisfies Meta<typeof Example>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
  args: {},
};

感想

TanStack Routerが提供する型安全な開発体験は素晴らしいですが、まだ周辺ライブラリへの対応ができていません。
おそらくテストツールでも近い現象が起こると思います。
ライブラリ選定や同じ課題に当たった方の役に立てれば光栄です🙏

参考: https://github.com/TanStack/router/discussions/952

Discussion