ProNextJS
    Professional Next.js Course
    Loading price
    30-Day Money-Back Guarantee
    lesson

    Storybook in a Turborepo Monorepo

    Jack HerringtonJack Herrington

    When it comes to using Storybook in a monorepo, there are a few different options. In this lesson, we'll look at three approaches for integrating Storybook into a Turborepo monorepo:

    1. Adding Storybook to a UI library package
    2. Adding Storybook to a Next.js app
    3. Creating a dedicated Storybook app

    We'll also learn how to run multiple Storybooks simultaneously using Turborepo.

    This lesson picks up with the Turborepo setup we created previously.

    Adding Storybook to the UI Library Package

    Let's begin by adding Storybook to our packages/ui directory, which will allow the UI library to have its own Storybook for showcasing its components.

    Navigate to the packages/ui directory in your terminal, the initialize Storybook:

    npx storybook@latest init
    

    When prompted for setup, select Vite to use as a builder.

    After installing, Storybook will automatically launch. Close the browser window and stop the process.

    Inside of the packages/ui directory, you'll find a storybook directory with the Storybook configuration. Remove the generated stories under src/stories.

    Create a new file src/button.stories.tsx to define stories for the button component.

    Inside the button.stories.tsx file, add the following code:

    // inside packages/ui/src/button.stories.tsx
    
    import type { Meta, StoryObj } from "@storybook/react";
    import { Button } from "./button";
    
    const meta = {
      title: "Example/Button",
      component: Button,
      parameters: {
        layout: "centered",
      },
      tags: ["autodocs"], // can be removed if not using autodocs
      argTypes: {
        appName: { control: "text" },
        children: { control: "text" },
        className: { control: "text" },
      },
    } satisfies Meta<typeof Button>;
    
    export default meta;
    type Story = StoryObj<typeof meta>;
    
    export const Primary: Story = {
      args: {
        appName: "Primary",
        children: "Primary",
      },
    };
    

    This code defines a story for the button component with a primary variant.

    Running pnpm storybook will launch Storybook, and we can see the button component's story:

    The Button Story

    Storybook also supports creating an MDX file like button.mdx to provide additional documentation:

    // inside packages/ui/src/button.mdx
    import { Canvas, Meta } from "@storybook/blocks";
    
    import * as ButtonStories from "./button.stories";
    
    <Meta of={ButtonStories} />
    
    # Button
    
    A Button helps you add interactivity to your site
    
    <Canvas of={ButtonStories.Primary} />
    

    Here's how the Button story looks with our custom documentation:

    Button story with custom docs

    Remember to turn off the autodocs option in the stories file to avoid collision with your own documentation.

    That's how you add Storybook to a UI library package in a Turborepo monorepo.

    Adding Storybook to a Next.js App

    Now, let's add Storybook to the Next.js app within our Turborepo.

    Navigate to the app directory at apps/main-site, then run the storybook init command.

    npx storybook@latest init
    

    The command will auto-detect the Next.js setup and initialize Storybook accordingly. Like before, we'll remove the generated stories directory to co-locate stories with components.

    Let's create a Counter component and its story.

    Here's code for the Counter:

    // inside apps/main-site/src/app/counter.tsx
    "use client";
    import { useState } from "react";
    
    export default function Counter() {
      const [count, setCount] = useState(1);
    
      return (
        <div className="black text-white">
          <p className="text-3xl">Count: {count}</p>
          <div className="flex gap-2">
            <button
              onClick={() => setCount(count + 1)}
              className="px-5 py-2 rounded-full bg-blue-800 text-white"
            >
              Increment
            </button>
            <button
              onClick={() => setCount(count - 1)}
              className="px-5 py-2 rounded-full bg-blue-800 text-white"
            >
              Decrement
            </button>
          </div>
        </div>
      );
    }
    

    And here's the corresponding story:

    // inside apps/main-site/src/app/counter.stories.tsx
    
    import type { Meta, StoryObj } from "@storybook/react";
    import Counter from "./Counter";
    
    const meta = {
      title: "Example/Counter",
      component: Counter,
      parameters: {
        layout: "centered",
      },
      // tags: ["autodocs"], 
      argTypes: {},
    } satisfies Meta<typeof Counter>;
    
    export default meta;
    type Story = StoryObj<typeof meta>;
    
    export const Primary: Story = {
      args: {},
    };
    

    Note that the autodocs option is commented out.

    Running pnpm storybook will launch Storybook, and we can see the Counter component story:

    the Counter story

    For now, the component isn't styled.

    The Storybook Styling Plugin

    Stop the Storybook process and install the styling plugin:

    npx storybook@latest add @storybook/addon-styling-webpack
    

    Now inside the apps/main-site/.storybook/preview.ts file, add in the global styles which will bring in Tailwind:

    // inside apps/main-site/.storybook/preview.ts
    import type { Preview } from "@storybook/react";
    import "../src/app/globals.css";
    
    ...
    

    Now when running Storybook, the Counter component will be styled with Tailwind CSS.

    React Server Components in Storybook

    Storybook 8 introduced support for React Server Components.

    To showcase an RSC in Storybook, create a new Pokemon.tsx file that will fetch and display a Pokemon:

    // inside apps/main-site/src/app/Pokemon.tsx
    import React from "react";
    
    export default async function Pokemon({ id }: { id: number }) {
      const pokemon = await fetch(
        `https://pokeapi.co/api/v2/pokemon/${id || 1}`
      ).then((res) => res.json());
    
      return (
        <div className="text-3xl">
          <h1>{pokemon.name}</h1>
          <img
            src={pokemon.sprites.other["official-artwork"].front_default}
            alt={pokemon.name}
          />
        </div>
      );
    }
    

    Then create a corresponding story file pokemon.stories.tsx:

    // inside apps/main-site/src/app/pokemon.stories.tsx
    import type { Meta, StoryObj } from "@storybook/react";
    import Pokemon from "./Pokemon";
    
    const meta = {
      title: "Example/Pokemon",
      component: Pokemon,
      parameters: {
        layout: "centered",
      },
      // tags: ["autodocs"],
      argTypes: {
        id: { control: "text" },
      },
    } satisfies Meta<typeof Pokemon>;
    
    export default meta;
    type Story = StoryObj<typeof meta>;
    
    export const Primary: Story = {
      args: {
        id: 1,
      },
    };
    

    In order to enable the RSC support, we need to add some config to the .storybook/main.ts file:

    // inside apps/main-site/.storybook/main.ts
    
    const config: StorybookConfig = {
      features: {
        experimentalRSC: true,
      },
      ...
    

    Now when running Storybook, you'll see the Pokemon component rendered with the fetched data. Changing the id prop in Storybook will fetch a different Pokemon:

    Pokemon display

    Note that server actions are not yet supported in Storybook's RSC integration, but they are planned for a future release.

    Creating a Dedicated Storybook App

    The last example we'll look at is creating a standalone Storybook app.

    Navigate to the apps directory, then create a new Storybook app using the Vite React template:

    # inside the apps directory
    pnpm create vite storybook --template react-ts
    

    After the install command finishes, we will remove the README and the gitignore from the generated Storybook app.

    Configuring the Standalone Storybook App

    There is some TypeScript configuration we need to do in the shared packages directory of the Turborepo app.

    Inside the packages/typescript-config directory, create a new file vite.json:

    {
      "extends": "./base.json",
      "compilerOptions": {
        "target": "ESNext",
        "useDefineForClassFields": true,
        "module": "ESNext",
        "lib": ["ESNext", "DOM"],
        "jsx": "react",
        "sourceMap": true,
        "resolveJsonModule": true,
        "noEmit": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noImplicitReturns": true
      },
      "exclude": ["node_modules"]
    }
    

    Now we can go back to the apps/storybook directory and update the tsconfig.json to just use the vite.json we just created:

    // inside apps/storybook/tsconfig.json
    
    {
      "extends": "@repo/typescript-config/vite.json",
      "include": ["src"]
    }
    

    We also need to add some configuration to the package.json file. First we'll add the @repo/ui dependency, then for the dev dependencies we'll add the ESLint and TypeScript configurations:

    // inside apps/storybook/package.json
      "dependencies": {
        "@repo/ui": "workspace:*",
        "react": "^18.2.0",
        "react-dom": "^18.2.0"
      },
      "devDependencies": {
        "@chromatic-com/storybook": "^1.3.1",
        "@repo/eslint-config": "workspace:*",
        "@repo/typescript-config": "workspace:*",
        ...
    

    Running the Standalone Storybook App

    With the configuration in place, we can run the standalone app:

    pnpm dev
    

    By default, a Vite app runs at http://localhost:5173 and shows a simple counter:

    the standalone app

    Since we know that Vite works, let's update it to bring up Storybook by default.

    To do this, we need to initialize Storybook:

    npx storybook@latest init
    

    Storybook will automatically recognize that this is a Vite application.

    Inside of .storybook/main.ts we can see that it is currently looking for stories under src. We can remove the stories directory and copy over the stories from the @repo/ui package.

    The change we need to make is to have the component be imported from the @repo/ui package:

    // inside apps/storybook/src/button.stories.tsx
    import type { Meta, StoryObj } from "@storybook/react";
    import { Button } from "@repo/ui/button";
    
    const meta = {
      title: "Example/Button",
      component: Button,
      parameters: {
        layout: "centered",
      },
      // tags: ["autodocs"],
      argTypes: {
        appName: { control: "text" },
        children: { control: "text" },
        className: { control: "text" },
      },
    } satisfies Meta<typeof Button>;
    
    export default meta;
    type Story = StoryObj<typeof meta>;
    
    export const Primary: Story = {
      args: {
        appName: "Primary",
        children: "Primary",
      },
    };
    

    Now when running pnpm dev, Storybook will launch with the button component from the @repo/ui package:

    the button component story

    Running Multiple Storybooks Simultaneously

    With Turborepo, you can run multiple Storybooks simultaneously.

    Inside the packages/ui/package.json file we can see that Storybook is set to run on port 6006:

    // inside packages/ui/package.json
    
    scripts: {
      ...
      "storybook": "storybook dev -p 6006",
    }
    

    Set the ports to 6007 and 6008 for the apps/main-site and apps/storybook respectively.

    Next, inside of the turbo.json file in the root of the project, we can add a storybook task definition. We'll also set the experimentalUi flag to true to enable the experimental UI feature:

    {
      "$schema": "https://turbo.build/schema.json",
      "globalDependencies": ["**/.env.*local"],
      "experimentalUI": true,
      "pipeline": {
        "build": {
          "dependsOn": ["^build"],
          "outputs": [".next/**", "!.next/cache/**"]
        },
        "lint": {
          "dependsOn": ["^lint"]
        },
        "dev": {
          "cache": false,
          "persistent": true
        },
        "storybook": {
          "interactive": true,
          "cache": false,
          "persistent": true
        }
      }
    }
    

    With these settings, we can run Storybook across multiple apps simultaneously by running the following command in the root of the project:

    pnpm turbo storybook
    

    The experimental UI allows you to navigate between different Storybooks and interact with their respective terminals:

    the experimental turbo ui

    Whether you choose to add Storybook to individual apps, UI libraries, or create a centralized Storybook app, Turborepo makes it easy to manage and run multiple Storybooks simultaneously.

    Transcript

    Using a storybook in a monorepo context can be a little bit interesting because where you're going to put your storybook is an interesting choice. You can associate it with an app. If an app wants to have its own storybook, you can associate it with say the packages UI, the UI library perhaps in your monorepo, it might have its own storybook or you might choose to have an overall storybook that takes elements from all over the monorepo and has all of those in its storybook. So I'm going to show you all three of those scenarios as we build out the storybook on Turbo Repo. We've actually built this off of the Next.js on Turbo Repo.

    So you're going to need to start there. But let's go take a look at how to augment the Next.js on TurboDepot with Storybook. I'm gonna start off by adding Storybook to our packages UI directory on the assumption that more than likely you're gonna wanna have your UI library have a storybook of all of the components that are in that library. So we'll start off with our terminal. We'll go in our packages UI, And then we'll run storybook init.

    So because packages UI is a package and not an app, we actually don't have Vite or Webpack installed in that package. So we're going to use Vite for the storybook here. Now it automatically launches Storybook for us. I'm just going to close this and we're going to go back into our VS Code and stop it. Now let's go see what it did to our packages UI directory.

    So it created a Storybook directory that's got our configuration in it for storybook. Also created under source stories, we're gonna get rid of those and we're gonna create a story for button. So under source, we're gonna create a new file called button.stories.tsx. In that file, we are going to define the stories for our button. So in this case, we are going to title it, example button.

    We're going to bring in that component button and specify it as the component that we are going to write our story about. Of course, that file is located right next door because we want to co-locate our stories. And then down here, we're going to have our primary story about our button. So let's hit save and then run the storybook again. The storybook install has automatically added a storybook script, so let's just run it.

    There we go. Now we've got our primary button and it has automatically generated documentation. You can even change the children dynamically. Really nice. Another option for documentation is to create an MDX file associated with that.

    So let's go back over to our source. We'll do button.mdx. In there we'll create some specialty formatted markdown. We're going to import the button stories that we just created and then use the canvas to go and display them. So you get to decide how you want to do that.

    Let's hit save, see what happens. Now we get an issue because the stories are tagged as auto-doc. So let's go back into our stories, turn off auto-doc. Auto-docs is the option to automatically have Storybook generate documentation for us. We're adding our own documentation, so they're basically colliding.

    So let's hit Save. And there we go. There is our documentation that we just created, as well as the story that we can play around with. But let's say that we want to go and add Storybook to our app. So how do we go and add Storybook to our Next.js application inside of our Turbo Reboot?

    Close that out and go back over to our app, close out the windows, clear out our console. Now let's go back to our apps and into our main site. And then inside of our main site, again, we're going to do that storybook init. This time it's auto detected that we are in a next application. Once again, it's helpfully launching the storybook for us.

    We really don't want that, so we can get rid of it, and then we will close that out. Let's go back into our app and see what happened. So in our main site, We have created a .storybook. That's fine with our configurations. It's also created stories.

    Again, I like to co-locate my stories. So I'm going to just remove that directory wholesale. Now we have to have a component that we actually want to write a story about. So let's go create a counter. This is just like the counter that we've seen in some previous examples.

    We're adding some nice tailwind to make it look good. Let's hit save. You can bring that into the page and then use it in our app. But really what we want to do is write a story about it. So let's go and create a story for it.

    So we'll create a story right next door. When we bring in the counter, we say that that's the component. We remove auto docs. We don't want those generated automatically. And we only have the one story, which is the primary.

    So let's hit save and let's try it out. So back in the main site, I am going to run storybook. And now we have our counter. Of course it's not styled, so let's see if we can get the tailwind working in there. To do that we're gonna go back into VS Code.

    So we use the Storybook command to add the styling plugin. And then over in our preview, we're going to add our globals. That's going to bring in Tailwind. Let's try it again. Now we've got some black text on a black background.

    Let's go and change that to white. And yeah, that looks really good. Nice, got our tailwind styling. Excellent. So one of the really cool things that came out with Storybook 8 is the ability to show React server components in your Storybook.

    Let's give that a try. So I'll get rid of these and I'll create a new file called Pokemon.tsx. It's going to be an asynchronous React server component that goes and gets a Pokemon from the Pokemon API and then displays its official artwork and its name. Let's hit save. And now we want to create a story about it.

    So to do that, we create a new file called pokemon.stories.tsx. And in that file, we import the Pokemon, set that as the component, put it in the example directory, and that's it. We create a story for it. Pretty easy. All right.

    Let's hit save and see how it goes. All right. So our counter looks good. Let's go over to our Pokemon, and we got a problem. Okay.

    So what we need to do is enable the experimental RSC support. To do that we go over in our main, under features, we enable experimental RSC. Hit save, try it again, and now when I go to Pokemon we get Bulbasaur. And I can actually change the ID to 50 and that is Diglett. And now I can change that ID to say 2, we get Ivysaur as opposed to Bulbasaur.

    That's actually going off and hitting that endpoint, getting the data back, rendering it on the server, and then showing it to us. Now, there are limitations. Unfortunately, this does not actually support server actions yet, so we can't really use this in the Lego concept that we're going to talk about towards the end of this tutorial. But otherwise I think this experimental RSC support is actually really cool. Now one more option when it comes to Storybook inside of a monorepo is to have a dedicated Storybook app.

    So let's go try that out. So back in our application, I'm going to go back up into the apps directory, and then I'm going to create our storybook application. Now for that, I'm actually going to use Vite and its React template. Why? Because we really just need a React host app to host the storybook.

    So Vite is actually probably the easiest way to do that. So I'm going to use create-vite to create the storybook application. We're going to call it storybook and we're going to use the React TypeScript template. Now go into storybook And let's take a look at what you have. You can get rid of the readme, get rid of the gitignore.

    Now I'm just going to leave the ESLint configuration as is, but I do want to fix the TypeScript configuration. So what we need to do is go over in our packages and then add a TypeScript configuration for Vite applications. Call that Vite.json. This configuration file is of course in the GitHub repo associated with this video, as well as in the instructions. So now that we have our Vite configuration for TypeScript, we can go back into our app and use it.

    We'll move the TSConfig for node and set its work for the TSConfig.json. And all we're gonna do there is just use that TypeScript config. Of course, we have to actually add the TypeScript configuration package to our package JSON. So let's go and do that. So we'll first add the UI library that we're going to show.

    And then as the dev dependencies, we'll go and add the repo for the ESLint config and the TypeScript config. And there we go. Now we should be able to run it. So Vita applications are by default over on localhost 5173. Let's go take a look.

    All right, seems to be working. Okay. Now, let's actually go and use that button from the repo. So go over here to our source and app.tsx and replace that with just a button from repo button and see. Hey, wow, cool.

    All right, so we got a Vite application working. So now let's turn that into a host for a storybook. To do that, I'm going to run storybook init. It's going to auto detect this as a Vite application. And it's going to bring up a storybook by default.

    Don't really need that. So now let's go into our storybook configuration and see where it's looking for stories. So it's currently looking for stories under source. That's good. So any stories that appear in source are going to work.

    That's great. So I'm just going to remove the stories directory. Then I'm going to copy the story from the packages that we already created a while back. I just grab the button stories, pop it into our source. But instead of getting button locally, I'm going to get it from the repo.

    Let's give it a try. PMBM storybook. And there we go. Now we have our primary button, we have a story about it, and we're importing that from that repo. So the centralized storybook application can be used to bring in stories from a bunch of different packages and show the entire design system.

    Let me show off a little bit about TurboRepo. So we can actually run all of these storybooks simultaneously. So if we take a look at the package JSONs, so the package JSON here is running storybook in dev mode on 606, that's fine. Let's go and change the storybook in the storybook app to say 607, and the main site to port 608. Cool.

    Now let's go back to our TurboJot JSON. We'll create a definition for a storybook task. We'll say that it is not cached and is persistent. We'll also say that it is interactive. Hit true, and we'll enable experimental UI.

    At the time of this production, this is a new feature that came out with Turbo Repo. It actually gives you a much nicer look and feel when you're actually running the same task across multiple applications. This case means we're gonna be running that storybook across those multiple applications. Let's go ahead and try it. So in my storybook on Turbo Repo inside of my terminal.

    I'm going to run turbo and then storybook. That's going to run that storybook script on any package or app that has a storybook script. Let's go to try. We can actually jump up or down and see all of the different storybooks in action. And if we want to actually interact with any of them we can hit return and then type in to those terminals.

    That's a fantastic addition to Turbo Repo. There you go three different ways to set up a storybook inside of a Turbo Repo Monorepo.