Storybook: @storybook/[email protected] and @storybook/[email protected] do not update components using JSX render functions in stories

Created on 22 Nov 2018  路  23Comments  路  Source: storybookjs/storybook

Describe the bug
Knobs no longer update components when using JSX render functions in stories. I managed to get it working for standard string templates, but I have a large project using JSX for stories.

To Reproduce
Steps to reproduce the behavior:

  1. git clone https://gitlab.com/alexkcollier/storybook-vue-knobs-issue.git
  2. npm install
  3. npm start
  4. Change a knob

Expected behavior
Component in story should updates, but won't unless you rollback storybook and addons to 4.0.7

Screenshots
Note how knobs do not match component state.
image

Code snippets

// SButton.stories.jsx

import SButton from './SButton.vue'
import { boolean, select, text, withKnobs } from '@storybook/addon-knobs'
import { storiesOf } from '@storybook/vue'

storiesOf('SButton JSX', module)
  .addDecorator(withKnobs)
  .add('Button JSX', () => {
    const buttonColor = select('Button color', ['green', 'red'], 'green', 'Optional Props')
    const buttonSize = select(
      'Button size',
      ['small', 'regular', 'large'],
      'small',
      'Optional Props'
    )
    const buttonText = text('Button text', 'Sample text', 'Slots')

    const props = {
      buttonColor,
      buttonSize
    }

    const disabled = boolean('disabled', false, '$attrs')

    return {
      render: h => (
        <div>
          <SButton {...{ props }} disabled={disabled}>
            {buttonText}
          </SButton>
          <p>{buttonColor}</p>
          <p>{buttonSize}</p>
        </div>
      )
    }
  })
// SButton.vue

<template>
  <button v-bind="$attrs" :class="classList" class="button"><slot /></button>
</template>

<script>
export default {
  name: 'SButton',

  props: {
    buttonColor: {
      type: String,
      default: ''
    },

    buttonSize: {
      type: String,
      default: ''
    }
  },

  computed: {
    classList() {
      return [
        // Handles storybook default
        this.buttonColor ? `button--color-${this.buttonColor}` : 'button--color-green',
        this.buttonSize ? `button--size-${this.buttonSize}` : ''
      ]
    }
  }
}
</script>

<style>
.button--color-green {
  background-color: green;
}
.button--color-red {
  background-color: red;
}

.button--size-large {
  font-size: 3rem;
}
.button--size-regular {
  font-size: 1rem;
}
.button--size-small {
  font-size: 0.5rem;
}
</style>

System:

  • OS: Windows 10

    • Device: Desktop

  • Browser: Chrome
  • Framework: Vue, project configured using vue-cli
  • Addons: @storybook/addon-knobs
  • Version: 4.0.8
    .
knobs vue bug has workaround

Most helpful comment

you should have an invite to a repo. thank you for your help :-)

Edit: If you go to storybook-demo you can see the same behaviour. When you click on the section knobs and then on "All Knobs" you will see that the component will not react to the changes of knobs. Just try to alter the name for example.

... sry i missed the "public" ...
here it is: StorybookTest-Repo

All 23 comments

@igor-dv looks like this PR might have introduced a regression https://github.com/storybooks/storybook/pull/4773

CC @y-nk

The same issue occurs without the render function:

Story:

import { storiesOf } from '@storybook/vue'
import { withKnobs, text } from '@storybook/addon-knobs'
import TestComponent from './test-component.vue'

storiesOf(TestComponent.name, module)
  .addDecorator(withKnobs)
  .add('as Component', () => {
    const textProp = text('text', '')
    return {
      components: { TestComponent },
      template: `<TestComponent text="${textProp}" />`,
    }
  })

Component:

<template>
  <div><p>{{ text }}</p></div>
</template>

<script>
export default {
  name: 'TestComponent',
  props: {
    text: {
      type: String,
      default: 'peter',
    },
  },
}
</script>

OS: Windows 10
Device: Desktop
Browser: Chrome
Framework: Vue, project configured using vue-cli
Addons: @storybook/addon-knobs
Version: 4.0.9
Storybook Version: 4.0.9

We made changes with knobs when handling Vue.

As you may know, Vue renders differently from React.

The way promoted before in storybook was using knobs as fixed values in template, or as values bound to the internal state of the story component (data). The direct consequence is that the story component was destroyed and recreated each and every time a knob was updated (easily spottable with a console.log in created() and/or destroyed())

