Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Usage with Storybook? #952

Unanswered
MerlinMason asked this question in Q&A
Jan 8, 2024 · 15 comments · 27 replies
Discussion options

Hi there,

We're currently using React-Router and considering migrating to Tanstack Router...

We use Storybook a lot and we needed to use an add-on to enable React Router to work with Storybook.

Is this also the case with Tanstack Router? and if so does such an add-on exist yet?

Thanks!

You must be logged in to vote

Replies: 15 comments 27 replies

Comment options

Currently with Storybook I get this error on most stories: ReferenceError: Cannot access 'Root' before initialization
I'm on @tanstack/react-router: "1.4.6",
Haven't been able to figure it out

You must be logged in to vote
1 reply
Comment options

I figured it out. Importing anything from a router file leads to the execution of the router file which can lead to errors even in unrelated stories.

Comment options

I would like to know how to use Storybook as well. I think a plugin would be great, or simply a working solution how to create the router context for stories that you can display components which includes any API of the router

You must be logged in to vote
3 replies
Comment options

@dohomi have you found a solution for this?

Comment options

I use this:

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 })
// @ts-ignore todo maybe soon a better solution for this?
export const withSbTanstackRouter: Preview["decorators"][0] = (Story, context) => {
 return <RouterProvider router={router} defaultComponent={() => <Story {...context} />} />
}

then wrap your most outer global decorator story like this:

export const SbDecorator: Decorator = (Story,args) => (
 <>{withSbTanstackRouter(Story,args))}</>
)
Comment options

@dohomi thanks, much appreciated!

Comment options

I'm interested to know if there are any workaround/solutions to render the Link component inside a story. Some components might want to use router hooks even.

You must be logged in to vote
1 reply
Comment options

@alireza-mh have you found a solution for this?

Comment options

Here's what I created: Storybook fake Tanstack router - Gist

You must be logged in to vote
0 replies
Comment options

Here is what I've done

import type {PartialStoryFn, StoryContext} from '@storybook/types';
import {createMemoryHistory, createRootRoute, createRoute, createRouter, RouterProvider} from '@tanstack/react-router';
export default function withRouter(Story: PartialStoryFn, {parameters}: StoryContext) {
	const {initialEntries = ['/'], initialIndex, routes = ['/']} = parameters?.router || {};
	const rootRoute = createRootRoute();
	const children = routes.map((path) =>
		createRoute({
			path,
			getParentRoute: () => rootRoute,
			component: Story,
		}),
	);
	rootRoute.addChildren(children);
	const router = createRouter({
		history: createMemoryHistory({initialEntries, initialIndex}),
		routeTree: rootRoute,
	});
	return <RouterProvider router={router} />;
}
declare module '@storybook/types' {
	interface Parameters {
		router?: {
			initialEntries?: string[];
			initialIndex?: number;
			routes?: string[];
		};
	}
}

You can add it as global decorator in preview.tsx file.

You must be logged in to vote
8 replies
Comment options

@wurmr , I have 1.32.3 and it still works fine

Comment options

@boonya Can you explain further how to achieve this for newbies? I am one of them.

Comment options

@d4vidsha what do you mean by "achieve this"? I have it working if it doesn't? I don't know. It depends on many factors - set of dependencies, storybook setup, browser and many others.

Comment options

@boonya Ah I was just wondering where the above code snippet should be placed more generally, as I'm a little lost even after reading up on the docs for Storybook decorators. Just trying to link your comment and those docs together in my head to create a minimum reproducible example.

Comment options

I would recommend you remove as many as you can and leave the only code related to router. Then check how it work. If does, add other feature one by one to catch the guilty one.

Comment options

Here's something simple that's been working fine with router v1.4 and storybook v7.6. You can see it has no business logic at all, but it's enough to provide the router context so things don't break.

// preview.tsx
import type { Preview } from '@storybook/react';
import { RouterProvider, createMemoryHistory, createRootRoute, createRouter } from '@tanstack/react-router';
const preview: Preview = {
 decorators: [
 (Story) => {
 return <RouterProvider router={createRouter({
 history: createMemoryHistory(),
 routeTree: createRootRoute({
 component: Story
 })
 })} />
 }
 ]
};
export default preview
You must be logged in to vote
3 replies
Comment options

