Storybook w/ react-router - You should not use <Link> outside <Router>
Asked Answered
E

8

51

Posting the solution to a problem I had a hard time finding albeit Sensei Googling skills.

Although my app with react-router worked without any problem Storybook threw the error "Invariant failed: You should not use <Link> outside <Router>".

Error: Invariant failed: You should not use <Link> outside a <Router>
    at invariant (tiny-invariant.esm.js:11)
    at react-router-dom.js:181
    at updateContextConsumer (react-dom.development.js:19747)
    at beginWork$1 (react-dom.development.js:20079)
    at HTMLUnknownElement.callCallback (react-dom.development.js:358)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:410)
    at invokeGuardedCallback (react-dom.development.js:463)
    at beginWork$$1 (react-dom.development.js:25730)
    at performUnitOfWork (react-dom.development.js:24631)
    at workLoopSync (react-dom.development.js:24613)

Strange, as the app worked (and therefor no <Link> was used outside <Router>).

Expressivity answered 18/11, 2019 at 7:21 Comment(0)
E
-2

The problem was Storybook rendering the individual stories. In this case the component using <Link> was in fact rendered outside a <Router>.

The solution was to wrap the individual stories with the <Router> using addDecorator;

//config.js
//...
addDecorator(story => <Router>{story()}</Router>);
Expressivity answered 13/10, 2020 at 15:28 Comment(0)
P
88

Here's what worked for me:

  1. Create a preview.js file in your .storybook folder.
  2. Add the following global decorator in your preview.js file.
    import React from "react";
    import { addDecorator } from "@storybook/react";
    import { MemoryRouter } from "react-router";
    
    addDecorator(story => <MemoryRouter initialEntries={['/']}>{story()}</MemoryRouter>);

Notes

  • Storybook version used: "@storybook/react": "^5.3.13"
  • Based on this solution here
  • More about global decorators here

Edit: Update for Storybook V7 (Dec 2022)

  1. Rename preview.js to preview.tsx (I guess it would work the same with jsx but my project is made in TypeScript)

  2. addDecorator function is not available for v7 anymore so you need to add it like this.

import React from "react";
import { MemoryRouter } from "react-router";
    
export const decorators = [
  (Story) => (
    <MemoryRouter initialEntries={['/']}>
      <Story />
    </MemoryRouter>
  ),
];
Pepe answered 8/3, 2020 at 1:38 Comment(4)
This is the way and should be the accepted solutionPansir
Good, i just switch import react-router to react-router-domCompetence
Same, I also needed to switch the imports because I'm using react-router-dom and useNavigate()Circumambient
This worked straightaway for me thanks. Was wondering though, while I was looking around I saw this add-on storybook.js.org/addons/storybook-addon-react-router-v6. Has anyone used that?Ocotillo
M
27

This is what worked for me. Add in the Memory Router inside the decorators property.

import React from 'react';
import {MemoryRouter} from 'react-router-dom';
import //your component// from //component location//

export default {
title : //How you want your component to show in storybook eg: 'Components/Button'
component : //Specify the name of component that you imported eg: 'Button'
decorators : [(Story) => (<MemoryRouter><Story/></MemoryRouter>)] //Wrapping the story inside the router
};
Merozoite answered 30/10, 2020 at 1:20 Comment(0)
M
5

For version 6.1 of Storybook works storybook-router in the new code notation. Here is an example for a single component:

import React from 'react';
import StoryRouter from 'storybook-react-router';
import //your component// from //component location//

export default {
  title: '//your component//',
  component: //your component//,
  decorators: [StoryRouter()],
};

export const Name = () => <//your component// />;
Mccallum answered 21/1, 2021 at 18:25 Comment(0)
L
3

Further to Web-ski's answer. Add StoryRouter to your .storybook/preview.js

import StoryRouter from 'storybook-react-router';

addDecorator(StoryRouter());

It's now available globally, in every story.

Luedtke answered 29/6, 2021 at 7:28 Comment(1)
this is my code import StoryRouter from 'storybook-react-router'; import { addDecorator } from "@storybook/react"; addDecorator(StoryRouter()); and is not working for me : /Traveller
H
1

If you want to do this for an individual component, rather than globally, then, in the story file, wrap the component in the memory router.

For example I have a header component that has a Link element so in my header.stories.tsx file I would change the following:

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

to

const Template: ComponentStory<typeof Header> = (args) => (
  <MemoryRouter>
    <Header {...args} />
  </MemoryRouter>
);
Hamitosemitic answered 10/11, 2021 at 16:22 Comment(0)
B
0

Fabian's answer, the one that currently has the most votes, solves the problem only if you don't want to use React Router's data APIs in your stories.

Here's my modified version of Fabian's workaround that adds the ability to use React Router's data APIs, so it is possible to use data router-specific features like their extended Form component:

import { createMemoryRouter, RouterProvider } from "react-router-dom";
import { addDecorator } from "@storybook/react";

addDecorator((story) => {
  const router = createMemoryRouter([{ path: "/", element: story() }], {
    initialEntries: ["/"],
  });

  return <RouterProvider router={router} />;
});
Bikales answered 25/1, 2023 at 19:44 Comment(0)
E
-2

The problem was Storybook rendering the individual stories. In this case the component using <Link> was in fact rendered outside a <Router>.

The solution was to wrap the individual stories with the <Router> using addDecorator;

//config.js
//...
addDecorator(story => <Router>{story()}</Router>);
Expressivity answered 13/10, 2020 at 15:28 Comment(0)
G
-2

If you also want to use route path parameters, you will need to use the storybook decorator. user Web-ski and sidonaldson already showed how to add the router mock to storybook.

Here is my implementation of storybook router mock in react v 18

preview.js file

import { withRouter } from "storybook-addon-react-router-v6";

export const decorators = [withRouter];

component.story.js file

import Component from "./component";

export default {
  component: Component,
  title: "component with rout path and parameters",
  parameters: {
    reactRouter: {
      routePath: "/root/:primary/:secondary",
      routeParams: { primary: "firstparam", secondary: "secondparam" },
    },
  },
};

const Template = (args) => <Component {...args} />;

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

This will give functions like useParams the following result:

const {primary, secondary} = useParams();
console.log('primary value: ' + primary)
console.log('secondary value: ' + secondary)
// outputs: primary value: firstparam
// outputs: secondary value: secondparam
Gordan answered 22/9, 2022 at 13:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.