T
TanStack3y ago
ratty-blush

storybook support?

although i use ladle, but it works similar to storybook. storybook https://storybook.js.org renders each component in isolation. what would be the idiomaitc way to render components that uses router features (i.e links)?
9 Replies
afraid-scarlet
afraid-scarlet3y ago
I haven't tried in storybook yet, but I think you would want to use a memory router similar to how you would do it in your app, and then set up a storybook decorator. Some docs on memory router: https://tanstack.com/router/v1/docs/guide/history-types#memory-routing This code might work:
import {
createMemoryHistory,
createReactRouter,
createRouteConfig,
RouterProvider
} from "@tanstack/react-router";

const rootRoute = createRouteConfig();

const indexRoute = rootRoute.createRoute({ path: "/" });

const memoryHistory = createMemoryHistory({
initialEntries: ["/"], // Pass your initial url
});

const router = createReactRouter({ routeConfig, history: memoryHistory });

export default {
title: "Button",
component: Button,
decorators: [
(Component: ElementType): ReactElement => (
<RouterProvider router={router}>
<Component />
</RouterProvider>
),
],
};
import {
createMemoryHistory,
createReactRouter,
createRouteConfig,
RouterProvider
} from "@tanstack/react-router";

const rootRoute = createRouteConfig();

const indexRoute = rootRoute.createRoute({ path: "/" });

const memoryHistory = createMemoryHistory({
initialEntries: ["/"], // Pass your initial url
});

const router = createReactRouter({ routeConfig, history: memoryHistory });

export default {
title: "Button",
component: Button,
decorators: [
(Component: ElementType): ReactElement => (
<RouterProvider router={router}>
<Component />
</RouterProvider>
),
],
};
History Types | TanStack Router Docs
While it's not required to know the history API itself to use TanStack Router, it's a good idea to understand how it works. Under the hood, TanStack Router requires and uses a history instance to manage the routing history. If you don't create a history instance, a browser history instance is created for you when the router is initialized.
ratty-blush
ratty-blushOP3y ago
thanks, will look on it.
stormy-gold
stormy-gold3y ago
Yep, memory router is the way to go!
frail-apricot
frail-apricot3y ago
how about if I don't need any routing functionality but just need a component that uses <Link> to render without errors nvm
import type { Decorator } from '@storybook/react';

import {
RootRoute,
Route,
Router,
RouterProvider,
createMemoryHistory,
} from '@tanstack/router';

const memoryHistory = createMemoryHistory({
initialEntries: ['/'],
});

type PartialStoryFn<StoryArgs> = Parameters<Decorator<StoryArgs>>[0];

export default function RouterStoryDecorator<StoryArgs>(
story: PartialStoryFn<StoryArgs>,
) {
const rootRoute = new RootRoute();
const indexRoute = new Route({
getParentRoute: () => rootRoute,
path: '/',
component: story,
});
const routeTree = rootRoute.addChildren([indexRoute]);
const router = new Router({
routeTree,
history: memoryHistory,
});

return <RouterProvider router={router} />;
}
import type { Decorator } from '@storybook/react';

import {
RootRoute,
Route,
Router,
RouterProvider,
createMemoryHistory,
} from '@tanstack/router';

const memoryHistory = createMemoryHistory({
initialEntries: ['/'],
});

type PartialStoryFn<StoryArgs> = Parameters<Decorator<StoryArgs>>[0];

export default function RouterStoryDecorator<StoryArgs>(
story: PartialStoryFn<StoryArgs>,
) {
const rootRoute = new RootRoute();
const indexRoute = new Route({
getParentRoute: () => rootRoute,
path: '/',
component: story,
});
const routeTree = rootRoute.addChildren([indexRoute]);
const router = new Router({
routeTree,
history: memoryHistory,
});

return <RouterProvider router={router} />;
}
used like this:
const meta: Meta<StoryArgs> = {
component: Component,
decorators: [RouterStoryDecorator],
} satisfies Meta<StoryArgs>;
const meta: Meta<StoryArgs> = {
component: Component,
decorators: [RouterStoryDecorator],
} satisfies Meta<StoryArgs>;
the downside is that you can't do <Link to={route.fullPath} />
stormy-gold
stormy-gold3y ago
You do still need the router provider otherwise the link will have zero context
adverse-sapphire
adverse-sapphire3y ago
What I use is decorator used in the preview.tsx file:
import type { Preview } from "@storybook/react";
import { createMemoryHistory, RootRoute, Route, Router, RouterProvider } from "@tanstack/react-router";

const rootRoute = new RootRoute();
const indexRoute = new Route({ getParentRoute: () => rootRoute, path: "/" });
const memoryHistory = createMemoryHistory({ initialEntries: ["/"] });
const routeTree = rootRoute.addChildren([indexRoute]);
const router = new Router({ routeTree, history: memoryHistory });

const withRouter: Preview["decorators"][0] = (Story, context) => {
return <RouterProvider router={router} defaultComponent={() => <Story {...context} />} />;
};
import type { Preview } from "@storybook/react";
import { createMemoryHistory, RootRoute, Route, Router, RouterProvider } from "@tanstack/react-router";

