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
args
config that takes theargs
as a value and returns a partial of theargs
object. - Check for that callback function in the global decorator
- If the callback is there, invoke it and assign the result to the
args
object (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.