Okay I see, but how about the situation you need to render your component by the specific route? How can I do that?

Comment options

Not sure, but in my opinion a component like that doesn't belong in storybook. I use storybook for things that are supposed to be generically composable. Buttons, modals, dropdowns, cards, etc. The only story I currently have that relies on router is a reusable implementation of tanstack table that puts its pagination, sorting, and filtering state in the URL. Doesn't care what route it's on, it just adds its own state.

If you need to recreate some non-trivial route tree, data fetching, and other stuff to make your stories work, then either: (a) you're trying to take storybook too far or (b) your components manage state badly and need to be written cleaner to be truly reusable.

Comment options

"Too far" is too abstract statemnt. If you work with components library, you are correct. But what if you want to implement some component of real application which is dependent on router context as well as some data API. Does that mean I should not do so? But why? Storybook is amazing tool which gives me such ability throught it's decorators API and parameters.
So, what is "too far" to you may be "not enough" to someone other.
With that toolset storybook helps me implement frontend far ahead of backend in my projects. It save us a tone of time and make my team free of redundant headache.

Comment options

Just FYI, there's quite a bit of subtlety involved with the Storybook (8) integration, the simple solution I and others have pasted above does not always work properly when switching between stories:

  • it will keep a stale component (not re-rendered)
  • once you navigate away from the default route, it will not navigate back
You must be logged in to vote
0 replies
Comment options

I bypassed the RouterProvider and used the RouterContextProvider and so far it works well. Can't promise it wouldn't break in a future version or that it works in all cases.
In the preview.ts file add a decorator, like:

 const router = createRouter({ routeTree });
 ...
 decorators: [story => <RouterContextProvider router={router}>{story()}</RouterContextProvider>]
You must be logged in to vote
1 reply
Comment options

This will not work when the story actually navigates anywhere (eg. in more "system-level" stories / form submits).

(note that changing eg. hash parameters due to eg. radio group selection is also considered a navigation, but depending on the exact state management setup it might still work)

Comment options

I tried all the above ways but it all looses either strong typings on useSearch or useParams and the user is forced to relax the typings with strict: false which is not really ideal. I would like to display landing pages of tanstack router pages and if there is any business logic involved which is strongly typed I receive error messages.

Invariant failed: Could not find a nearest match!
Invariant failed: Could not find an active match from "/_dashboard/team/$teamId/players"

I start to provide my own useSearch which basically keeps the same typings but replaces the options with {strict:false}

import {
 type AnyRoute,
 type FullSearchSchema,
 type RegisteredRouter,
 type RouteById,
 type RouteIds,
 useSearch
} from "@tanstack/react-router"
import { isStorybook } from "../lib/generalConfig"
type StringLiteral<T> = T extends string ? (string extends T ? string : T) : never
type StrictOrFrom<TFrom, TStrict extends boolean = true> = TStrict extends false
 ? {
 from?: never
 strict: TStrict
 }
 : {
 from: StringLiteral<TFrom> | TFrom
 strict?: TStrict
 }
type UseSearchOptions<TFrom, TStrict extends boolean, TSearch, TSelected> = StrictOrFrom<
 TFrom,
 TStrict
> & {
 select?: (search: TSearch) => TSelected
}
type Expand<T> = T extends object
 ? T extends infer O
 ? O extends Function
 ? O
 : { [K in keyof O]: O[K] }
 : never
 : T
/**
 * Enhanced useSearch hook that optionally disables strict mode if running in Storybook.
 */
export function usePtSearch<
 TRouteTree extends AnyRoute = RegisteredRouter["routeTree"],
 TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
 TStrict extends boolean = true,
 TSearch = TStrict extends false
 ? FullSearchSchema<TRouteTree>
 : Expand<RouteById<TRouteTree, TFrom>["types"]["fullSearchSchema"]>,
 TSelected = TSearch
>(opts: UseSearchOptions<TFrom, TStrict, TSearch, TSelected>): TSelected {
 // Create a modified options object if in Storybook mode
 // Call the useSearch hook with the appropriate options
 return useSearch(isStorybook ? { strict: false } : opts)
}
You must be logged in to vote
4 replies
Comment options