const rootRoute = new RootRoute();
const indexRoute = new Route({ getParentRoute: () => rootRoute, path: "/" });
const memoryHistory = createMemoryHistory({ initialEntries: ["/"] });
const routeTree = rootRoute.addChildren([indexRoute]);
const router = new Router({ routeTree, history: memoryHistory });

const withRouter: Preview["decorators"][0] = (Story, context) => {
return <RouterProvider router={router} defaultComponent={() => <Story {...context} />} />;
};
correct-apricot
correct-apricot3y ago
Your solution worked like a charm form me. Here is my full code which includes decorator for both router + query
import React from 'react';
import type { Preview } from '@storybook/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createMemoryHistory, RootRoute, Route, Router, RouterProvider } from '@tanstack/react-router';

const rootRoute = new RootRoute();
const indexRoute = new Route({ getParentRoute: () => rootRoute, path: "/" });
const memoryHistory = createMemoryHistory({ initialEntries: ["/"] });
const routeTree = rootRoute.addChildren([indexRoute]);
const router = new Router({ routeTree, history: memoryHistory });

const withRouter: Preview["decorators"][0] = (Story, context) => {
return <RouterProvider router={router} defaultComponent={() => <Story {...context} />} />;
};

import '../src/index.css';

const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
decorators: [
(story) => {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
{withRouter(story)}
</QueryClientProvider>
)
}
]
};

export default preview;
import React from 'react';
import type { Preview } from '@storybook/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createMemoryHistory, RootRoute, Route, Router, RouterProvider } from '@tanstack/react-router';

const rootRoute = new RootRoute();
const indexRoute = new Route({ getParentRoute: () => rootRoute, path: "/" });
const memoryHistory = createMemoryHistory({ initialEntries: ["/"] });
const routeTree = rootRoute.addChildren([indexRoute]);
const router = new Router({ routeTree, history: memoryHistory });

const withRouter: Preview["decorators"][0] = (Story, context) => {
return <RouterProvider router={router} defaultComponent={() => <Story {...context} />} />;
};

import '../src/index.css';

const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
decorators: [
(story) => {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
{withRouter(story)}
</QueryClientProvider>
)
}
]
};

export default preview;
deep-jade
deep-jade2y ago
Just in case someone comes here in search (as I did). A few of the functions have since this been deprecated. I had to update a few lines ending up with this.
const rootRoute = createRootRoute()
const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: "/" })
const memoryHistory = createMemoryHistory({ initialEntries: ["/"] })
const routeTree = rootRoute.addChildren([indexRoute])
const router = createRouter({ routeTree, history: memoryHistory })
const rootRoute = createRootRoute()
const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: "/" })
const memoryHistory = createMemoryHistory({ initialEntries: ["/"] })
const routeTree = rootRoute.addChildren([indexRoute])
const router = createRouter({ routeTree, history: memoryHistory })
plain-purple
plain-purple2y ago
Thank you @wutanc for posting this! Here's the full code for a preview.tsx file if anyone needs it.
import type { Preview } from "@storybook/react";
import React from "react";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
RouterProvider,
} from "@tanstack/react-router";

const rootRoute = createRootRoute();
const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: "/" });
const memoryHistory = createMemoryHistory({ initialEntries: ["/"] });
const routeTree = rootRoute.addChildren([indexRoute]);
const router = createRouter({ routeTree, history: memoryHistory });

const queryClient = new QueryClient();


// @ts-expect-error
const withRouter: Preview["decorators"][0] = (Story, context) => {
return (
<RouterProvider
router={router}
defaultComponent={() => <Story {...context} />}
/>
);
};

const preview: Preview = {
parameters: {
layout: "centered",
controls: {
expanded: true,
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
decorators: [
(Story) => (
<QueryClientProvider client={queryClient}>
{withRouter(Story)}
</QueryClientProvider>
),
],
};

export default preview;
import type { Preview } from "@storybook/react";
import React from "react";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
RouterProvider,
} from "@tanstack/react-router";

const rootRoute = createRootRoute();
const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: "/" });
const memoryHistory = createMemoryHistory({ initialEntries: ["/"] });
const routeTree = rootRoute.addChildren([indexRoute]);
const router = createRouter({ routeTree, history: memoryHistory });

const queryClient = new QueryClient();


// @ts-expect-error
const withRouter: Preview["decorators"][0] = (Story, context) => {
return (
<RouterProvider
router={router}
defaultComponent={() => <Story {...context} />}
/>
);
};

const preview: Preview = {
parameters: {
layout: "centered",
controls: {
expanded: true,
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
decorators: [
(Story) => (
<QueryClientProvider client={queryClient}>
{withRouter(Story)}
</QueryClientProvider>
),
],
};

export default preview;
This seems to break composeStories from @storybook/react. Does anyone have an information on how best to resolve this? I'm getting an empty render in my tests - if I comment out the above handling for the router, the tests work again.

Did you find this page helpful?