storybook: Vue component Error: Failed to mount component

Describe the bug Custom components imported to Storybook do not render, console error:

[Vue warn]: Failed to mount component: template or render function not defined.

found in

---> <Logo> at components/logo/Logo.vue

To Reproduce Steps to reproduce the behavior:

  1. Create story of a custom component.
  2. start-storybook -p 9001 -c .storybook
  3. See error in console

Expected behavior Component Renders correctly

Screenshots Components___Logo_-_Logo_⋅_Storybook_and_New_Issue_·_storybookjs_storybook

System:

  • OS: MacOS
  • Device: Macbook Pro 2018
  • Browser: chrome
  • Framework: vue, nuxt
  • Version: 5.0.11
  • node -v v12.3.1

Additional context

package.json

{
// ...
  "author": "Alvaro Saburido",
  "private": true,
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
    "precommit": "npm run lint",
    "test": "jest --verbose",
    "serve:static": "nuxt generate && serve ./dist",
    "storybook": "start-storybook -p 9001 -c .storybook"
  },
  "dependencies": {
    "@nuxtjs/pwa": "^2.6.0",
    "babel-plugin-dynamic-import-node": "^2.2.0",
    "cross-env": "^5.2.0",
    "nuxt": "^2.8.0",
    "nuxt-i18n": "^5.12.3",
    "save": "^2.4.0"
  },
  "devDependencies": {
    "@nuxtjs/eslint-config": "^0.0.1",
    "@nuxtjs/style-resources": "^0.1.2",
    "@storybook/addon-actions": "5.0.11",
    "@storybook/addon-centered": "5.0.11",
    "@storybook/addon-console": "^1.1.0",
    "@storybook/addon-options": "5.0.11",
    "@storybook/addon-storysource": "5.0.11",
    "@storybook/addon-viewport": "5.0.11",
    "@storybook/addons": "5.0.11",
    "@storybook/vue": "^5.0.11",
    "@vue/test-utils": "^1.0.0-beta.27",
    "babel-core": "7.0.0-bridge.0",
    "babel-eslint": "^10.0.1",
    "babel-jest": "^24.8.0",
    "babel-preset-vue": "^2.0.2",
    "eslint": "^5.16.0",
    "eslint-config-prettier": "^4.3.0",
    "eslint-config-standard": ">=12.0.0",
    "eslint-loader": "^2.1.2",
    "eslint-plugin-import": ">=2.17.3",
    "eslint-plugin-jest": ">=22.6.4",
    "eslint-plugin-node": ">=9.1.0",
    "eslint-plugin-nuxt": ">=0.4.3",
    "eslint-plugin-prettier": "^3.1.0",
    "eslint-plugin-promise": ">=4.1.1",
    "eslint-plugin-standard": ">=4.0.0",
    "eslint-plugin-vue": "^5.2.2",
    "jest": "^24.8.0",
    "node-sass": "^4.12.0",
    "nodemon": "^1.19.1",
    "prettier": "^1.17.1",
    "sass-loader": "^7.1.0",
    "storybook-readme": "^5.0.3",
    "vue-jest": "^3.0.4",
    "vue-loader": "^15.7.0"
  }
}

.sotybook/config.js

import { configure } from '@storybook/vue'
import { setOptions } from '@storybook/addon-options'
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

Vue.component('nuxt-link', {
  functional: true,
  render: function(createElement, context) {
    let allClass = {}
    let arrClass = context.data.staticClass
      ? context.data.staticClass.split(' ')
      : []
    arrClass.forEach(theClass => {
      allClass[theClass] = true
    })
    return createElement('a', { class: allClass }, context.children)
  }
})
Vue.component('no-ssr', {
  functional: true,
  render(_createElement, context) {
    return context.children
  }
})

setOptions({
  name: 'Arepa Ipsum',
  url: 'https://github.com/hunterliu1003/blog',
  addonPanelInRight: true
})

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

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

configure(loadStories, module)

.storybook/webpack.config.js