@dohomi
How is ../lib/generalConfig implemented? I couldn't figure out how to detect if Storybook is running.

Comment options

its simply a process.env.STORYBOOK lookup. I set this to truthy

Comment options

Got it, thanks!

Comment options

I've been working on this for days. One thing that i found is how /_dashboard/team/$teamId/player is diferrent from /_dashboard/team/$teamId/player/. If this file is a index one on a file based routing the custom hooks inside wont recognize the actual route until is proper configured

Comment options

Here's what worked for me on a project with file-based routing. I simply create a router with a route for the story, and depending on the route I render a passthrough layout route if there's any passless (layout) on the route. It defaults to an index route for stories of components with no strict route APIs. I do on this component:

export function StoryRouterProvider(
 props: PropsWithChildren<{
 parameters: StoryContext["parameters"];
 }>,
): ReactElement {
 const { children, parameters } = props;
 let storyRoutePath = "/";
 let layoutRouteId;
 // If the route parameter exists, extract any layout segment from the path,
 // if there's any layout segment, a layout route will be created as a parent of the story route
 if (parameters["route"]) {
 const pathSegments = parameters["route"].id.split("/").filter((e) => e);
 const layoutSegments = pathSegments.slice(
 0,
 pathSegments.findLastIndex((s) => s.startsWith("_")) + 1,
 );
 layoutRouteId = layoutSegments.join("/") || undefined;
 storyRoutePath = "/" + pathSegments.slice(layoutSegments.length).join("/");
 }
 const rootRoute = createRootRoute({});
 let layout =
 layoutRouteId &&
 createRoute({
 component: Outlet,
 id: layoutRouteId,
 getParentRoute: () => rootRoute,
 });
 const storyRoute = createRoute({
 component: () => children,
 path: storyRoutePath,
 getParentRoute: () => (layout ? layout : rootRoute),
 });
 layout?.addChildren([storyRoute]);
 rootRoute.addChildren([layout ? layout : storyRoute]);
 const finalPath = parameters["route"]?.params
 ? interpolatePath({
 path: storyRoutePath,
 params: parameters["route"].params,
 }).interpolatedPath
 : storyRoutePath;
 const router = createRouter({
 history: createMemoryHistory({
 initialEntries: [finalPath],
 }),
 routeTree: rootRoute,
 });
 return <RouterProvider router={router} defaultComponent={() => children} />;
}

Then I wrap the Story decorator on preview.js with the above component, then on the stories files, if a component uses route-specific APIs like getRouteApi, I provide a route parameter with the route ID and values for path parameters if there's any, for example

 parameters: {
 route: {
 id: "_authenticated/users/$userId/profile",
 params: { userId: 1 },
 },
 }
You must be logged in to vote
0 replies
Comment options

Hey guys, based on the previous answers i came up with a solution that supports the use of hooks, layouts and dynamic parameters.

I just wish to find a way to handle navigation flows based on the array entries and the generated route tree like this example with react router and mock service worker: https://github.com/mswjs/msw-storybook-addon/blob/main/packages/docs/src/demos/react-router-react-query/App.stories.tsx

export interface RouteData {
 path: string;
 loader?: Record<string, unknown>;
}
interface InitialEntries {
 entry: string;
 params?: Record<string, string>;
}
interface RouterParameters {
 initialEntries?: InitialEntries[];
 routes?: RouteData[];
}
const defaultRoute = [{ path: '/' }];
const defaultInitialEntries = [{ entry: '/', params: undefined }];
export function withTanstackRouter(Story: PartialStoryFn, { parameters }: StoryContext) {
 const { initialEntries = defaultInitialEntries, routes = defaultRoute } = parameters?.router || {};
 const rootRoute = createRootRoute({});
 routes.forEach((route) => {
 const { parentRoute, storyRoute } = createRouteConfig(route, Story, rootRoute);
 if (parentRoute !== rootRoute) rootRoute.addChildren([parentRoute]);
 parentRoute.addChildren([storyRoute]);
 });
 // Process entries with dynamic parameters
 const processedEntries = initialEntries.map(({ entry, params }) => {
 if (params) {
 return interpolatePath({
 path: entry,
 params
 }).interpolatedPath;
 }
 return entry;
 });
 const router = createRouter({
 history: createMemoryHistory({
 initialEntries: processedEntries,
 initialIndex: 0
 }),
 routeTree: rootRoute
 });
 return <RouterProvider router={router}></RouterProvider>;
}
declare module '@storybook/types' {
 interface Parameters {
 router?: RouterParameters;
 }
}
/**
 * Creates route configuration for Storybook stories
 * @param {RouteData} route - Full story path pattern and loader if exists
 * @param {PartialStoryFn} Story - Storybook component
 * @param {RootRoute} rootRoute - Application root route
 * @returns {Object} Contains layout and story routes
 */
