How to update components props in storybook
Asked Answered
P

6

25

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?

Pose answered 28/8, 2017 at 11:53 Comment(0)
B
12

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} />
    }
);
Baloney answered 8/1, 2019 at 9:32 Comment(2)
Where do you import "storybookWrapper" from?Miscible
@Miscible - my mistake, I should have removed it since it was only relevant to my implementation (at the time) [answer updated]Baloney
H
6

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>
  )
}
Herwick answered 20/2, 2018 at 21:4 Comment(4)
How about without using a third-party package?Fistic
How does ColorPicker uses the store from State ? (it's unclear)Baloney
@Baloney by passing store={store} to StateMiscible
I might be wrong, but I think @Baloney was asking how, within the ColorPicker component, one makes use of the value stored in the store? Even if they weren't asking that, I would love to know the answer?Ernie
M
1

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}
        />
    );
};
Microgroove answered 12/2, 2021 at 19:25 Comment(0)
X
0

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

Xeniaxeno answered 28/10, 2021 at 12:2 Comment(1)
But how do you update them in your story code? (I'll understand if you're too busy to respond!)Ladder
H
0

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 :)

Hydromechanics answered 26/7, 2023 at 19:43 Comment(0)
D
0

The official approach from the Storybook team is using the useArgs hook:

import { StoryObj, Meta } from '@storybook/react';
import { useArgs } from '@storybook/preview-api';
import { Checkbox } from './checkbox';

const meta: Meta<typeof Checkbox> = {
  title: 'Inputs/Checkbox',
  component: Checkbox,
};
export default meta;

type Story = StoryObj<typeof Checkbox>;

export const Example: Story = {
  args: {
    isChecked: false,
    label: 'Try Me!',
  },

  render: function Render(args) {
    const [{ isChecked }, updateArgs] = useArgs();

    function onChange() {
      updateArgs({ isChecked: !isChecked });
    }

    return <Checkbox {...args} onChange={onChange} isChecked={isChecked} />;
  },
};

Link to documentation: https://storybook.js.org/docs/writing-stories/args#setting-args-from-within-a-story

Digiacomo answered 16/7 at 9:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.