ChucKN0risK
ChucKN0risK

Reputation: 425

How can I make Vuex store work with Storybook?

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>

Upvotes: 11

Views: 14551

Answers (5)

Picard
Picard

Reputation: 4112

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>

Upvotes: 1

Oleksandr Stukalo
Oleksandr Stukalo

Reputation: 31

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({
});

Upvotes: 3

Sergeon
Sergeon

Reputation: 6788

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.

Upvotes: 10

Nada Le Coupanec
Nada Le Coupanec

Reputation: 145

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,
    // ...
})

Upvotes: 4

jisoo youn
jisoo youn

Reputation: 71

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: ... 
        }
      }
    };
  });

Upvotes: 7

Related Questions