export function createRouteConfig(route: RouteData, Story: PartialStoryFn, rootRoute: RootRoute) {
 const { layoutRouteId, storyRoutePath } = parseRoutePath(route.path);
 const parentRoute = layoutRouteId
 ? createRoute({
 id: layoutRouteId,
 component: Outlet,
 getParentRoute: () => rootRoute
 })
 : rootRoute;
 const storyRoute = createRoute({
 component: Story,
 path: `${storyRoutePath}`,
 getParentRoute: () => parentRoute,
 loader: () => route.loader
 });
 return { parentRoute, storyRoute };
}
/**
 * Finds the index of the last layout segment in a route.
 * @param segments - Array of route segments.
 * @returns The index of the last layout found, or -1 if no layouts are present.
 */
const findLastLayoutIndex = (segments: string[]): number => {
 let lastLayoutIndex = -1;
 for (let i = segments.length - 1; i >= 0; i--) {
 if (segments[i].startsWith('_')) {
 lastLayoutIndex = i;
 break;
 }
 }
 return lastLayoutIndex;
};
/**
 * Parses a route and separates layout and story components.
 * @param path - Route to parse.
 * @returns Object with the layout route or undefined, and the story route path.
 * @throws Error if the path is invalid.
 */
export function parseRoutePath(path: string): RouteParseResult {
 if (!path || typeof path !== 'string') {
 throw new Error('Invalid path: must be a non-empty string');
 }
 const hasTrailingSlash = path.endsWith('/');
 const pathSegments = path.split('/').filter(Boolean);
 const layoutMarkerIndex = findLastLayoutIndex(pathSegments);
 const hasLayout = layoutMarkerIndex !== -1;
 const boundaryIndex = layoutMarkerIndex + 1;
 const layoutSegments = hasLayout ? pathSegments.slice(0, boundaryIndex) : [];
 const storySegments = hasLayout ? pathSegments.slice(boundaryIndex) : [...pathSegments];
 const layoutRouteId = layoutSegments.length ? `/${layoutSegments.join('/')}` : undefined;
 let storyRoutePath = storySegments.length ? `/${storySegments.join('/')}` : '/';
 if (hasTrailingSlash && storyRoutePath !== '/') storyRoutePath += '/';
 return {
 layoutRouteId,
 storyRoutePath
 };
}

Usage

Basic Route

export const Basic = {
 parameters: {
 router: {
 routes: [{ path: '/dashboard' }],
 initialEntries: [{ entry: '/dashboard' }]
 }
 }
};

Route with Dynamic Parameters

export const WithParams = {
 parameters: {
 router: {
 routes: [{ path: '/users/:id' }],
 initialEntries: [
 {
 entry: '/users/:id',
 params: { id: '123' }
 }
 ]
 }
 }
};

Route with Dynamic Parameters and loader

export const WithParams = {
 parameters: {
 router: {
 routes: [
 { 
 path: '/_layout/users/:id',
 loader: {
 user: {id: '123', name:'Lorem Ipsum'}
 }
 }
 ],
 initialEntries: [
 {
 entry: '/users/:id',
 params: { id: '123' }
 }
 ]
 }
 }
};
You must be logged in to vote
1 reply
Comment options

Nice! By the way, you can improve the type safety of the types using the FileRoutesByFullPath type exported from the routeTree.gen file:

// take an intersection and combine it for better hovers
export type Compute<T> = { [K in keyof T]: T[K] } & unknown;
export type IfMaybeUndefined<T, True, False> = undefined extends T
 ? True
 : False;