const path = require('path');

module.exports = {
  module: {
    rules: [
      {
        test: /\.stories\.js?$/,
        loaders: [require.resolve('@storybook/addon-storysource/loader')],
        enforce: 'pre'
      },

      {
        test: /\.vue?$/,
        use: ['vue-loader', 'vue-style-loader']
      }
    ]
  },
  resolve: {
    alias: {
      '@': path.dirname(path.resolve(__dirname)),
      vue$: 'vue/dist/vue.common.js'
    }
  }
}

Logo.vue

<template>
  <h1 class="logo">
    <img src="~/assets/svg/icon.svg" alt="Arepa Ipsum Logo" />
  </h1>
</template>

<style scoped>
.logo {
  width: 256px;
  height: 256px;
}
</style>

⚠️UPDATE: after a lot of digging into it, the problem is when using vue-loader in the webpack.config.js together with storybook. I saw in other issues it’s a pretty common problem reported before. Any known fix?

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 4
  • Comments: 20 (11 by maintainers)

Most upvoted comments

This is a configuration issue, not a casing nor loader’s one.

The problem

Adding vue-loader to loaders array. Storybook (in this case, @storybook/vue) has its own preconfigured loaders array. Adding vue-loader to module.rules causes duplicated vue-loader rules, which does <source code> -> vue-loader -> vue-loader.

// like this
module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: ['vue-loader', 'vue-loader']
      }
    ]
  }
}

So you should not add vue-loader without removing an existing one.

How to fix

  1. Just remove the ~vue-loader~ rule for .vue file from your custom webpack config (.storybook/webpack.config.js). In many cases, you won’t need to set it manually (@storybook/vue’s default config does the job well).
// .storybook/webpack.config.js
module.exports = ({ config }) => {
  // config.module.rules.push({
  //   test: /\.vue$/,
  //   use: [{
  //     loader: 'vue-loader',
  //   }]
  // })

  return config
}
  1. If you need to use vue-loader with non-default options, you can add it with removing default one.
// .storybook/webpack.config.js
module.exports = ({ config }) => {
  // Remove SB's default vue-loader
  config.module.rules = config.module.rules.filter(rule =>
    !(rule.test instanceof RegExp) || !rule.test.test('.vue')
  )

  config.module.rules.push({
    test: /\.vue$/,
    use: [
      {
        loader: 'vue-loader',
        options: { /* your options here */ }
      }
    ]
  })
}

I posted in #7593, but I suppose it makes more sense to post my final webpack config here as well for anyone who’s having similar issues:

From #7593 :

ok I got this working with the following config. Hoping this helps someone in the future. The difference between the sass and the scss rules are the semicolon at the end of the imported styles.scss file. One expects semicolons and one doesn’t so combining them into 1 loader became impossible.

Frankly getting Storybook working with Vuetify is entirely too much trouble

const path = require('path');

module.exports = async ({ config }) => {

  function resolve(dir) {
    return path.join(__dirname, '..', dir);
  }

  /** removes existing scss rule */
  config.module.rules = config.module.rules.filter(rule =>
    !rule.test.test('.scss')
  )
  config.module.rules.push({
    test: /\.sass$/,
    use: [
      'vue-style-loader',
      'css-loader', {
        loader: 'sass-loader',
        options: {
          implementation: require('sass'),
          data: `
            @import '@/styles/styles.scss'
          `,
        }
      },
    ],
  }, {
    test: /\.scss$/,
    use: [
      'vue-style-loader',
      'css-loader', {
        loader: 'sass-loader',
        options: {
          implementation: require('sass'),
          data: `
            @import '@/styles/styles.scss';
          `,
        }
      },
    ],
  })

  config.resolve = {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      vue$: 'vue/dist/vue.esm.js',
      '@': resolve('src'),
    },
  };

  return config;
}; 

OK, my bad I just did not look at your screenshot hard enough. I have your story. Just as a rule of thumb, if you want to share code, it usually is better to share it with text 😉

Thank you for sharing.