How to mock React Router v6 inside Storybook stories with Typescript

by Randy — 3 minutes

When developing components in Storybook and also working with React Router, we probably want stories of a component that depends on the React Router context.

Unfortunately, that’s not possible with a default Storybook setup because Storybook has no configuration to support React Router. You will see the error below in the browser console.

ERROR: useRoutes() may be used only in the context of a <Router> component.

It means there’s no React Router context. There are ways to inject the context of a <Router> component. For example, through a story decorator. We can use a decorator if a component in a story needs to have extra context, such as a context provider or additional markup. The next code block is an example of a simple decorator.

const decorator: DecoratorFn = (Story) => (
  <div style={{ margin: "3em" }}>
    <Story />
  </div>
);

React Router decorator with Router context

With the example decorator in mind we create a simplified version of a decorator that wraps a story with the Router context. So the context is accessible by a component in the story.

We are using the <MemoryRouter>, <Routes>, and <Route> components part of React Router, and we wrap these components.

const reactRouterDecorator: DecoratorFn = (Story) => {
  return (
    <MemoryRouter>
      <Routes>
        <Route path="/*" element={<Story />} />
      </Routes>
    </MemoryRouter>
  );
};

Now we have a React Router decorator that we can use for every story with a component that uses routing functionality.

Why MemoryRouter? A <MemoryRouter> stores its locations internally in an array. Unlike <BrowserHistory> and <HashHistory>, it isn't tied to an external source, like the history stack in a browser. This makes it ideal for scenarios where you need complete control over the history stack, like testing.

Implementation of the React Router v6 decorator

With the next example, we implement reactRouterdecorator in a story with the component that depends on the Router context.

export default {
  decorators = [reactRouterdecorator],
} as ComponentMeta<typeof Tabs>;

const Template: ComponentStory<typeof Tabs> = (args) => <Tabs {...args} />;

export const Default = Template.bind({});

Now, all the stories in this component story file will have the Router context. So we can create multiple states of the component to check if all the routes are working correctly.

TIP! With initialEntries you can change the initial routing location. For example, <MemoryRouter initialEntries={[“/account”]}> This will route to the account tab.

Extra decorator to log the location

We can extend the functionality to add location logging to see the active route. With this decorator, we use the useLocation hook of React Router to log the current location in the actions panel of the Storybook UI. When the location changes, the logger will log the new location.

const reactRouterLoggerDecorator = (Story) => {
  const location = useLocation();
  useEffect(() => {
    action("location")(location);
  }, [location]);

  return <Story />;
};

export default {
  decorators = [reactRouterLoggerDecorator, reactRouterdecorator],
} as ComponentMeta<typeof Tabs>;

Note: action(name: string) is a function from @storybook/actions-addon that can log to the actions panel.

Now, we have a decorator that implements the Router context in a Story and can log the current location. We made it possible to use React Router based components in stories. So you can develop the UI without needing the complex React Router configuration of the application.

There’s a Storybook add-on that does this for us. But the add-on doesn’t (at the time of writing this article) support React Router version 6.

If you have any questions or feedback, email me (randy.konings@divotion.com).

Enjoy!

meerdivotion

Cases

Blogs

Event