export type RequiredKeys<T> = {
 [K in keyof T]: undefined extends T[K] ? never : K;
}[keyof T];
export type HasRequiredKeys<T, True, False> =
 RequiredKeys<T> extends never ? False : True;
export type RouteData = {
 [Path in keyof FileRoutesByFullPath]: Compute<
 { path: Path } & IfMaybeUndefined<
 // require loader data if we need it (i.e. it can't be undefined)
 FileRoutesByFullPath[Path]["types"]["loaderData"],
 {
 loaderData?: FileRoutesByFullPath[Path]["types"]["loaderData"];
 },
 {
 loaderData: FileRoutesByFullPath[Path]["types"]["loaderData"];
 }
 >
 >;
}[keyof FileRoutesByFullPath];
export type InitialEntries = {
 [Path in keyof FileRoutesByFullPath]: Compute<
 { entry: Path } & HasRequiredKeys<
 // require params if we have any (no need to provide {})
 FileRoutesByFullPath[Path]["types"]["params"],
 {
 params: FileRoutesByFullPath[Path]["types"]["params"];
 },
 {
 params?: FileRoutesByFullPath[Path]["types"]["params"];
 }
 >
 >;
}[keyof FileRoutesByFullPath];

This means you'll get proper type checking for loader data and params (and errors if they're wrong or missing).

Comment options

import { Decorator } from '@storybook/react';
import {
createRootRoute,
createRouter,
RouterProvider
} from '@tanstack/react-router';
export const RouterDecorator: Decorator = (Story) => {
const rootRoute = createRootRoute({
component: () => <Story />
});
const routeTree = rootRoute;
const router = createRouter({
routeTree
});
return <RouterProvider router={router} />;
};
You must be logged in to vote
5 replies
Comment options

For those who want to check the whole file, here's my .storybook/preview.tsx file based on @llite22 suggestion :

import type { Decorator, Preview } from "@storybook/react";
import { createRootRoute, createRouter, RouterProvider } from "@tanstack/react-router";
import React from "react";
import "../src/index.css";
const RouterDecorator: Decorator = (Story) => {
 const rootRoute = createRootRoute({ component: () => <Story /> });
 const routeTree = rootRoute;
 const router = createRouter({ routeTree });
 return <RouterProvider router={router} />;
};
const preview: Preview = {
 decorators: [RouterDecorator],
};
export default preview;

Works great thanks <3

Comment options

import { Decorator } from '@storybook/react';
import {
createRootRoute,
createRouter,
RouterProvider
} from '@tanstack/react-router';
export const RouterDecorator: Decorator = (Story) => {
const rootRoute = createRootRoute({
component: () => <Story />
});
const routeTree = rootRoute;
const router = createRouter({
routeTree
});
return <RouterProvider router={router} />;
};

I love you <3

Comment options

Work great, until you use import { getRouteApi } from "@tanstack/react-router" in your component.

Any way to make Storybook work when using hooks like Route.useSearch()?

Comment options

@theoludwig Read the fine discussion here, solution is already mentioned...

Comment options

Other solutions will display the whole route/page. No solution here allows to show only 1 component in isolation while still having everything TanStack Start/Router work, or I missed it, please link me to it, thanks!

Comment options

I just decided to load the entire routeTree 🤷

import type { Meta, StoryObj } from '@storybook/react-vite';
import { createMemoryHistory, createRouter, RouterProvider } from '@tanstack/react-router';
import { expect, userEvent, within } from 'storybook/test';
import { routeTree } from '../../routeTree.gen';
type Args = {
 initialEntries: string[];
 initialIndex: number;
};
function Routes({ initialEntries, initialIndex }: Args) {
 const router = createRouter({
 history: createMemoryHistory({ initialEntries, initialIndex }),
 routeTree,
 });
 return <RouterProvider router={router} />;
}
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
 title: 'Routes/Enroll',
 component: Routes,
 args: {
 initialIndex: 0,
 },
} satisfies Meta<typeof Routes>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Success: Story = {
 args: {
 initialEntries: ['/enroll'],
 },
 async play({ canvasElement }) {
 const { findByRole, findByText } = within(canvasElement);
 await userEvent.type(await findByRole('textbox', { name: 'Enrollment code' }), '1234{enter}');
 await expect(await findByText('Computer successfully enrolled:')).toBeInTheDocument();
 },
};

