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!
Latest 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:
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.Thank you. I was just clarifying the bigger picture.