DEV Community

Mike Schutte
Mike Schutte

Posted on • Updated on

Controlling global decorators via Storybook `args` and `parameters`

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];
Enter fullscreen mode Exit fullscreen mode

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 })
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Use a good ol' pure callback function in the args config that takes the args as a value and returns a partial of the args object.
  2. Check for that callback function in the global decorator
  3. If the callback is there, invoke it and assign the result to the args object (or whatever part needs mutation).
  4. Pass around your updated data accordingly.

Enjoy!

Top comments (6)

Collapse
 
kjk93 profile image
Kyle Kravette

This was exactly what I was looking for! Thanks for publishing this.

Collapse
 
ghcassell profile image
ghcassell

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?

Collapse
 
tmikeschu profile image
Mike Schutte

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:

const mockStore = configureMockStore(middlewares)(args.reduxState);
return <Provider store={mockStore}>{children}</Provider>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ghcassell profile image
ghcassell

Thank you. I was just clarifying the bigger picture.

Collapse
 
ghcassell profile image
ghcassell

Is there any way to get those created args to be visible in story book?

Thread Thread
 
ghcassell profile image
ghcassell

Ah, it looks as though you have to explicitly say the table: { disable: false } in the associated argTypes.