Definitely is a bit more integration testing-ish, but I can just set the proper intitialEntries and go from there.

You must be logged in to vote
0 replies
Comment options

We managed to get hooks working like so

//decorator
import type { Decorator } from '@storybook/react'
import {
 createRootRoute,
 createRouter,
 RouterProvider,
 createRoute,
 createHashHistory,
} from '@tanstack/react-router'
export const RouterDecorator: Decorator = (Story) => {
 const Root = createRootRoute({ component: () => <Story /> })
 const HomeRoute = createRoute({
 getParentRoute: () => Root,
 path: '/',
 })
 const routeTree = Root.addChildren([HomeRoute, TestRoute])
 const hashHistory = createHashHistory()
 const router = createRouter({
 routeTree,
 history: hashHistory,
 })
 return <RouterProvider router={router} />
}

Then in our story which showcases a navigation blocker we can use the useNavigation hook to navigate({ to: '/' }) and trigger the Block component

You must be logged in to vote
0 replies
Comment options

I put together this solution (~draft) that addresses my main issue: displaying a full page with type-safe specific search or path parameters in Storybook using TanStack Router. I managed to get it working with this <WithRouter /> component. I haven't extensively tested it yet, but it seems to have some potential for others facing similar challenges.

import {
 createMemoryHistory,
 createRootRoute,
 createRoute,
 createRouter,
 Outlet,
 RouteIds,
 RouterProvider,
 NavigateOptions,
 RouteById,
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
import { router } from '@shared/router'
export type ExtractRouteOptions<TId extends RouteIds<(typeof router)['routeTree']>> = Omit<
 NavigateOptions<typeof router, string, TId>,
 'to' | 'search' | 'params'
> & {
 search?: Partial<RouteById<(typeof router)['routeTree'], TId>['types']['fullSearchSchema']>
} & (RouteById<(typeof router)['routeTree'], TId>['types']['allParams'] extends Record<
 string,
 never
 >
 ? { params?: undefined }
 : { params: RouteById<(typeof router)['routeTree'], TId>['types']['allParams'] })
/**
 * A utility component to wrap children with a mocked router for testing purposes.
 * It allows you to specify the initial route, search parameters, and route parameters.
 *
 * @param children - The child components to be rendered within the router context.
 * @param routeId - The ID of the route to navigate to initially.
 * @param search - Optional search parameters for the route.
 * @param params - Optional route parameters.
 *
 * @returns A RouterProvider component that provides the mocked router context.
 */
export const WithRouter = <
 TRouter extends typeof router,
 const TId extends RouteIds<TRouter['routeTree']>,
>({
 children,
 routeId,
 search,
 params,
}: React.PropsWithChildren<
 {
 routeId: TId
 } & ExtractRouteOptions<TId>
>) => {
 const history = createMemoryHistory()
 const rootRoute = createRootRoute({
 component: () => (
 <>
 <Outlet />
 <TanStackRouterDevtools />
 </>
 ),
 })
 const mockedRouter = createRouter({
 history,
 routeTree: rootRoute.addChildren([
 createRoute({
 getParentRoute: () => rootRoute,
 path: routeId,
 component: () => children,
 }),
 ]),
 })
 mockedRouter.navigate({ to: routeId.replace(/\/$/, ''), search, params } as any)
 return <RouterProvider router={mockedRouter} />
}

The usage looks like this:

import type { Meta, StoryObj } from '@storybook/react'
import { WithRouter } from '@shared/lib/test'
import { YourPageComponent } from './YourPageComponent'
const meta: Meta<typeof YourPageComponent> = {
 component: YourPageComponent,
}
export default meta
type Story = StoryObj<typeof YourPageComponent>
export const Example: Story = {
 render: () => (
 <WithRouter routeId="/your-route/$entityId" params={{ entityId: 'abc' }}>
 <TransformationPlansListingPage />
 </WithRouter>
 ),
}
You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /