An (almost) comprehensive guide on using Storybook with Nuxt.js
I’ve been wanting to implement Storybook in my Nuxt.js projects for a while now.
For those who don’t know, Storybook is an open source tool for developing UI components in isolation. Check out its use cases.
Using Storybook with plain Vue.js is no hassle but it’s another story with Nuxt as it doesn’t work straight out the box. The info out there is disperse and I had to dig around other people’s repos and examples to have it working with Nuxt, including with how I usually make use of the Store.
I thought I’d write it down and make a more robust example repository for others out there who want to start using Storyboook with Nuxt.
My usual project setup includes using the Vuex Store, the Nuxt’s Axios module, TailwindCSS and custom SCSS.
That’s roughly what I’d like to see working along with Storybook without having to change too much how I use Nuxt in general.
At the end of this example, we’ll have a List component that loads data externally from JSONPlaceholder.
Check out how it’ll look like here.
This is an extensive guide, so feel free to jump right to the section you’re looking for. You can grab all this code here.
Content
- Setup initial Nuxt project — regular install of Nuxt using npx;
- Adding Storybook to Nuxt — basic install & setup of Storybook;
- Creating a Story — our first example Story;
- Adding a Store to the project — adding Vuex and enhancing the example component with other components and making API calls (not yet using the Store with Storybook);
- Handling Storybook Issues — making Storybook work with:
- the @ alias;
- loading CSS/SCSS;
- the <nuxt-link> tag; - Using Vuex with Storybook — adding Vuex to our Stories and re-using our current Store;
- Deploying it to Netlify — share it to your team and clients;
Initial setup
Because this guide is from the ground up, we’re starting with a fresh Nuxt project using create-nuxt-app:
npx create-nuxt-app nuxt-storybook
Also, we’ll update Nuxt to it’s latest stable release 2.5.1:
npm rm nuxt && npm i -S nuxt
Build error?
At the time of writing this article, upgrading to Nuxt 2.5 results in an error when building:
ERROR Failed to compile with 1 errors friendly-errors 13:29:07
[...]
Module parse failed: Unexpected token (7:24) friendly-errors 13:29:07
[...]
|
| var _0c687956 = function _0c687956() {
> return interopDefault(import('../pages/index.vue'
| /* webpackChunkName: "pages/index" */
| ));
If that’s still the case, the following works on my machine™ (macOS), based on this workaround:
rm -rf node_modules package-lock.json
npm i -D webpack@4.28.4
npm i
Running npm run dev
should now display the default Nuxt welcome page.
Adding Storybook
We’ll install Storybook and necessary dependencies manually according to their guidelines for Vue. Most dependencies are already present due to Nuxt, with babel-preset-vue being the only one missing.
// Add Storybook & dependencies
npm i -D @storybook/vue babel-preset-vue
Now create a folder named .storybook and add the file config.js in it.
Config.js is used as the “entry point” to tell Storybook where to look for and load Stories as well as importing and using other necessary plugins or addons to use with Stories.
Following the Vue Guidelines, config.js would initially look like this:
What it’s doing is iterating over each file ending as .stories.js within the folder stories. Because I like to have my Stories near my Components and not all in bulk within a stories folder, I simply change the folder to components and let the function traverse each folder in it.
We’ll be coming back to config.js later. For now let’s make sure Storybook is able to load a simple Story and display it.
Adding our first Story
Inside your components directory, create a new folder named list and within it a file named List.vue with the code below. We’ll use it to build our final component as we go.
Nothing much, just something to have our Story display. Now in the same folder add a file named List.stories.js with the following code in it:
Now to start Storybook we need to add the run script to package.json (to run it on a different port, add -p <port-number>
)
“storybook”: “start-storybook”
Type innpm run storybook
in your terminal and your browser will open up a new tab:
That’s Storybook running. And because it uses hot reloading, you’ll be able to see your changes reflected instantly.
Notice the second story doesn’t work? That’s because we haven’t told Storybook to use our List component for that Story as we did for the first one (opening the browser console will show you these errors).
We can register List as a global component just like we register them using Nuxt’s plugins, only within config.js, so it ends up looking like:
Now the 2nd story works. That was just to give you an idea that some of your components may use other ones. In order to avoid always importing these, we can define them globally as we did (the remaining example won’t make use of this so you can delete that).
You now have a vanilla setup of Storybook working with Nuxt. But it ain’t much of a story yet.
Enhancing our List component & Adding the Store
First off we’ll add some complexity to our List and worry with the errors Storybook throws at us later.
The List should:
- once mounted — fetch either fake users or fake comments using JSONPlaceholder;
- iterate each user/comment and render it using a ListItem component;
- make use of Vuex to dispatch our API calls;
- look prettier, using TailwindCSS & some custom styles;
Styles
For the styling we’ll use some TailwindCSS utility classes as well as some custom styles to exemplify its usage with Storybook. I use SCSS so we’ll need to add the usual node-sass
& sass-loader:
npm i -D node-sass sass-loader
The List will accept the prop source so it knows which data source we want to fetch. We’ll also leave it prepared to call the adequate Store action to perform the API calls once we’ve build these.
The List component should now look like:
Adding the Store & API calls
I usually keep my API calls in the Store’s actions so I can easily call them using this.$store.dispatch.
.env: We’ll keep our endpoints in a .env file, so in order to get these values we’ll install the @nuxtjs/dotenv module npm i -S @nuxtjs/dotenv
and add it to out nuxt.config.js modules.
Create a .env within the project’s root file and add:
USERS_ENDPOINT=https://jsonplaceholder.typicode.com/users
COMMENTS_ENDPOINT=https://jsonplaceholder.typicode.com/comments
Onto adding out Store actions to retrieve Users and Comments. Add an actions.js file under the existing store directory with the following code:
We can now modify our List component’s methods to call these actions when it is mounted, ending up looking like:
We now get an array of data returned from each endpoint. Let’s display them.
Adding ListItem component
Depending on whether we are listing Users or Comments, we’ll display a variation of the ListItem component. Each variation will have its own component too.
Create a folder under list called items and create a file named ListItem.vue. Here’s the code to add to it:
Nothing much for now, we’ll change it in a moment. In the meanwhile, I styled the home page so we can see both of our lists side by side:
Now let’s actually use our ListItem component to iterate each entity returned by our API and style it accordingly.
Adding a User & Comment component
We’ll create a component for each entity, based on the following data structure:
// User
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
}// Comment
{
"postId": 1,
"id": 1,
"name": "id labore ex et quam laborum",
"email": "Eliseo@gardner.biz",
"body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium"
}
Add a Comment.vue file in /components/list/items/ with the code:
Add a User.vue file in /components/list/items/ with the code:
Note: for the sake of the example I’ve added a nuxt-link. For it we’ve also added the respective page /pages/user/index.vue. There’s nothing in it, just for nuxt-link to link somewhere.
Let’s change our ListItem component to make use of these new components:
Finally we need to change List.vue so we actually pass the API call’s response as props instead of just logging it. Change the methods so it looks like:
// /components/list/List.vue
[...]
methods: {
loadUsers() {
this.$store.dispatch('GET_USERS')
.then(res => {
this.entities = res.data
})
.catch(err => {
console.log('API error')
console.log(err)
})
},
loadComments() {
this.$store.dispatch('GET_COMMENTS')
.then(res => {
this.entities = res.data
})
.catch(err => {
console.log('API error')
console.log(err)
})
},
}
[...]
After some minor style tweaks, this is how it should now look:
We’re now ready to move on to Storybook and see what happens.
Resolving Storybook’s complaints
We’ll now iron out each of the raised issues when running Storybook, the first one being:
Module not found
Error: Can’t resolve ‘@/components/list/items/ListItem’
If you look at Storybook’s examples, you’ll see it references components using relative paths. That’s an issue for us using Nuxt as the framework makes use of the @ alias.
Do we now need to use relative paths everywhere? Luckily no. Remember we installed babel-preset-vue previously? That plus using webpack’s alias allows us to work around this issue.
First, create a file in the .storybook folder named .babelrc with the following:
// /.storybook/.babelrc{
"presets": [
"@babel/preset-env",
"babel-preset-vue"
]
}
Create another file named webpack.config.js in the .storybook folder with the following:
You should now be able to keep using the @ alias to import your components.
For the sake of having concise code, we can now change how we import the List component in its Story from import List from './List'
to import List from '@/components/list/List'
.
Module parse failed: handling SCSS
Storybook now throws:
Module parse failed: Unexpected character ‘#’ (69:13)
You may need an appropriate loader to handle this file type.
That’s because we haven’t specified how to load these. We can resolve that by adding a module rule for CSS/SCSS to webpack, making our file now look like this:
We must also add import '@/assets/css/tailwind.css
to .storybook/config.js so we can use Tailwind’s utility classes.
Fire up Storybook again, and this time you should have the browser open a new tab with the nicest:
Using Vuex with Storybook
If you’ve followed Storybook’s Vue guidelines prior to this walkthrough, you should already be importing and using Vuex in config.js.
If not, here’s how it should be looking now:
But just that won’t cut it.
Nuxt components reference the Store as this.$store and our Story doesn’t know about that, therefore we need to create a new Store and pass it along to our component.
But do we need to recreate the whole Store? Thankfully no. We’ll indeed create a store, but reuse all existing actions, getters, mutations or states our existing Store has.
For that we’ll create a file named store.js in the .storybook folder with the following code:
We can now import and pass along this store to our Stories.
For now we only have a story listing Users, being the default source. Let’s add another Story to list Comments and rename each:
@nuxtjs/axios module caveat
The
store.$axios.$axios = axios
hack allows us to pass Axios to our new store instance but keep in mind it isn’t Nuxt’s module so you won’t have access to all its features.In order to use Stories that require Vuex you may need to make changes on how you’re using $axios so both your Stories and app work but be careful.
In the case of Thoro I needed to change the usage of
this.$axios.$get
tothis.$axios.get
.Can’t figure a way of accessing the app’s actual Store so if you know a better way to do this, share in the comments!
After following the above steps we should now see both Stories for our List component:
Handling <nuxt-link>
Finally we can see something! But our links are missing..
If you open up the browser console on Storybook’s tab, you’ll see it doesn’t know what nuxt-link is (also, you can always have a look at potential errors there if things aren’t working properly).
A final adjustment to Storybook is required in order to have these working and functional.
For that, we need to install the @storybook/addon-actions dependency: npm i -D @storybook/addon-actions
and add them to Storybook by creating the file addons.js in the .storybook directory with the lines:
// /.storybook/addons.jsimport '@storybook/addon-actions'
import '@storybook/addon-actions/register'
Finally we need import { action } from '@storybook/addon-actions
in config.js and register a tweaked nuxt-link component to Vue. Our config.js file should now look like:
This replaces all instances of <nuxt-link> with a regular anchor element as well as setting a log method showing the lint’s path when clicking it.
After that, we should no longer see any error on the browser’s console and have actual clickable links on our users’ names:
Storybook working with Nuxt!
It took a while but we’ve managed to have Storybook working nicely with Vue.js components within a Nuxt.js project.
This isn’t a fully-fledged guide as we’re missing tests and key aspects of Nuxt such as the <no-ssr> tag (I also wonder how asyncData and Storybook may end up working together).
Bonus: deploy Storybook to Netlify
When running Storybook, you get an IP you can share to others in your local network and that’s cool if you’re on the same WiFi. But what if you want to share it to your clients so they can give you feedback on last week’s iteration?
In that case, host it on Netlify. Simply add the script below to your package.json file which will generate a static Storybook in the storybook-static directory:
"build-storybook": "build-storybook -c .storybook"
Then access Netlify and select your repository. Define the build command as npm run build-storybook
and the publish directory as storybook-static
.
You should then have your Storybook live and updated each time you push/merge into your master branch. Check it out!
Accessing environment variables
Most likely you’ll have environment variables being used throughout your store or components.
In order to build Storybook with those variables, you first need provide them to Netlify and then make use of webpack’s DefinePlugin to expose them through Storybook’s webpack.config.js, like such:
Final Repo & Resources
Feel free to grab the code on Github (https://github.com/mstrlaw/nuxt-storybook) and check out this reading material and other repos that were useful to build this guide:
- Storybook’s Vue guide;
- learnstorybook.com (Vue guide);
- This blog post (chinese) and this repo;
- ’s blog post which dives into actual tests using Jest;
- This question;
Chime in and leave your thoughts and suggestions in the comments below.
Edits:
- Added in how to access env variables when deploying to Netlify;
- Changed code snippets to Gists;