Reputation: 609
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
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