How can I make Vuex store work with Storybook?
Asked Answered
B

5

11

I have a component story that requires an API call performed by an ACTION from my Vuex store. However, the store can't be found by Storybook: Unhandled promise rejection TypeError: "this.$store is undefined".

I've tried to access the store through the created and mounted Vue lifecycle hooks but each of them returned undefined.

My Vuex store is correctly working inside my app.

I run on storybook 5.0.1 and vuex 3.1.1.

Here's my storybook config.js:

// Taken from https://davidwalsh.name/storybook-nuxt & https://github.com/derekshull/nuxt-starter-kit-v2/blob/master/.storybook/config.js
import { addParameters, configure } from '@storybook/vue';
import { withOptions } from '@storybook/addon-options';
import { setConsoleOptions } from '@storybook/addon-console';
import { create } from '@storybook/theming';
import Vue from 'vue';
import VueI18n from 'vue-i18n';

// Vue plugins
Vue.use(VueI18n);

setConsoleOptions({
  panelExclude: [],
});

// Option defaults:
addParameters({
  options: {
    /**
     * show story component as full screen
     * @type {Boolean}
     */
    isFullScreen: false,
    /**
     * display panel that shows a list of stories
     * @type {Boolean}
     */
    showNav: true,
    /**
     * display panel that shows addon configurations
     * @type {Boolean}
     */
    showPanel: true,
    /**
     * where to show the addon panel
     * @type {String}
     */
    panelPosition: 'bottom',
    /**
     * sorts stories
     * @type {Boolean}
     */
    sortStoriesByKind: false,
    /**
     * regex for finding the hierarchy separator
     * @example:
     *   null - turn off hierarchy
     *   /\// - split by `/`
     *   /\./ - split by `.`
     *   /\/|\./ - split by `/` or `.`
     * @type {Regex}
     */
    hierarchySeparator: /\/|\./,
    /**
     * regex for finding the hierarchy root separator
     * @example:
     *   null - turn off multiple hierarchy roots
     *   /\|/ - split by `|`
     * @type {Regex}
     */
    hierarchyRootSeparator: /\|/,
    /**
     * sidebar tree animations
     * @type {Boolean}
     */
    sidebarAnimations: true,
    /**
     * enable/disable shortcuts
     * @type {Boolean}
     */
    enableShortcuts: true,
    /**
     * theme storybook, see link below
     */
    theme: create({
      base: 'light',
      brandTitle: '',
      brandUrl: '',
      // To control appearance:
      // brandImage: 'http://url.of/some.svg',
    }),
  },
});

const req = require.context('../src/components', true, /\.story\.js$/)

function loadStories() {
  req.keys().forEach((filename) => req(filename))
}

configure(loadStories, module);

Here's my component's story:

import { storiesOf } from '@storybook/vue';
import { withReadme } from 'storybook-readme';
import { withKnobs } from '@storybook/addon-knobs';
import HandoffMainView from './HandoffMainView.vue';
import readme from './README.md';

storiesOf('HandoffMainView', module)
  .addDecorator(withReadme([readme]))
  .addDecorator(withKnobs)
  .add('Default', () => {
    /* eslint-disable */
    return {
      components: { HandoffMainView },
      data() {
        return {
          isLoading: true,
          component: {
            src: '',
            data: [],
          },
        };
      },
      template: '<handoff-main-view :component="component" />',
    };
  });

Here's my component:

<template>
  <main class="o-handoff-main-view">
    <div class="o-handoff-main-view__content">
      <div
        :class="[
          'o-handoff-main-view__background',
          background ? `o-handoff-main-view__background--${background}` : false
        ]"
      >  
        <loader
          v-if="isLoading"
          :color='`black`'
          class="o-handoff-main-view__loader"
        />
        <div
          v-else
          class="o-handoff-main-view__ui-component"
          :style="getUiComponentStyle"
        >
          <img
            :src="uiComponent.src"
            alt=""
          >
          <handoff-main-view-layer-list
            :layers="uiComponent.data"
          />
        </div>
      </div>
    </div>
    <div class="o-handoff-main-view__controls">
      <handoff-main-view-zoom-handler
        :default-zoom-level="zoomLevel"
        :on-change="updateZoomLevel"
      />
    </div>
  </main>