As for now, with @igor-dv we proposed to migrate this behavior to preserve the component instance and prefer the use of knobs as props, as it should be (since knobs are external values injected into your component - just what props are).

for @alexkcollier - i think the story component should be written this way instead :

storiesOf('SButton JSX', module)
  .addDecorator(withKnobs)
  .add('Button JSX', () => ({
    props: {
      buttonColor: {
        type: String,
        default: select('Button color', ['green', 'red'], 'green', 'Optional Props')
      },

      buttonSize: {
        type: String,
        default:  select(
          'Button size',
          ['small', 'regular', 'large'],
          'small',
          'Optional Props'
        )
      },

      buttonText: {
        type: String,
        default: text('Button text', 'Sample text', 'Slots')
      },

      disabled: {
        type: Boolean,
        default: boolean('disabled', false, '$attrs')
      },
    },

    render(h) {
      const { buttonText, disabled, ...props } = this.$props

      return (<div>
        <SButton {...{ props }} disabled={disabled}>
          {buttonText}
        </SButton>
        <p>{buttonColor}</p>
        <p>{buttonSize}</p>
      </div>)
    }
  }))

for @Laslo89 :

import { storiesOf } from '@storybook/vue'
import { withKnobs, text } from '@storybook/addon-knobs'
import TestComponent from './test-component.vue'

storiesOf(TestComponent.name, module)
  .addDecorator(withKnobs)
  .add('as Component', () => ({
    components: { TestComponent },

    props: {
      textProp: {
        type: String,
        default: text('text', '')
      }
    },

    template: `<TestComponent text="${textProp}" />`,
  }))

@alexkcollier @Laslo89 As your examples are mentioning stateless components, you may not be feeling the effects which we are trying to solve with this ; but if you ever encounter a component with an internal state, this will become obvious.

Every comment/remarks regarding this modification is highly welcomed.

@y-nk , I think now maybe we need somehow to have a backward comparability to the previous functionality. Otherwise it looks like a breaking change

@y-nk Thank you for your answer.
Unfortunately the way you wrote the story, does not work. I think what you've wanted to write is:

storiesOf(TestComponent.name, module)
  .addDecorator(withKnobs)
  .add('as Component', () => ({
    components: { TestComponent },
    props: {
      textProp: {
        type: String,
        default: text('text', 'peter'),
      },
    },
    template: `<TestComponent :text.sync="textProp" />`,
  }))

But still it never updates the component, when I change the value of the knob. Is this working as intended?

@igor-dv indeed i'll try to think about a backward compatibility but i'm foreseeing it will probably collide (as in, _maybe_ not possible).

@Laslo89 I've been running your example in the vue-kitchen-sink with success, running on tag v4.1.0-alpha.8. I can see the knob modifying the prop, and the TestComponent reacts to it.

Component :

<template>
  <div><p>{{ text }}</p></div>
</template>

<script>
export default {
  name: 'TestComponent',
  props: {
    text: {
      type: String,
      default: 'peter',
    },
  },
}
</script>

Story :

import { storiesOf } from '@storybook/vue'
import { withKnobs, text } from '@storybook/addon-knobs'
import TestComponent from './test-component.vue'

storiesOf(TestComponent.name, module)
  .addDecorator(withKnobs)
  .add('as Component', () => ({
    components: { TestComponent },

    props: {
      textProp: {
//        type: String, // default value is String, so no need to write it
        default: text('text', 'peter')
      }
    },

    template: '<TestComponent :text="textProp" />',
  }))

As you can see from this file, there's no need for .sync. Basically, when a knob changes, the export default render function gets called. A simple { prop: value } object will be built from the story. As we use the default field to declare the knob, this should be always in sync with the knob values ; then it's passed in #60, and finally triggers a new render with the knobs values from before.

@y-nk Thank you for the detailed explanation. I tried your story and with your advice,I updated the knobs-addon and storybook to v4.1.0-alpha.8 but the issue remains. It also shows same odd side effect. When I am changing the knob nothing happens, but when i change the knob, click on another story and go back to story before, it shows my change.

I should mention, that i add storybook via storybook-cli-plugin it automatically adds the 4.0.0-alpha.20 maybe this is the issue.

@Laslo89 i'd like to offer more help. is there any way to isolate your trial into a public repo which i could fork ?

you should have an invite to a repo. thank you for your help :-)

Edit: If you go to storybook-demo you can see the same behaviour. When you click on the section knobs and then on "All Knobs" you will see that the component will not react to the changes of knobs. Just try to alter the name for example.

