Storybook Knobs not updating

Androme picture Androme · Aug 9, 2019 · Viewed 7.1k times · Source

I have setup storybook for my react project, and i have 2 stories. One that is a simple button and a larger component. I use knobs for both, it works fine of the simple button but it dose not update the larger component (The withInfo addon actaully shows the correct updated information from knobs, but the component dose not update.) This tells me there is something wrong with my component and not storybook knobs. But the component seems to work just fine in my application.

My Button story

  .add('with text', () => {
    const buttonText = text('Button Text', 'Hello Button');
    return (
      <Button onClick={action('clicked')}>{buttonText}</Button>)
  })

My larger component story

import React from 'react';
import { storiesOf } from '@storybook/react';
import { text, number, file, boolean } from '@storybook/addon-knobs/react';

import blueSignImage from '../src/images/sign-3.jpg';
import SignType from '../src/components/SignType/index';

storiesOf('Sign Type', module)
    .add('With current signs and requested signs', () => {
        const typeId = text("Type id", "test-1234");
        const availableSigns = number('Avalible Signs', 5);
        const requestedSigns = number('Requested Signs', 3);
        const typeName = text("Type name", "Blue Sign Image");
        const isSaving = boolean("Is saving", false);
        const onRequsetSigns = (id, value) => {
            console.log("Got requset for " + value + " signs for id " + id);
        };

        return (
            <SignType typeId={typeId} 
                availableSigns={availableSigns} 
                requestedSigns={requestedSigns} 
                typeName={typeName} 
                image={blueSignImage} 
                isSaving={isSaving}
                onRequsetSigns={onRequsetSigns}
                />
        )
    });

The component

import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Card, CardContent, CardMedia, Typography, Button, Slider, LinearProgress } from '@material-ui/core';
import SendIcon from '@material-ui/icons/Send';
import CloseIcon from '@material-ui/icons/Close';
import "./index.scss";

class SignType extends PureComponent {

    constructor(props) {        
        super(props);
        this.state = {
            typeId: props.typeId,
            typeName: props.typeName,
            image: props.image,
            availableSigns: props.availableSigns,
            requestedSigns: props.requestedSigns,
            isSaving: props.isSaving,
            onRequsetSigns: props.onRequsetSigns,

            // Local Vars
            requestAmount: 5,
            showOrderMore: false,
        };

        this.onOrderMoreClicked = this.onOrderMoreClicked.bind(this);
        this.onHideOrderMore = this.onHideOrderMore.bind(this);
        this.onRequestAmountChanged = this.onRequestAmountChanged.bind(this);
        this.onSaveRequest = this.onSaveRequest.bind(this);
    }

    componentDidMount() {

    }

    onOrderMoreClicked() {
        this.setState({ showOrderMore: true });
    }
    onHideOrderMore() {
        this.setState({ showOrderMore: false });
    }
    onRequestAmountChanged(event, value) {
        this.setState({ requestAmount: value });
    }
    onSaveRequest() {
        this.setState({ showOrderMore: false });
        this.state.onRequsetSigns(this.state.typeId, this.state.requestAmount);
    }

    render() {
        const { typeId, availableSigns, requestedSigns, image, typeName, showOrderMore, isSaving, requestAmount } = this.state;
        return (
            <Card className="signType">
                <CardMedia
                    className="cover"
                    image={image}
                    title="Live from space album cover"
                />
                <div className="details">
                    <CardContent className="content">
                        <Typography component="h5" variant="h5">
                            {typeName}
                        </Typography>
                        <Typography variant="body1" color="textPrimary">
                            {availableSigns} signs available
                            {!showOrderMore && !isSaving && (
                            <Button color="primary" onClick={this.onOrderMoreClicked}>
                                Order more
                            </Button>)}
                        </Typography>

                        {requestedSigns > 0 && (
                            <Typography variant="subtitle2" color="textSecondary">
                                {requestedSigns} requested signs pending
                        </Typography>
                        )}
                    </CardContent>
                    {showOrderMore && (
                        <div className="requestSignsControl">
                            <Typography id={"request-slider-" + typeId} variant="h6" color="textPrimary">
                                Request additional signs
                                </Typography>
                            <Slider
                                defaultValue={5}
                                aria-labelledby={"request-slider-" + typeId}
                                valueLabelDisplay="auto"
                                step={1}
                                marks
                                min={1}
                                max={25}
                                onChangeCommitted={this.onRequestAmountChanged}
                            />
                            <div>
                                <Button variant="contained" color="secondary" onClick={this.onHideOrderMore} className="icon">
                                    <CloseIcon className="icon" />
                                </Button>
                                <Button variant="contained" color="primary" onClick={this.onSaveRequest} className="icon">
                                    Send Request
                                    <SendIcon className="icon" />
                                </Button>
                            </div>
                        </div>
                    )}
                    {isSaving && <LinearProgress />}
                </div>
            </Card>
        );
    }
}

SignType.propTypes = {
    typeId: PropTypes.string.isRequired,
    typeName: PropTypes.string.isRequired,
    image: PropTypes.string.isRequired,
    availableSigns: PropTypes.number.isRequired,
    requestedSigns: PropTypes.number.isRequired,
    isSaving: PropTypes.bool.isRequired,
    onRequsetSigns: PropTypes.func.isRequired,
};

export default SignType;

My storybook config

import { configure, addDecorator } from '@storybook/react';
import { withKnobs } from '@storybook/addon-knobs';
import { withInfo } from '@storybook/addon-info';
import { withConsole } from '@storybook/addon-console';

import 'typeface-roboto';

// automatically import all files ending in *.stories.js
const req = require.context('../stories', true, /\.stories\.js$/);
function loadStories() {
  req.keys().forEach(filename => req(filename));
}

addDecorator(withInfo);
addDecorator(withKnobs);
addDecorator((storyFn, context) => withConsole()(storyFn)(context));

configure(loadStories, module);

my addons.js

import '@storybook/addon-actions/register';
import '@storybook/addon-knobs/register';
import '@storybook/addon-links/register';
import '@storybook/addon-actions/register';

Answer

apachuilo picture apachuilo · Aug 12, 2019

Your component is only using props when you get the initial state, you aren't even using them during the render.

Sort of an anti-pattern - https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html

Also not exactly what I would call a 'pure' component - https://reactjs.org/docs/react-api.html#reactpurecomponent

Unless you actually update your state (which you do in your handlers), you won't see an update based on those props.

That said if you wish to continue down this path you'll need a way to react to those prop changes. You could make use of the componentWillReceiveProps lifecycle and call setState here.