</template>

<script>
  import { mapActions } from 'vuex';
  import Loader from '../../01-atoms/Loader/Loader.vue';
  import HandoffMainViewZoomHandler from '../HandoffMainViewZoomHandler/HandoffMainViewZoomHandler.vue';
  import HandoffMainViewLayerList from '../HandoffMainViewLayerList/HandoffMainViewLayerList.vue';

  export default {
    components: {
      Loader,
      HandoffMainViewZoomHandler,
      HandoffMainViewLayerList,
    },
    props: {
      background: {
        type: String,
        default: 'damier',
      },
      component: {
        type: Object,
        required: true,
      },
    },
    data() {
      return {
        isLoading: true,
        zoomLevel: 1,
        uiComponent: {
          src: null,
        }
      };
    },
    mounted() {
      this.setUiComponentImage();
    },
    methods: {
      ...mapActions('UiComponent', [
        'ACTION_LOAD_SIGNED_URLS'
      ]),
      async setUiComponentImage() {
        const uiComponentImg = new Image();
        const signedUrls = await this.ACTION_LOAD_SIGNED_URLS([this.component.id]);
        uiComponentImg.onload = () => {
          this.isLoading = false;
        };
        uiComponentImg.src = this.uiComponent.src;
      },
    },
  };
</script>
Borderline answered 20/6, 2019 at 9:0 Comment(1)
IMO you shouldn't need vuex for storybook. Components in storybook should be emitting events to a parent component which calls the store. Nearly all data in storybook should be dummy data.Daedal
I
10

I bet somewhere in your app, probably main.js, you're doing something like:

import Vuex from 'vuex';
Vue.use(Vuex);

const store = new Vuex.Store({
  state,
  mutations,
  getters,
});

And then, when creating the Vue app, your calling new Vue({store, i18n...}).

You're already forging Vue with the ' i18n' module in your config.js. You would need to import Vuex and the store there too.


Now, having to import your store -or mock it- in your storybook setup may be a smell of your components being too large, or being too coupled with your store.

Usually, storybook is more intended to show components that display stuff (form controls, list of things... ) that have a dedicated functionality. Such components usually communicate with the rest of your application via props and events. Let's call this presentational components.

On the contrary, components that communicates with a store are usually views or pages, and they orchestrate the state and talk with the backend, and supply data to the former.

I think you should display on the storybook showcase only presentational components, and avoid talking global modules within them. At least, I believe this is the spirit behind storybook and how it is mainly used. That may be the reason because you don't find much docs about how to mock your store in storybook: storybook projects usually don't connect to vuex in the first place, I think.

Ivanovo answered 20/6, 2019 at 9:55 Comment(3)
Indeed, I have a main.js where I include all my Vue config and especially my Vuex store. I also think I should import my store in my storybook config.js file but I don't know what to do next. I'm starting to think I need to mock the specific parts of the store of my app and put them inside my component stories. But it's not clear at all. I couldn't find any documentation about that.Borderline
Updated my answer about that.Ivanovo
I totally get your point and I think I might agree with you. This makes me think about this article I saw a while ago: medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0Borderline
A
7

pass new store instance (or mocking) in story

import Vuex from "vuex";
import { storiesOf } from '@storybook/vue';
import { withReadme } from 'storybook-readme';
import { withKnobs } from '@storybook/addon-knobs';
import HandoffMainView from './HandoffMainView.vue';
import readme from './README.md';

