Storybook decorators (in React) provide a powerful way to reuse component environments across multiple stories. However, there aren't any off-the-shelf levers to manipulate global decorators from individual stories.
At work (we're hiring!) I recently cleaned up our many context providers into one Base decorator. Here is a simplified example.
// .storybook/decorators/base.tsx
export const Base: DecoratorFn = (Story, options) => {
return (
<TestReactRoot {...options.args}>
<Story {...options} />
</TestReactRoot>
);
};
// .storybook/preview.js
import { Base } from './decorators/base';
export const decorators = [Base];
TestReactRoot encapsulates a few providers, including the classic react-redux provider. So now we can easily write stories that have useSelector and other Redux hooks with minimal boilerplate. But how do I, say, set the initial Redux state from a story, when there is no visible reference to the global Base decorator? Specifically, I want to use Storybook controls to dynamically set the Redux state.
I couldn't find any existing strategies for this in the Storybook community, so I ended up using inversion of control: individual stories supply a function to the args config, which the global decorator invokes.
// ./storybook/decorators/base.tsx
export const Base: DecoratorFn = (Story, options) => {
const { args, parameters } = options;
if (parameters.modifyArgs) {
Object.assign(args, parameters.modifyArgs(args));
}
return (
<TestReactRoot {...args}>
<Story {...options} />
</TestReactRoot>
);
};
// src/components/user-avatar.stories.tsx
export default {
title: "User Avatar",
args: {
admin: false,
},
parameters: {
modifyArgs: (args) => {
return {
reduxState: generateReduxState({ admin: args.admin })
}
}
}
}
Boom! The story config just knows it can pass a pure function to modifyArgs, and the Base decorator decides what do do with the return value.
So there you have it: if you want to influence global decorator/provider state via Storybook controls:
- Use a good ol' pure callback function in the
argsconfig that takes theargsas a value and returns a partial of theargsobject. - Check for that callback function in the global decorator
- If the callback is there, invoke it and assign the result to the
argsobject (or whatever part needs mutation). - Pass around your updated data accordingly.
Enjoy!
Top comments (6)
This was exactly what I was looking for! Thanks for publishing this.
Are you please able to add an example of how this is used? How and where is the return reduxState: generateReduxState({ admin: args.admin }) used?
Hi! I don't think the specifics of how/where are important, but let me know if this doesn't help:
Given the reduxState, we construct a redux store object that is used for the classic redux provider. Pseudo code would be:
Thank you. I was just clarifying the bigger picture.
Is there any way to get those created args to be visible in story book?
Ah, it looks as though you have to explicitly say the
table: { disable: false }in the associated argTypes.