How to update components props in storybook

30.5k views Asked by At

I am using storybook (this) to play with my components in isolation. I want to mock all the flux cycle (that in the full app it is done with the help of redux) and update a property using a simple object in the story, but I am missing something.

  storiesOf('Color picker', module).add('base', () => {
    let colorPickerState = {
      changeColor: function(data) {
        this.color = data.color
      },
      color: '#00aced'
    }
    return (
      <ColorPicker
        name="color"
        onChange={colorPickerState.changeColor.bind(colorPickerState)}
        value={colorPickerState.color}
      />
    )
  }

I expect the value prop of <ColorPicker /> to be updated when the onChange is called; I can see the value of colorPickerState.color being updated correctly, but the component does not re-render.

What am I missing?

5

There are 5 answers

0
David Peters On

I would try employing the useState hook from react - updating state values via its setters seems to have the effect of re-rendering your storybook component.

I had a similar problem where my data input form's state and event handling (including complex validation) is done in globally, up a level in the component tree. I was able to get a working demo of this component and its complex validation by writing a simple event handler to pass along to my component.

Do note that my code is in Typescript. I am pretty new to the React world so this might not be perfect.

// Things defined elsewhere: IComponentProps, IValidationResult, IFormInput, EntryForm
// useState and useCallbac comes from react
export const MyComponentDemo = (args: IComponentProps) => {

    const [validations, setValidations] = useState<IValidationResult[]>([]);
    const [theForm, setTheForm] = useState<IFormInput>(
        {
            ...args.formValues,
            // set other interesting default form values here
        }
    );

    const dummyValidationMethod = useCallback((form: IFormInput) => {

        let validations : IValidationResult[] = [];

        // Do validation using data from form

        setValidations(validations);
        setTheForm(form);

    }, []);

    return (
        <EntryForm
            {...args}
            formValues={theForm}
            onValidation={dummyValidationMethod}
            validatedLocations={validations}
        />
    );
};
0
Dario Mincioni On

For anyone looking for the solution with modern React and Storybook

Let's say we have re-written ColorPicker to be a functional component in React which takes state from a parent component with the following interface:

interface ColorPickerProps {
  name: string
  useColorPickerState: [value: string, setValue: (value: string) => void ] // useState() array passed from parent (we're replacing onChange and value props with this)
}

React doesn't let you use hooks outside of functions, so we can just create a wrapper functional component to make state work as expected in storybook:

import { StoryObj, Meta } from '@storybook/react'
import ColorPicker, { ColorPickerProps } from 'location-of-your-component'
import { useState } from 'react'

function ColorPickerWrapper({ name }: Pick<ColorPickerProps, 'name'>) {
  const [value, setValue] = useState('#0ed0ac')
  return <ColorPicker name={name} useColorPickerState=[value, setValue] />
}

const meta: Meta<typeof ColorPicker> = {
  title: 'Components/ColorPicker',
  component: ColorPickerWrapper
}

export default meta

type Story = StoryObj<typeof ColorPicker>

export const Base: Story = {
  args: {
    name: 'color'
  }
}

I appreciate this is a different case to the OP's question, but hopefully this helps someone looking for this :)

2
vsync On

You can write a dummy-component which will render the real story component inside it, and then you get to have that dummy-component's state property.

In the below example I'm using knobs addon in a story of a Slider component

stories.addDecorator(withKnobs)
    .add('Slider', () => {
        // create dummy component that wraps the Slider and allows state:
        class StoryComp extends React.Component {
            constructor( props ){
                super(props);

                this.state = {
                    value : this.props.value || 0,
                }
            }

            onValueChange = value => this.setState({ value })

            render(){
                const props = {
                    ...this.props, 
                    onValueChange:this.onValueChange, // <--- Reason "StoryComp" is needed
                    value:this.state.value            // <--- Reason "StoryComp" is needed
                }

                return <Slider {...props} />
            }
        }

        // knobs (customaziable props)
        const widthKnobOptions = {
            range : true,
            min   : 200,
            max   : 1500,
            step  : 1
        }

        const props = {
            value : number('value', 200000),
            min   : number('min', 100),
            step  : number('step', 1000),
            max   : number('max', 1000000),
            width : number('width', 700, widthKnobOptions)
        }

        return <StoryComp {...props} />
    }
);
1
Zakhar G On

In the latest version (v6) this functionality calls Args. Also you can use argTypes to see action's logs or specify props.

4
smurf On

You can use an addon to achieve this: https://github.com/Sambego/storybook-state

So your code would look like:

import { State, Store } from '@sambego/storybook-state';

const store = new Store({
  value: '#00aced',
});

storiesOf('Color picker', module).add('base', () => {
  return (
    <State store={store}>
      <ColorPicker
        name="color"
        onChange={(data) => store.set({ value: data.color })}
      />
    </State>
  )
}