ivanjonas
ivanjonas

Reputation: 609

How can you wrap a story with a react-router layout route?

I have a very simple and plain ComponentX that renders some styled HTML, no data fetching or even routing needed. It has a single, simple story. ComponentX is meant to be used in a dark-themed website, so it assumes that it will inherit color: white; and other such styles. This is crucial to rendering ComponentX correctly. I won't bore you with the code for ComponentX.

Those contextual styles, such as background-color: black; and color: white;, are applied to the <body> by the GlobalStyles component. GlobalStyles uses the css-in-js library Emotion to apply styles to the document.

import { Global } from '@emotion/react';

export const GlobalStyles = () => (
  <>
    <Global styles={{ body: { backgroundColor: 'black' } }} />
    <Outlet />
  </>
);

As you can see, this component does not accept children, but rather is meant to be used as a layout route, so it renders an <Outlet />. I expect the application to render a Route tree like the below, using a layout route indicated by the (1)

  <Router>
    <Routes>
      <Route element={<GlobalStyles/>} >      <== (1)
        <Route path="login">
          <Route index element={<Login />} />
          <Route path="multifactor" element={<Mfa />} />
        </Route>

Not pictured: the <Login> and <Mfa> pages call ComponentX.

And this works!

The problem is with the Stories. If I render a plain story with ComponentX, it will be hard to see because it expects all of those styles on <body> to be present. The obvious solution is to create a decorator that wraps each story with this <Route element={<GlobalStyles/>} >. How can this be accomplished? Here's my working-but-not-as-intended component-x.stories.tsx:

import React from 'react';

import ComponentX from './ComponentX';

export default {
  component: ComponentX,
  title: 'Component X',
};

const Template = args => <ComponentX {...args} />;

export const Default = Template.bind({});
Default.args = {};
Default.decorators = [
  (story) => <div style={{ padding: '3rem' }}>{story()}</div>
];

(I realize that I can make <GlobalStyles> a simple wrapper component around the entire <Router>, but I want to use this pattern to create stories for other components that assume other, intermediate layout routes.)

Upvotes: 5

Views: 2350

Answers (1)

Drew Reese
Drew Reese

Reputation: 203061

What I've usually done is to create custom decorator components to handle wrapping the stories that need specific "contexts" provided to them.

Example usage:

Create story decorator functions

import React from 'react';
import { Story } from '@storybook/react';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
import theme from '../src/constants/theme';
import { AppLayout } from '../src/components/layout';

// Provides global theme and resets/normalizes browser CSS
export const ThemeDecorator = (Story: Story) => (
  <ThemeProvider theme={theme}>
    <CssBaseline />
    <Story />
  </ThemeProvider>
);

// Render a story into a routing context inside a UI layout
export const AppScreen = (Story: Story) => (
  <Router>
    <Routes>
      <Route element={<AppLayout />}>
        <Route path="/*" element={<Story />} />
      </Route>
    </Routes>
  </Router>
);

.storybook/preview.js

import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
import { ThemeDecorator } from './decorators';

export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
  options: {
    storySort: {
      includeName: true,
      method: 'alphabetical',
      order: ['Example', 'Theme', 'Components', 'Pages', '*'],
    },
  },
  viewport: {
    viewports: {
      ...INITIAL_VIEWPORTS,
    },
  },
};

export const decorators = [ThemeDecorator]; // <-- provide theme/CSS always

Any story that needs the app layout and routing context:

import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { AppScreen, MarginPageLayout } from '../../.storybook/decorators';

import BaseComponentX from './ComponentX';

export default {
  title: 'Components/Component X',
  component: BaseComponentX,
  decorators: [AppScreen], // <-- apply additional decorators
  parameters: {
    layout: 'fullscreen',
  },
} as ComponentMeta<typeof BaseComponentX>;

const BaseComponentXTemplate: ComponentStory<typeof BaseComponentX> = () => (
  <BaseComponentX />
);

export const ComponentX = BaseComponentXTemplate.bind({});

In my example you could conceivably place all your providers and that Global component (w/ props) in what I've implemented as ThemeDecorator and set as a default decorator for all stories.

Upvotes: 1

Related Questions