frodare/addon-redux

Allow setting default state based off args

SebastienGllmt opened this issue ยท 6 comments

Background

This library provides a way to override the default Redux state by setting the PARAM_REDUX_MERGE_STATE parameter for the story.
This works for basic usage, but it means nothing in your Redux state can leverage controls (the recommended way to provide knobs in Storybook as of Storybook v6)

Motivation

An example of why this would be useful is I would like all my stories to have a control for the language used, so it has to be part of the component "args". However, "selectedLanguage" is also part of my Redux state so I would like to inject the arg value using this addon.

Solution

I asked the Storybook team and they said this is not solvable using parameters as they are meant to be static. This is the solution they proposed:

i think a lot of the issue is that many addons were written before args/globals existed, and we haven't been prescriptive enough to get addon authors to use them as intended.

parameters are great for configuring things but if you want to support dynamic data, you need to use args/globals and many addons use parameters because that was the only configuration mechanism until 6.0 and all of the existing addons use them

Globals doesn't sound like the right solution since this is a per-story config, so probably args need to be used instead.

There seems to be a related discussion here.

This makes sense to me. I will read up more on args and see what I can come up with. From what I see now, I am hoping I can support a new arg field that would link it up with a part of the redux store.

I gave some though about how to achieve this.

Nested objects problem

The problem is that although Storybook supports objects a args, you get more control by using a flat object

For example, you can have Storybook give you a dropdown of all valid languages in your app by doing this

const LANGUAGES = {
  EN: 'en-US',
}
Main.args = {
  locale: LANGUAGES.EN
};
Main.argTypes = {
  locale: {
    options: Object.values(LANGUAGES),
    control: { type: 'select' },
  },
}

image

However, if you have a nested object you lose the ability to choose how to render the control

Main.args = {
  settings: {
    locale: LANGUAGES.EN,
  },
};
// no way to specify argTypes for nested object

image

This is problematic for us because Redux uses a nested state, yet it would be nice to still have user-friendly controls on the story.

Solution?

I think probably support for injecting default state as part of the args should probably be done using the argTypes field instead (addons can access argTypes using useArgTypes )

const LANGUAGES = {
  EN: 'en-US',
};
export const Main = Template.bind({});
Main.args = {
  locale: LANGUAGES.EN,
};
Main.argTypes = {
  locale: {
    options: Object.values(LANGUAGES),
    control: { type: 'select' },
    [REDUX_MERGE_STATE_PATH]: 'settings.locale'
  },
};

This way you can define how your flat arg object maps to your Redux state.
This addon would look at all the argType values and use something like lodash to override the default Redux state.

Statically type-checking solution

This is still somewhat possible to make sure the keys in your Storybook still refer to real elements in your Redux state (as opposed to being an nightmare to fix during refactoring) using a library called `ts-nameof because you can do something like

const state = store.getState();

Main.argTypes = {
  locale: {
    options: Object.values(LANGUAGES),
    control: { type: 'select' },
    [REDUX_MERGE_STATE_PATH]: nameof.full(state.settings.locale, 1); // this returns "settings.locale" as a string
  },
};

This is still kind of messy if you ever need to access arrays in your Redux state though. Maybe there is a better solution?

I was just looking into this. I think I came to the same conclusion of putting a path in the argTypes, something like:

  argTypes: {
    foo: {
      control: { type: 'text' },
      [REDUX_PATH]: 'test.counter'
    }
  }

For the path I was going to use a syntax I have used before, which while not perfect does support arrays. It would look like this:
foo.bar.2.baz to pull a value from:

{ foo: { bar: [0, 1, { baz: 'value' }]}}

Of course that fails if there are dots in any of the key names.

I was thinking this would be used for more than just a default value, but be synced with the redux state, similar to how you can edit the state in the state panel. That why it would work like normal prop-injected controls. Not sure how tricky that will be yet though.

I have made progress on this feature and published a test version (2.0.7-rc1), however it still has some bugs I am working out. It does look promising though.

I have a lightly tested working version released as 2.0.7-rc2. I only implemented one way sync for args -> state for stability, but I think that accomplishes the goal nicely. Here is an example how how it works if you want to test it out.

import React from 'react'
import App from './App'
import { ARG_REDUX_PATH } from 'addon-redux'

export default {
  title: 'App',
  component: App,
  argTypes: {
    name1: {
      control: { type: 'text' },
      [ARG_REDUX_PATH]: 'todos.0.text'
    },
    completed1: {
      control: { type: 'boolean' },
      [ARG_REDUX_PATH]: 'todos.0.completed'
    },
    name2: {
      control: { type: 'text' },
      [ARG_REDUX_PATH]: 'todos.1.text'
    },
    completed2: {
      control: { type: 'boolean' },
      [ARG_REDUX_PATH]: 'todos.1.completed'
    },
  }
};

const Template = (args) => <App />;

export const All = Template.bind({});
All.args = {
  name1: 'fooo',
  completed1: false,
  name2: 'fooo',
  completed2: false
};

Version 2.0.7 supports this feature. The README is not updated yet, but an example can be found here: https://github.com/frodare/addon-redux-example/blob/master/src/components/App.stories.js