loading...
Cover image for Develop Components for your Next.js Site in Isolation with Storybook
OpenPatch

Develop Components for your Next.js Site in Isolation with Storybook

mikebarkmin profile image Mike Barkmin ・5 min read

All of our projects are open source, so you can go head and explore our repository https://gitlab.com/openpatch/web-frontend. You can also find our storybook here https://openpatch.gitlab.io/web-frontend/

Do you ever just started developing your next website with nextjs and after a while felt unproductive, because all of your components are scattered throughout your site?

Then you want to add something like an indicator to cards displayed on your site, showing whether they are public or private. Now you need to change this component, reload the site and maybe provide some new data via a mock-server or a real API.

But there is a better way. Many in the React community already using storybook to develop components in isolation. In this blog post I will show you in five acts how we at OpenPatch have integrated storybook in our existing nextjs site.

Act 1: Evaluating the stack

At first we evaluated our stack and assess what a storybook for our components should be capable of.

Our stack:

  • Next.js
  • Material UI for our visuals (we support dark-mode 😎)
  • Lingui for supporting multiple languages
  • ReST-APIs provided by other backend services

So we wanted to have a storybook, which is capable of switching between dark and light mode, switching between different languages and mocking our backend services.

Act 2: Adding storybook

This should be simple, right? Just run npx -p @storybook/cli sb init, write some stories and you should be golden. Unfortunately this is not the case. If you just do this, you will run in many webpack and babel errors. This is because nextjs uses a custom babel configuration baked into the next ... commands, and we use a custom webpack configuration in next.config.js.

OK then. We need to share these configurations with storybook, but how do we do this?

Babel

You can provide a custom .babelrc which will be picked up by storybook. So we created this one:

{
  "presets": ["next/babel"],
  "plugins": [
    "macros"
  ]
}

We just use the next babel preset, this fixes all the babel errors. Only the ones which are produced by lingui are not fixed. Therefore, we also added the macros plugins.

Webpack

Because we are also using a custom webpack config, we also need to account for that. Therefore, we create a .storybook/main.js and copied the webpack configuration from next.config.js over.

const webpack = require('webpack');

module.exports = {
  webpackFinal: async (baseConfig) => {
    baseConfig.module.rules.push({
      test: /\.po/,
      use: [
        {
          loader: '@lingui/loader',
        },
      ],
    });
    return baseConfig;
  },
};

Maybe there is a better way, but this works.

Act 3: Writing Stories

We wanted to have our stories alongside the components, therefore we added stories: ['../**/*.stories.(js|mdx)'] to our storybook config. Now we are ready to go and can write our stories.

But the stories are not picking up our material ui theme or allowing us to change to dark mode or change the language. So we have to modify our storybook config a little more, to fit our needs.

Act 4: Integrate our Stack into Storybook

Material UI

We are using a custom ThemeProvider, which allows to switch to dark-mode on the fly. It looks like this:

export const ThemeProviderView = ({
  darkMode,
  primary,
  secondary,
  ...props
}) => {
  const theme = useMemo(
    () =>
      createMuiTheme({
        palette: {
          type: darkMode ? 'dark' : 'light',
          primary: {
            main: primary,
          },
          secondary: {
            main: secondary,
          },
        },
      }),
    [darkMode, primary, secondary]
  );
  return <MuiThemeProvider theme={theme} {...props} />;
};

To integrate this ThemeProvider into our storybook, we need to create a .storybook/preview.js. In this will we can add custom decorators to storybook. So we could just do this:

import { addDecorator } from '@storybook/react';
import CssBaseline from '@material-ui/core/CssBaseline';

import ThemProvider from '../theme';

const withMaterialUI = storyFn => (
    <ThemeProvider darkMode={true}>
        <CssBaseline />
        {storyFn()}
    </ThemeProvider>
);

addDecorator(withMaterialUI);

Because we are also loading the Roboto font from google in our nextjs site, we need to create a custom .storybook/preview-head.html:

<link
  rel="stylesheet"
  href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>

Now we also want to have a little ui toggle in our storybook, to switch to dark-mode with a single click. Therefore, we add storybook-dark-mode to our storybook config (.storybook/main.js), which should now look something like this:

const webpack = require('webpack');

module.exports = {
  stories: ['../**/*.stories.(js|mdx)'],
  addons: [
    'storybook-dark-mode/register',
  ],
  webpackFinal: async (baseConfig) => {
    baseConfig.module.rules.push({
      test: /\.po/,
      use: [
        {
          loader: '@lingui/loader',
        },
      ],
    });
    // merge whatever from nextConfig into the webpack config storybook will use
    return baseConfig;
  },
};

Afterwards we update our withMaterialUI decorator to make use of the new addon.

import { addDecorator } from '@storybook/react';
import CssBaseline from '@material-ui/core/CssBaseline';
import { useDarkMode } from 'storybook-dark-mode';

import ThemProvider from '../theme';

const withMaterialUI = storyFn => (
    <ThemeProvider darkMode={useDarkMode()}>
        <CssBaseline />
        {storyFn()}
    </ThemeProvider>
);

addDecorator(withMaterialUI);

We should now see a little moon icon in our storybook toolbar and should be able to switch between dark and light mode on the fly.

Alt Text

Language

OK. Let us do something similar for our i18n provider.

At first we added storybook-addon-i18n to our .storybook/main.js.
Then we modified the .storybook/preview.js, so we ended up with:

import { addParameters, addDecorator } from '@storybook/react';
import { withI18n } from 'storybook-addon-i18n';


import I18nProvider from '../components/I18nProvider';
import catalogEn from '../locale/en/messages.po';
import catalogDe from '../locale/de/messages.po';

const LocaleProvider = ({ locale, children }) => (
  <I18nProvider
    language={locale}
    catalogs={{
      en: catalogEn,
      de: catalogDe,
    }}
  >
      {children}
  </I18nProvider>
);

addParameters({
  i18n: {
    provider: LocaleProvider,
    supportedLocales: ['en', 'de'],
  },
});

addDecorator(withI18n);

You should now see a little world icon in the storybook toolbar and should be able to switch the language on the fly.

Alt Text

Act 5: Mocking Stuff

Let's finish with some mocks.

ReST APIs

We are using axios for all of our API calls. So we are going to mock axios with the axios-mock-adapter.

Add this to your preview.js:

import MockAdapater from 'axios-mock-adapter';
import api from '../api'; // our custom axios instance

const mockApi = new MockAdapter(api);

// mock all the routes you like, for example:
const baseURL = process.env.BASE_URL;
mockApi.onGet(`${baseURL}/members`).reply(200, {
    members: ["Joe", "Claire"]
});

Next.js

You may see some errors, when writing stories for components which using the next/router or next/link, thus we are mocking the router.

Write this into your preview.js:

import Router from 'next/router';
import { action } from '@storybook/addon-actions';

const actionWithPromise = e => {
  action("link clicked")(e);
  return new Promise((resolve) => resolve());
}

Router.router = {
  push: actionWithPromise,
  replace: actionWithPromise,
  prefetch: actionWithPromise,
};

Now every click on a link will dispatch an action.

Alt Text

After-Show

This was more of an experience report than a concrete tutorial on how to implement storybook with your custom nextjs site. Of course, if you have the exact same stack, you can use most of our configuration. But I hope that this helps you integrate your own custom stack.

If you have any questions, feel free to ask :) or if you have any improvements, feel free to point them out :D.

P.S.: All of our projects are open source, so you can go head and explore our repository https://gitlab.com/openpatch/web-frontend

OpenPatch

OpenPatch is an open source platform for assessment and training of competencies.

Discussion

markdown guide