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