diff --git a/docs/_snippets/tanstack-react-add-framework.md b/docs/_snippets/tanstack-react-add-framework.md new file mode 100644 index 000000000000..9543e4b6eac4 --- /dev/null +++ b/docs/_snippets/tanstack-react-add-framework.md @@ -0,0 +1,25 @@ +```diff filename=".storybook/main.ts" renderer="react" language="ts" tabTitle="CSF 3" +- import type { StorybookConfig } from '@storybook/react-vite'; ++ import type { StorybookConfig } from '@storybook/tanstack-react'; + +const config: StorybookConfig = { + // ... +- framework: '@storybook/react-vite', ++ framework: '@storybook/tanstack-react', +}; + +export default config; +``` + +```diff filename=".storybook/main.ts" renderer="react" language="ts" tabTitle="CSF Next ๐Ÿงช" +- import { defineMain } from '@storybook/react-vite/node'; ++ import { defineMain } from '@storybook/tanstack-react/node'; + +export default defineMain({ + // ... +- framework: '@storybook/react-vite', ++ framework: '@storybook/tanstack-react', +}); +``` + + diff --git a/docs/_snippets/tanstack-react-dynamic-params.md b/docs/_snippets/tanstack-react-dynamic-params.md new file mode 100644 index 000000000000..6d962d5a1125 --- /dev/null +++ b/docs/_snippets/tanstack-react-dynamic-params.md @@ -0,0 +1,45 @@ +```ts filename="Showcase.stories.ts" renderer="react" language="ts" tabTitle="CSF 3" +import type { Meta } from '@storybook/tanstack-react'; + +import { Route } from './$id'; + +const meta = { + parameters: { + tanstack: { + router: { + route: Route, + params: { id: '42' }, + routeOverrides: { + '/showcase/$id': { + loader: () => ({ item: mockItem }), + }, + }, + }, + }, + }, +} satisfies Meta; + +export default meta; +``` + +```ts filename="Showcase.stories.ts" renderer="react" language="ts" tabTitle="CSF Next ๐Ÿงช" +import preview from '../.storybook/preview'; + +import { Route } from './$id'; + +const meta = preview.meta({ + parameters: { + tanstack: { + router: { + route: Route, + params: { id: '42' }, + routeOverrides: { + '/showcase/$id': { + loader: () => ({ item: mockItem }), + }, + }, + }, + }, + }, +}); +``` diff --git a/docs/_snippets/tanstack-react-framework-options.md b/docs/_snippets/tanstack-react-framework-options.md new file mode 100644 index 000000000000..c0ef2145e148 --- /dev/null +++ b/docs/_snippets/tanstack-react-framework-options.md @@ -0,0 +1,33 @@ +```ts filename=".storybook/main.ts" renderer="react" language="ts" tabTitle="CSF 3" +import type { StorybookConfig } from '@storybook/tanstack-react'; + +const config: StorybookConfig = { + framework: { + name: '@storybook/tanstack-react', + options: { + builder: { + // Vite builder options + }, + }, + }, +}; + +export default config; +``` + +```ts filename=".storybook/main.ts" renderer="react" language="ts" tabTitle="CSF Next ๐Ÿงช" +import { defineMain } from '@storybook/tanstack-react/node'; + +export default defineMain({ + framework: { + name: '@storybook/tanstack-react', + options: { + builder: { + // Vite builder options + }, + }, + }, +}); +``` + + diff --git a/docs/_snippets/tanstack-react-install.md b/docs/_snippets/tanstack-react-install.md new file mode 100644 index 000000000000..261aa989531f --- /dev/null +++ b/docs/_snippets/tanstack-react-install.md @@ -0,0 +1,11 @@ +```shell packageManager="npm" +npm install --save-dev @storybook/tanstack-react +``` + +```shell packageManager="pnpm" +pnpm add --save-dev @storybook/tanstack-react +``` + +```shell packageManager="yarn" +yarn add --dev @storybook/tanstack-react +``` diff --git a/docs/_snippets/tanstack-react-mock-db-client.md b/docs/_snippets/tanstack-react-mock-db-client.md new file mode 100644 index 000000000000..64f81a654426 --- /dev/null +++ b/docs/_snippets/tanstack-react-mock-db-client.md @@ -0,0 +1,8 @@ +```ts filename="src/db/__mocks__/client.ts" renderer="react" language="ts" +import type { drizzle } from 'drizzle-orm/postgres-js'; +import type * as schema from '../schema'; + +export const db = new Proxy({} as ReturnType>, { + get: () => () => Promise.resolve([]), +}); +``` diff --git a/docs/_snippets/tanstack-react-mock-module-preview.md b/docs/_snippets/tanstack-react-mock-module-preview.md new file mode 100644 index 000000000000..e6c847fdf0fd --- /dev/null +++ b/docs/_snippets/tanstack-react-mock-module-preview.md @@ -0,0 +1,20 @@ +```ts filename=".storybook/preview.ts" renderer="react" language="ts" tabTitle="CSF 3" +import { sb } from 'storybook/test'; + +// Prevents postgres (Node-only) from loading in the browser +sb.mock(import('../src/db/client.ts')); + +export default {}; +``` + +```ts filename=".storybook/preview.ts" renderer="react" language="ts" tabTitle="CSF Next ๐Ÿงช" +import { definePreview } from '@storybook/tanstack-react'; +import { sb } from 'storybook/test'; + +// Prevents postgres (Node-only) from loading in the browser +sb.mock(import('../src/db/client.ts')); + +export default definePreview({}); +``` + + diff --git a/docs/_snippets/tanstack-react-mock-server-fn-stories.md b/docs/_snippets/tanstack-react-mock-server-fn-stories.md new file mode 100644 index 000000000000..381d319b44cf --- /dev/null +++ b/docs/_snippets/tanstack-react-mock-server-fn-stories.md @@ -0,0 +1,65 @@ +```ts filename="ProfileForm.stories.ts" renderer="react" language="ts" tabTitle="CSF 3" +import type { Meta, StoryObj } from '@storybook/tanstack-react'; +import { expect, mocked } from 'storybook/test'; + +import { updateProfile } from '../lib/updateProfile'; +import { ProfileForm } from './ProfileForm'; + +const meta = { + component: ProfileForm, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Success: Story = { + beforeEach: async () => { + mocked(updateProfile).mockResolvedValue({ ok: true, name: 'Ada Lovelace' }); + }, + play: async ({ canvas, userEvent }) => { + await userEvent.type(canvas.getByLabelText('Name'), 'Ada Lovelace'); + await userEvent.click(canvas.getByRole('button', { name: 'Save profile' })); + + await expect(updateProfile).toHaveBeenCalled(); + }, +}; + +export const Failure: Story = { + beforeEach: async () => { + mocked(updateProfile).mockRejectedValue(new Error('Could not save profile')); + }, +}; +``` + +```ts filename="ProfileForm.stories.ts" renderer="react" language="ts" tabTitle="CSF Next ๐Ÿงช" +import { expect, mocked } from 'storybook/test'; + +import preview from '../.storybook/preview'; + +import { updateProfile } from '../lib/updateProfile'; +import { ProfileForm } from './ProfileForm'; + +const meta = preview.meta({ + component: ProfileForm, +}); + +export const Success = meta.story({ + beforeEach: async () => { + mocked(updateProfile).mockResolvedValue({ ok: true, name: 'Ada Lovelace' }); + }, + play: async ({ canvas, userEvent }) => { + await userEvent.type(canvas.getByLabelText('Name'), 'Ada Lovelace'); + await userEvent.click(canvas.getByRole('button', { name: 'Save profile' })); + + await expect(updateProfile).toHaveBeenCalled(); + }, +}); + +export const Failure = meta.story({ + beforeEach: async () => { + mocked(updateProfile).mockRejectedValue(new Error('Could not save profile')); + }, +}); +``` + + diff --git a/docs/_snippets/tanstack-react-plain-component-story.md b/docs/_snippets/tanstack-react-plain-component-story.md new file mode 100644 index 000000000000..39d46c7d1741 --- /dev/null +++ b/docs/_snippets/tanstack-react-plain-component-story.md @@ -0,0 +1,51 @@ +```ts filename="Page.stories.ts" renderer="react" language="ts" tabTitle="CSF 3" +import type { Meta, StoryObj } from '@storybook/tanstack-react'; + +import { Page } from './Page'; + +const meta = { + component: Page, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + parameters: { + tanstack: { + router: { + route: { + path: '/demo/form/address', + }, + query: { view: 'list' }, + }, + }, + }, +}; +``` + +```ts filename="Page.stories.ts" renderer="react" language="ts" tabTitle="CSF Next ๐Ÿงช" +import preview from '../.storybook/preview'; + +import { Page } from './Page'; + +const meta = preview.meta({ + component: Page, +}); + +export const Default = meta.story({ + parameters: { + tanstack: { + router: { + route: { + path: '/demo/form/address', + }, + query: { view: 'list' }, + }, + }, + }, +}); +``` + + diff --git a/docs/_snippets/tanstack-react-preview-migrate.md b/docs/_snippets/tanstack-react-preview-migrate.md new file mode 100644 index 000000000000..718be6e3461b --- /dev/null +++ b/docs/_snippets/tanstack-react-preview-migrate.md @@ -0,0 +1,21 @@ +```diff filename=".storybook/preview.tsx" renderer="react" language="ts" tabTitle="CSF 3" +- import type { Preview } from '@storybook/react-vite'; ++ import type { Preview } from '@storybook/tanstack-react'; + +const preview: Preview = { + //... +}; + +export default preview; +``` + +```diff filename=".storybook/preview.tsx" renderer="react" language="ts" tabTitle="CSF Next ๐Ÿงช" +- import { definePreview } from '@storybook/react-vite'; ++ import { definePreview } from '@storybook/tanstack-react'; + +export default definePreview({ + //... +}); +``` + + diff --git a/docs/_snippets/tanstack-react-query-and-path.md b/docs/_snippets/tanstack-react-query-and-path.md new file mode 100644 index 000000000000..156c9f33eedf --- /dev/null +++ b/docs/_snippets/tanstack-react-query-and-path.md @@ -0,0 +1,70 @@ +```ts filename="Page.stories.ts" renderer="react" language="ts" tabTitle="CSF 3" +import type { Meta, StoryObj } from '@storybook/tanstack-react'; + +import { Route } from './Page'; + +const meta = { + parameters: { + tanstack: { + router: { + route: Route, + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const WithHash: Story = { + parameters: { + tanstack: { + // ๐Ÿ‘‡ Provide the URL fragment (hash) for the route + router: { path: '/#section-name' }, + }, + }, +}; + +export const WithSearch: Story = { + parameters: { + tanstack: { + // ๐Ÿ‘‡ Provide the query string for the route + router: { query: { tab: 'details', page: '2' } }, + }, + }, +}; +``` + +```ts filename="Page.stories.ts" renderer="react" language="ts" tabTitle="CSF Next ๐Ÿงช" +import preview from '../.storybook/preview'; + +import { Route } from './Page'; + +const meta = preview.meta({ + parameters: { + tanstack: { + router: { + route: Route, + }, + }, + }, +}); + +export const WithHash = meta.story({ + parameters: { + tanstack: { + // ๐Ÿ‘‡ Provide the URL fragment (hash) for the route + router: { path: '/#section-name' }, + }, + }, +}); + +export const WithSearch = meta.story({ + parameters: { + tanstack: { + // ๐Ÿ‘‡ Provide the query string for the route + router: { query: { tab: 'details', page: '2' } }, + }, + }, +}); +``` diff --git a/docs/_snippets/tanstack-react-query-in-story.md b/docs/_snippets/tanstack-react-query-in-story.md new file mode 100644 index 000000000000..da94db748a2d --- /dev/null +++ b/docs/_snippets/tanstack-react-query-in-story.md @@ -0,0 +1,55 @@ +```ts filename="Navbar.stories.ts" renderer="react" language="ts" tabTitle="CSF 3" +import type { Meta, StoryObj } from '@storybook/tanstack-react'; +import type { QueryClient } from '@tanstack/react-query'; + +import { Navbar } from './Navbar'; + +const meta = { + component: Navbar, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const LoggedIn: Story = { + loaders: [ + async ({ parameters }) => { + const qc: QueryClient = parameters.tanstack?.router?.context?.queryClient; + qc?.setQueryData(['currentUser'], { + id: 'user-1', + name: 'Ada Lovelace', + }); + }, + ], +}; +``` + +```ts filename="Navbar.stories.ts" renderer="react" language="ts" tabTitle="CSF Next ๐Ÿงช" +import type { QueryClient } from '@tanstack/react-query'; + +import preview from '../.storybook/preview'; + +import { Navbar } from './Navbar'; + +const meta = preview.meta({ + component: Navbar, +}); + +export const Default = meta.story(); + +export const LoggedIn = meta.story({ + loaders: [ + async ({ parameters }) => { + const qc: QueryClient = parameters.tanstack?.router?.context?.queryClient; + qc?.setQueryData(['currentUser'], { + id: 'user-1', + name: 'Ada Lovelace', + }); + }, + ], +}); +``` + + diff --git a/docs/_snippets/tanstack-react-query-setup.md b/docs/_snippets/tanstack-react-query-setup.md new file mode 100644 index 000000000000..765bbc25bd69 --- /dev/null +++ b/docs/_snippets/tanstack-react-query-setup.md @@ -0,0 +1,80 @@ +```tsx filename=".storybook/preview.tsx" renderer="react" language="tsx" tabTitle="CSF 3" +import { type QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +// ๐Ÿ‘‡ Create a new QueryClient +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + staleTime: Infinity, + }, + }, +}); + +export default { + loaders: [ + // ๐Ÿ‘‡ Clear the cache between stories so each story starts fresh + () => { + queryClient.clear(); + }, + ], + parameters: { + tanstack: { + router: { + // ๐Ÿ‘‡ Make queryClient available to route loaders via ctx.context.queryClient + context: { queryClient }, + }, + }, + }, + decorators: [ + (Story) => ( + // ๐Ÿ‘‡ Provide the QueryClient to all stories + + + + ), + ], +}; +``` + +```tsx filename=".storybook/preview.tsx" renderer="react" language="tsx" tabTitle="CSF Next ๐Ÿงช" +import { definePreview } from '@storybook/tanstack-react'; +import { type QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +// ๐Ÿ‘‡ Create a new QueryClient +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + staleTime: Infinity, + }, + }, +}); + +export default definePreview({ + loaders: [ + // ๐Ÿ‘‡ Clear the cache between stories so each story starts fresh + () => { + queryClient.clear(); + }, + ], + parameters: { + tanstack: { + router: { + // ๐Ÿ‘‡ Make queryClient available to route loaders via ctx.context.queryClient + context: { queryClient }, + }, + }, + }, + decorators: [ + (Story) => ( + // ๐Ÿ‘‡ Provide the QueryClient to all stories + + + + ), + ], +}); +``` + + diff --git a/docs/_snippets/tanstack-react-route-story.md b/docs/_snippets/tanstack-react-route-story.md new file mode 100644 index 000000000000..7f185680d993 --- /dev/null +++ b/docs/_snippets/tanstack-react-route-story.md @@ -0,0 +1,87 @@ +```ts filename="Page.stories.ts" renderer="react" language="ts" tabTitle="CSF 3" +import type { Meta, StoryObj } from '@storybook/tanstack-react'; + +import { Route } from './Page'; + +const meta = { + parameters: { + layout: 'fullscreen', + tanstack: { + router: { + route: Route, // ๐Ÿ‘ˆ Supply the Route here + // ๐Ÿ‘‡ Rest of these properties are type-safe + params: { id: '42' }, + query: { tab: 'details' }, + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithCustomLoader: Story = { + parameters: { + tanstack: { + router: { + route: Route, // ๐Ÿ‘ˆ Supply the Route here + // ๐Ÿ‘‡ Rest of these properties are type-safe + params: { id: '42' }, + routeOverrides: { + '/items/$id': { + loader: async () => ({ + item: { id: '42', name: 'Loaded inside Storybook' }, + }), + }, + }, + }, + }, + }, +}; +``` + +```ts filename="Page.stories.ts" renderer="react" language="ts" tabTitle="CSF Next ๐Ÿงช" +import preview from '../.storybook/preview'; + +import { Route } from './Page'; + +const meta = preview.meta({ + parameters: { + layout: 'fullscreen', + tanstack: { + router: { + route: Route, // ๐Ÿ‘ˆ Supply the Route here + // ๐Ÿ‘‡ Rest of these properties are type-safe + params: { id: '42' }, + query: { tab: 'details' }, + }, + }, + }, +}); + +export const Default = meta.story(); + +export const WithCustomLoader = meta.story({ + parameters: { + tanstack: { + router: { + route: Route, // ๐Ÿ‘ˆ Supply the Route here + // ๐Ÿ‘‡ Rest of these properties are type-safe + params: { id: '42' }, + routeOverrides: { + '/items/$id': { + loader: async () => ({ + item: { id: '42', name: 'Loaded inside Storybook' }, + }), + }, + }, + }, + }, + }, +}); +``` + + diff --git a/docs/_snippets/tanstack-react-route-tree-overrides.md b/docs/_snippets/tanstack-react-route-tree-overrides.md new file mode 100644 index 000000000000..e88afd98d962 --- /dev/null +++ b/docs/_snippets/tanstack-react-route-tree-overrides.md @@ -0,0 +1,56 @@ +```ts filename="UserCard.stories.ts" renderer="react" language="ts" tabTitle="CSF 3" +import type { Meta, StoryObj } from '@storybook/tanstack-react'; + +import { Route } from './UserCard'; + +const meta = { + title: 'Users/UserCard', + parameters: { + tanstack: { + router: { + route: Route, + params: { userId: '42' }, + // ๐Ÿ‘‡ Override the route's loader so the story doesn't call the real API. + routeOverrides: { + '/users/$userId': { + loader: async () => ({ user: { id: '42', name: 'Ada Lovelace' } }), + }, + }, + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; +``` + +```ts filename="UserCard.stories.ts" renderer="react" language="ts" tabTitle="CSF Next ๐Ÿงช" +import preview from '../.storybook/preview'; + +import { Route } from './UserCard'; + +const meta = preview.meta({ + title: 'Users/UserCard', + parameters: { + tanstack: { + router: { + route: Route, + params: { userId: '42' }, + // ๐Ÿ‘‡ Override the route's loader so the story doesn't call the real API. + routeOverrides: { + '/users/$userId': { + loader: async () => ({ user: { id: '42', name: 'Ada Lovelace' } }), + }, + }, + }, + }, + }, +}); + +export const Default = meta.story(); +``` + + diff --git a/docs/_snippets/tanstack-react-route-tree-story.md b/docs/_snippets/tanstack-react-route-tree-story.md new file mode 100644 index 000000000000..a5af6a00defd --- /dev/null +++ b/docs/_snippets/tanstack-react-route-tree-story.md @@ -0,0 +1,54 @@ +```ts filename="SettingsProfile.stories.ts" renderer="react" language="ts" tabTitle="CSF 3" +import type { Meta, StoryObj } from '@storybook/tanstack-react'; + +// ๐Ÿ‘‡ Route file is part of the app route tree +import { Route } from './routes/_authenticated/settings/profile'; + +const meta = { + parameters: { + tanstack: { + router: { + // ๐Ÿ‘‡ Storybook walks up the tree to root and duplicates the full route tree, + // so parent layouts (e.g. the authenticated shell) also render. + route: Route, + path: '/settings/profile', + // ๐Ÿ‘‡ Stub out any parent route guards so the story can render standalone. + routeOverrides: { + '/_authenticated': { beforeLoad: () => {} }, + }, + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; +``` + +```ts filename="SettingsProfile.stories.ts" renderer="react" language="ts" tabTitle="CSF Next ๐Ÿงช" +import preview from '../.storybook/preview'; + +// ๐Ÿ‘‡ Route file is part of the app route tree +import { Route } from './routes/_authenticated/settings/profile'; + +const meta = preview.meta({ + parameters: { + tanstack: { + router: { + // ๐Ÿ‘‡ Storybook walks up the tree to root and duplicates the full route tree, + // so parent layouts (e.g. the authenticated shell) also render. + route: Route, + path: '/settings/profile', + // ๐Ÿ‘‡ Stub out any parent route guards so the story can render standalone. + routeOverrides: { + '/_authenticated': { beforeLoad: () => {} }, + }, + }, + }, + }, +}); + +export const Default = meta.story(); +``` diff --git a/docs/_snippets/tanstack-react-server-fn.md b/docs/_snippets/tanstack-react-server-fn.md new file mode 100644 index 000000000000..15304b3c7ce5 --- /dev/null +++ b/docs/_snippets/tanstack-react-server-fn.md @@ -0,0 +1,9 @@ +```ts filename="src/lib/updateProfile.ts" renderer="react" language="ts" +import { createServerFn } from '@tanstack/start-client-core'; + +export const updateProfile = createServerFn({ method: 'POST' }).handler( + async ({ data }: { data: { name: string } }) => { + return { ok: true, name: data.name }; + }, +); +``` diff --git a/docs/get-started/frameworks/tanstack-react.mdx b/docs/get-started/frameworks/tanstack-react.mdx new file mode 100644 index 000000000000..fefa12cfa107 --- /dev/null +++ b/docs/get-started/frameworks/tanstack-react.mdx @@ -0,0 +1,328 @@ +--- +title: Storybook for TanStack React +hideRendererSelector: true +sidebar: + order: 10 + title: TanStack React (Vite) +--- + +Storybook for TanStack React is Storybook's [framework](../../contribute/framework.mdx) integration for [TanStack Router](https://tanstack.com/router) and [TanStack Start](https://tanstack.com/start/latest) applications built with [React](https://react.dev/) and [Vite](https://vitejs.dev/). + +It builds on [`@storybook/react-vite`](./react-vite.mdx) to add router-aware story rendering, automatic router mocking, and mocked TanStack Start server functions. Components that depend on routing or server functions can render inside Storybook without booting your full app runtime. + +## Install + +To install Storybook in an existing TanStack Router or TanStack Start project, run this command in your project's root directory: + + + +You can then get started [writing stories](../whats-a-story.mdx), [running tests](../../writing-tests/index.mdx), and [documenting your components](../../writing-docs/index.mdx). For more control over the installation process, refer to the [installation guide](../install.mdx). + +### Requirements + + + +This integration expects a TanStack Router application with `@tanstack/react-router` available in your project. If your app uses TanStack Start APIs such as server functions, keep the matching TanStack Start packages installed as well. + +## Run Storybook + +To run Storybook for a particular project, run the following: + + + +To build Storybook, run: + + + +You will find the output in the configured `outputDir` (default is `storybook-static`). + +## Configure + +Storybook for TanStack React uses Vite through [`@storybook/builder-vite`](../../builders/vite.mdx) and automatically wraps each story in a [memory-backed TanStack Router](https://tanstack.com/router/latest/docs/guide/history-types#memory-routing). This gives you a working router context in Storybook without having to boot your full application shell. + +Out of the box, it supports these workflows: + +- Supply a TanStack Route to render it as the story component +- Set the initial route, params, and query string per story +- Override `loader`, `beforeLoad`, and more on the story route without modifying the original route object via `routeOverrides` +- Automatically mock `@tanstack/react-router` so navigation hooks work in stories and navigation attempts can be observed +- Automatically stub TanStack Start server and runtime entry points so components using server functions can render in Storybook + +### Routing + +#### Rendering a Route + +Supply a TanStack Route object via `parameters.tanstack.router.route`. Storybook extracts the route's React component from the route and keeps the route available for typed router configuration. + + + +##### Handling dynamic params (e.g., `/$id`) + +Supply `params` alongside `routeOverrides` under `parameters.tanstack.router`. The `params` object is interpolated into the URL, and `routeOverrides` lets you stub the loader without touching the original route. + + + +For the full set of properties, see [`Parameters`](#parameters). + +#### Rendering nested routes + +When `route` is a file route connected to your app's route tree, Storybook automatically includes parent layout routes so the story renders inside the same nested hierarchy as the real app. You can also pass the `routeTree` export from `routeTree.gen.ts` directly. + +Use `path` to navigate to the specific route, and `routeOverrides` to stub guards or loaders on ancestor routes so the story can render independently. + + + +#### Using router parameters with a non-Route component + +If your story renders a regular React component instead of a route object, you can still provide routing context through `parameters.tanstack.router`. + + + +This is useful when your component reads from hooks such as `useRouterState`, `useSearch`, `useParams`, or `useLoaderData`, but you do not want to make the route itself the story component. + +#### Defining search params and URL fragments (hashes) + +Use `query` for search params (e.g., `?tab=details&page=2`) and `path` for a URL fragment (e.g., `#section-name`) under `parameters.tanstack.router`: + + + +#### Overriding route options per story + +When a route has a [`loader`](https://tanstack.com/router/latest/docs/api/router/RouteOptionsType#loader-method) or [`beforeLoad`](https://tanstack.com/router/latest/docs/api/router/RouteOptionsType#beforeload-method) that calls real APIs, you can override those options per story without modifying the original route object. Pass `routeOverrides` under `parameters.tanstack.router`. Each key is a route ID and the value can override [`loader`](https://tanstack.com/router/latest/docs/api/router/RouteOptionsType#loader-method), [`beforeLoad`](https://tanstack.com/router/latest/docs/api/router/RouteOptionsType#beforeload-method), [`validateSearch`](https://tanstack.com/router/latest/docs/api/router/RouteOptionsType#validatesearch-method), [`loaderDeps`](https://tanstack.com/router/latest/docs/api/router/RouteOptionsType#loaderdeps-method), and [`context`](https://tanstack.com/router/latest/docs/guide/router-context). + +Use `'__root__'` as the key to target the root route. + + + +### Mocking + +#### Automatic TanStack Router and TanStack Start mocks + +This framework automatically redirects `@tanstack/react-router` imports to a Storybook-compatible mock layer. That mock re-exports TanStack Router APIs, keeps hooks such as [`useNavigate()`](https://tanstack.com/router/latest/docs/api/router/useNavigateHook), [`useSearch()`](https://tanstack.com/router/latest/docs/api/router/useSearchHook), and [`useParams()`](https://tanstack.com/router/latest/docs/api/router/useParamsHook) available in stories, and wires navigation attempts into Storybook spies. + +For TanStack Start apps, the integration also stubs TanStack Start server and runtime entry points. This is what allows components that depend on server functions or Start-specific runtime modules to render in Storybook without a running Start server. + +In practice, this means you can usually render TanStack Start components directly, and `createServerFn()` handlers are replaced with mock functions that you can observe and override in stories and tests. + +#### Mocking server functions in stories + +If your component imports a TanStack Start server function, Storybook turns that [`createServerFn().handler(...)`](https://tanstack.com/start/latest/docs/framework/react/guide/server-functions) result into a mock function. That means you can override it per story with standard mock APIs. + +For example, imagine your application code exports a server function like this: + + + +In Storybook, you can override that function for each story: + + + +This is useful for documenting loading, success, and error states without changing your application code. + +#### Handling server-only dependencies + +TanStack Start apps often import server-only packages (e.g. database clients, auth libraries) at module scope inside route files. When Storybook loads the route tree, those imports can crash the browser. The integration handles this at three layers: + +##### Framework-level mocks (automatic) + +The preset already intercepts `@tanstack/react-start`, `@tanstack/react-start/server`, `@tanstack/start-storage-context`, and related TanStack modules. It also replaces `createServerFn()` handlers with mock functions. You do not need to do anything for these. + +##### App-level server modules + +When your routes import app-specific server code (e.g. `~/db/client`, `~/auth/index.server`), use Storybook's mocking with a `__mocks__` file to prevent the real module (and its Node.js dependencies) from loading in the browser. + +**Step 1**: Register the mock in `.storybook/preview.ts`: + + + +**Step 2**: Create `src/db/__mocks__/client.ts` next to the real module. Use only `import type` so no server packages are pulled in: + + + + + +**Why a `__mocks__` file instead of automocking?** + +[Storybook's automocking](../../writing-stories/mocking-data-and-modules/mocking-modules.mdx#automocking) replaces _functions_ but still **evaluates the original module** and its imports. For modules that import `postgres`, `pg`, or other Node.js-only packages, the original module must never be evaluated, because it would crash the browser. A `__mocks__` file is the only approach that completely prevents evaluation of the original module and its dependency chain. + + + +##### Identifying what to mock + +Errors like `does not provide an export named 'default'` or `AsyncLocalStorage is not defined` mean a server-only module reached the browser. + +The fix is to mock the **server module itself**, not the component or route that uses it. For example, if `Dashboard.tsx` imports `~/auth/session`, and `~/auth/session` imports `~/db/client`, and `~/db/client` imports `postgres` โ€” mock `~/db/client`. +The Node.js dependency (`postgres`) is the smoking gun; mock the closest module to it that you control. + +To find that module, walk the error stack trace from top to bottom and stop at the first import you wrote yourself. Then add a [`__mocks__` file](#app-level-server-modules) for it. + +Two cases where you do _not_ need a mock: + +- The module is from `@tanstack/*` โ€” already handled by the framework preset. Make sure you are on the latest `@storybook/tanstack-react`. +- The module only imports `createServerFn` โ€” already mocked. The error is coming from another import in the same file. + +### TanStack Query + +You can use this framework together with [TanStack Query](https://tanstack.com/query) to provide a working QueryClient in Storybook and seed query data per story. + +#### Project setup + +TanStack Query is not automatically set up. The recommended approach is to create a single [`QueryClient`](https://tanstack.com/query/latest/docs/reference/QueryClient) in your preview file, clear it between stories via [`loaders`](../../writing-stories/loaders.mdx), and share the same instance through both `parameters.tanstack.router.context` and a `QueryClientProvider` decorator. + + + +#### Seeding query data per story + +In individual stories, use [`loaders`](../../writing-stories/loaders.mdx) to call [`setQueryData`](https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientsetquerydata) on the shared `QueryClient` before the component renders. Access it from `parameters.tanstack.router.context`: + + + +## FAQ + +### How do I manually install the TanStack React framework? + +To migrate to `@storybook/tanstack-react` from `@storybook/react-vite` or to set it up manually, follow these steps: + +First, install the framework: + + + +Then, update your `.storybook/main.js|ts` to set the framework: + + + +Then similarly update your `.storybook/preview.*` to import from `@storybook/tanstack-react`: + + + +### When should I use `@storybook/tanstack-react` instead of `@storybook/react-vite`? + +Use `@storybook/tanstack-react` when your components rely on TanStack Router or TanStack Start APIs and you want Storybook to provide router context, typed route parameters, automatic router mocking, and mocked TanStack Start server-function behavior. + +Use [`@storybook/react-vite`](./react-vite.mdx) when your app is a standard React and Vite project without TanStack Router. + +### My styles are missing in Storybook + +Import your application CSS in `.storybook/preview.*` so it is bundled with the preview: + +```ts title=".storybook/preview.tsx" +import '../src/styles/app.css'; +``` + +For more information, see the [styling documentation](../../configure/styling-and-css.mdx). + +### How do I provide React context providers (e.g. theme, toast, auth) to all stories? + +Add [project-level decorators](../../writing-stories/decorators.mdx#global-decorators) to apply providers to all stories. + +You can also add [component-level decorators](../../writing-stories/decorators.mdx#component-decorators) to apply providers to all stories for a specific component, or [story-level decorators](../../writing-stories/decorators.mdx#story-decorators) to apply providers to a single story. + +### Does `@storybook/tanstack-react` support React Server Components? + +No. `@storybook/tanstack-react` runs stories in the browser using a memory-backed router. [React Server Components](https://react.dev/reference/rsc/server-components) require a server runtime and are not supported. If your component is a Server Component, [extract the client-side parts into a Client Component](https://react.dev/reference/rsc/server-components#adding-interactivity-to-server-components) and write stories for that instead. + +### Story fails to render with an error about modules not providing a default export + +This usually means a server-only module is being imported in the browser. Check the error stack trace to find the module and add a Storybook mock for it as described in [Handling server-only dependencies](#handling-server-only-dependencies). + +## API + +### Modules + +The package exports these additional modules: + +#### `@storybook/tanstack-react/react-router` + +[TanStack Router](https://tanstack.com/router/latest/docs)-compatible mock implementations used by the framework to provide router behavior in stories. Import from this module when you need direct access to the mock APIs (for example, to assert against navigation spies in tests). + +#### `@storybook/tanstack-react/start` + +[TanStack Start](https://tanstack.com/start/latest/docs)-compatible mock implementations, including a mocked [`createServerFn()`](https://tanstack.com/start/latest/docs/framework/react/guide/server-functions) implementation. Import from this module when a story or test needs to interact directly with the Start mock layer. + +### Options + +You can pass an options object for additional configuration if needed: + + + +The available options are: + +#### `builder` + +Type: `Record` + +Configure options for the [framework's builder](../../api/main-config/main-config-framework.mdx#optionsbuilder). Available options can be found in the [Vite builder docs](../../builders/vite.mdx). + +### Parameters + +This framework contributes the following [parameters](../../writing-stories/parameters.mdx) to Storybook under the `tanstack.router` namespace: + +When `route` is supplied as a plain object, it may also include TanStack route options such as [`head`](https://tanstack.com/router/latest/docs/api/router/RouteOptionsType#head-method), [`search`](https://tanstack.com/router/latest/docs/guide/search-params#reading-search-params), and [`params.parse`](https://tanstack.com/router/latest/docs/api/router/RouteOptionsType#paramsparse-method). + +#### `context` + +Type: `Record` + +Router context values injected into the story router. + +#### `params` + +Type: `ResolveParams` + +Interpolates route params into the current path. When `route` is a typed file route, the type is constrained to the param names declared in that route's path (for example, `{ id: string }` for `/$id`). + +#### `path` + +Type: `string` + +Sets the initial URL path for the story router. + +#### `query` + +Type: `Record` + +Appends search params to the initial URL. + +#### `route` + +Type: `AnyRoute | route options object` + +Supplies a route instance directly or creates a temporary story route from route options. Storybook extracts the route's React component automatically from the route. + +#### `routeOverrides` + +Type: `Partial>` + +Per-route overrides keyed by route ID, applied to the story's route and root route. Use `'__root__'` to target the root route. Each entry can override `loader`, `beforeLoad`, `validateSearch`, `loaderDeps`, and `context`. + +#### `useRouterContext` + +Type: `({ storyContext }) => RouterContext` + +Dynamically computes the router context from the story context. Use this when the router context depends on values that are already available in the story (for example, a `QueryClient` that is loaded by a story loader). + +```ts +parameters: { + tanstack: { + router: { + useRouterContext: ({ storyContext }) => ({ + queryClient: storyContext.loaded.queryClient, + }), + }, + }, +} +``` + +This is an alternative to [`context`](#context) for cases where the router context needs a React context provider (e.g., TanStack Query's `QueryClientProvider`) that must be rendered in the story before the context value can be accessed. diff --git a/docs/get-started/frameworks/vue3-vite.mdx b/docs/get-started/frameworks/vue3-vite.mdx index 74d4948cf481..857edd3c5e02 100644 --- a/docs/get-started/frameworks/vue3-vite.mdx +++ b/docs/get-started/frameworks/vue3-vite.mdx @@ -2,7 +2,7 @@ title: Storybook for Vue with Vite hideRendererSelector: true sidebar: - order: 10 + order: 11 title: Vue (Vite) --- diff --git a/docs/get-started/frameworks/web-components-vite.mdx b/docs/get-started/frameworks/web-components-vite.mdx index 45c1857a41e5..725327d51773 100644 --- a/docs/get-started/frameworks/web-components-vite.mdx +++ b/docs/get-started/frameworks/web-components-vite.mdx @@ -2,7 +2,7 @@ title: Storybook for Web components with Vite hideRendererSelector: true sidebar: - order: 11 + order: 12 title: Web components (Vite) ---