storiesOf('HandoffMainView', module)
  .addDecorator(withReadme([readme]))
  .addDecorator(withKnobs)
  .add('Default', () => {
    /* eslint-disable */
    return {
      components: { HandoffMainView },
      data() {
        return {
          isLoading: true,
          component: {
            src: '',
            data: [],
          },
        };
      },
      template: '<handoff-main-view :component="component" />',
      store: new Vuex.Store({ // here
        modules: {
          namespaced: true,
          actions: ... 
        }
      }
    };
  });
Acrefoot answered 26/8, 2019 at 9:34 Comment(0)
V
4

If you are using Nuxt.js, here is how you can do it:

./storybook/store.js

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const store = new Vuex.Store({
    state: require("../store/index.js").state,
    getters: require("../store/index.js").getters,
    actions: require("../store/index.js").actions,
    mutations: require("../store/index.js").mutations,

    modules: {
        ads: {
            namespaced: true,
            state: require("../store/ads.js").state,
            getters: require("../store/ads.js").getters,
            actions: require("../store/ads.js").actions,
            mutations: require("../store/ads.js").mutations
        },

        features: {
            namespaced: true,
            state: require("../store/features.js").state,
            getters: require("../store/features.js").getters,
            actions: require("../store/features.js").actions,
            mutations: require("../store/features.js").mutations
        },


        user: {
            namespaced: true,
            state: require("../store/user.js").state,
            getters: require("../store/user.js").getters,
            actions: require("../store/user.js").actions,
            mutations: require("../store/user.js").mutations
        },
    }
});

export default store

Then in your story:

// ...
import store from '@/.storybook/store';

export default {
    title: 'MyComponent'
};

export const MyComponentStory = () => ({
    store: store,
    // ...
})
Vacationist answered 27/6, 2020 at 12:30 Comment(1)
After struggling for hours on my setup I simply had to add store: store, as per your advice.Janice
V
3

You could try to use a decorator

import { createStore } from 'vuex';

const _vue = require("@storybook/vue3");
const _addons = require("@storybook/addons");

const withVueRouter = function withVueRouter() {
  const store = arguments?.[0] || createStore({ state: {} });
  return _addons.makeDecorator({
    name: 'withStore',
    parameterName: 'withStore',
    wrapper: (storyFn, context) => {
      _vue.app.use(store);
      return storyFn(context);
    }
  });
};

export default withVueRouter;

usage

import withStore from '../../../config/storybook/decorators/withStore';
import { createStore } from 'vuex';

const store = createStore({
  state: {
    film: films[0],
  },
});

export default {
  title: 'film-details/FilmDetails',
  decorators: [withStore(store)]
};

const FilmDetailsTemplate = (args) => ({
  components: { FilmDetails },
  template: '<FilmDetails/>',
});

export const template = FilmDetailsTemplate.bind({
});
Vagrom answered 17/12, 2021 at 23:27 Comment(0)
R
1

If you're looking for a solution with .mdx type of story files, then you can mock the store behavior like this (I use the namespaced store configuration):

<!-- SomeComponent.stories.mdx -->

import Vuex from 'vuex';

[...]

export const Template = (args, { argTypes }) => ({
  props: Object.keys(argTypes),
  components: { SomeComponent },
  store: new Vuex.Store({
    modules: {
      auth: {
        namespaced: true,
        state: {
          user: {
            id: 20,
            avatar: "/images/avatar.png",
            name: "John Doe",
            login: "jonh.d",
          }
        },
        getters: {
          userPublicData: () => {
            return {
              id: 20,
              avatar: "/images/avatar.png",
              name: "John Doe",
              login: "jonh.d",
            };
          },
        }
      },
    },
  }),
  template: `
    <SomeComponentv-bind="$props" />
  `,
});

<Canvas>
  <Story
    name="Basic"
    args={{
    }}>
    {Template.bind({})}
  </Story>
</Canvas>
Realtor answered 11/1, 2022 at 9:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.