... sry i missed the "public" ...
here it is: StorybookTest-Repo

@Laslo89 sorry for my late response.


First : About the non-working "all knobs" demo :

It's actually normal that way. I think was _kindof a false-positive working feature_ in my opinion as I'm not convinced that this way of coding is a best practice regarding Vue components.

If you look at the source code of the story, then you can see that the story component is a simple { template: String } object, which template is a pure static string (because all the knobs are injected in the string from the backquotes). The direct consequence of that is that _the story component cannot mutate_ ; it will be _destroyed and recreated every single time_ you modify a knob and then all the internal changes and css animations you could have running will be destroyed/reset. You can see this behavior here. The patch I've been pushing prevents this by sending new prop values instead ; here's an example about it (following the previous one). That way, even if you use sub components (like in your TestComponent story), they will not be re-created at render ( non-working demo / working demo ).

I will update the examples asap so this practice won't be encouraged by storybook anymore.


On a more practical way, I found your bug. It actually comes from here : https://github.com/Laslo89/StorybookTest/blob/master/.storybook/config.js#L19-L22

Because of this wrapper, the render functions receives the vuetify app component instead of the actual story. I believe storybook should support this, and I'm working with @igor-dv to provide the best fix in no time.

In the meantime, in order not to let you blocked, if you only used this wrapper for providing the store to your component, you can comment this wrapper and provide the store directly in your stories.

@y-nk Thanks for taking the time and your detailed explanation.

when I remove the Decorator and properly update to the "@next" Version of knobs it works.

Before you pointed out the bug I also tried to remove the wrapper and update to "@next". The only difference was, that I updated Storybook as well. This lead to a webpack html-loader error. Therefore i reverted it to 4.0.9

Until you fix the (vuetify) wrapper issue I can live with that workaround of providing the store directly in my story . Thanks again for your great Help. Storybook is a great tool and improves my / our dev-process so much. I really liked it back in the days just for react and I really like it now!

@Laslo89 i'm glad i could be of help. I couldn't see your webpack html loader error when i ran your repo, so feel free to let me know ; it could be a side effect related to our update somehow.

We'll update you about this bug/issue whenever possible.

I still can not get my component to update its values, no matter what I do, the value is stuck. Is there a specific combination of versions I have to use? Thanks!

Edit: also I got a strange error now that my prop value is supposedly a function?

Invalid prop: type check failed for prop "user". Expected Object, got Function.

Also, the default value never gets called (the line with the debugger statement).
All versions are latest, expect knobs is, which is on "next" after reading this thread.

general bootstrap code:

import {addDecorator} from '@storybook/vue';

import {withNotes} from '@storybook/addon-notes';
import {withKnobs} from '@storybook/addon-knobs';

....
addDecorator(withKnobs);
addDecorator(withNotes);
....

story bootstrap code:

import {storiesOf} from '@storybook/vue';

import BuilderReadme from '../README.md';
import BuilderStory from './components/BuilderStory.vue';
import {boolean} from '@storybook/addon-knobs';

storiesOf('Customizer', module)
    .add('Builder', () => {

        const user = {
            test: boolean('test', true)
        };

        console.log(user);

        return {

            components: {BuilderStory},

            props: {
                userInput: {
                    type: Object,
                    default: () => {
                        debugger;
                        return user;
                    }
                }
            },

            template: '<BuilderStory :user="userInput" />'

        };

    }, {notes: {markdown: BuilderReadme}});

BuilderStory.vue

<template>
  <div>{{ user.test ? 'true' : 'false' }}</div>
</template>

<script>
export default {
  props: {
    user: Object
  }
}
</script>

@dasdeck Your template should be <BuilderStory :user="userInput()" />. You can also just set userInput.default to user, rather than the function.

@y-nk I refactored to your suggested implementation on 4.0.11 and ran into a new issue . Wrapping stories in a global decorator stops the knobs from being reactive.

@alexkcollier Thats the issue that i got into. ... This is a open issue / bug. see some comments above. At the moment, it is not possible to use knobs with global decorators that wrap stories

Sorry about that. I should read closer

the decorator problem from @Laslo89 should be fixed, once #5057 is merged

Yee-haw!! I just released https://github.com/storybooks/storybook/releases/tag/v4.2.0-alpha.6 containing PR #5057 that references this issue. Upgrade today to try it out!

Because it's a pre-release you can find it on the @next NPM tag.

Was this page helpful?
0 / 5 - 0 ratings