Skip to content

Publishing Storybook - One main Storybook instance using Storybook Composition

This guide extends the Using Storybook in a Nx workspace - Best practices guide. In that guide, we discussed the best practices of using Storybook in a Nx workspace. We explained the main concepts and the mental model of how to best set up Storybook. In this guide, we are going to see how to put that into practice, by looking at a real-world example. We are going to see how you can publish one single Storybook for your workspace, even you are using multiple frameworks, taking advantage of Storybook Composition.

In this case, we are dealing with a Nx workspace that uses multiple frameworks. Essentially, you would need to have one Storybook host for each of the frameworks, containing all the stories of that specific framework, since the Storybook builder can not handle multiple frameworks simultaneously.

However, there is still the option to combine all the hosts into one single Storybook instance, using Storybook composition.

Let's assume that you have a structure like the one described in the previous example, and your client app and the client libs are written in Angular, and the admin app the admin libs are written in React.

First of all, you have to create two Storybook host apps, one for Angular and one for React. Let's call them storybook-host-angular and storybook-host-react, which are configured to import all the Angular stories and all the React stories accordingly.

Now, we are going to combine the two Storybook host apps into one, using Storybook composition. You can read our Storybook Composition guide for a detailed explanation for how Storybook Composition works. In a nutshell, you can have one "host" Storybook instance running, where you can link other running Storybook instances.

We are going to assume that you are at the state where you already have your storybook-host-angular and storybook-host-react set up and ready to go.

It does not matter which framework you use for the host Storybook library. It can be any framework really, and it does not have to be one of the frameworks that are used in the hosted apps. The only thing that is important is for this host library to have at least one story. This is important, or else Storybook will not load. The one story can be a component, for example, which would work like a title for the application, or any other introduction to your Storybook you see fit.

So, let's use React for the Storybook Composition host library:

nx g @nx/react:lib libs/storybook-host --bundler=none --unitTestRunner=none

Now that your library is generated, you can write your intro in the generated component (you can also do this later, it does not matter).

Generate Storybook configuration for the host library

Section titled “Generate Storybook configuration for the host library”

Since you do need a story for your host Storybook, you should use the React storybook configuration generator, and actually choose to generate stories (not an e2e project though):

nx g @nx/react:storybook-configuration storybook-host --interactionTests=true --generateStories=true

Change the Storybook port in the hosted apps

Section titled “Change the Storybook port in the hosted apps”

It's important to change the Storybook ports in the storybook-host-angular and storybook-host-react projects. This is because the Storybook Composition host is going to be looking at these ports to find which Storybooks to host, and which Storybook goes where.

Update the project.json file of each library to set the port option to 4401 and 4402 accordingly:

libs/storybook-host-angular/project.json
{
// ...
"targets": {
// ...
"storybook": {
"options": {
"port": 4401
}
}
}
}
libs/storybook-host-react/project.json
{
// ...
"targets": {
// ...
"storybook": {
"options": {
"port": 4402
}
}
}
}

Add the refs to the main.ts of the host library

Section titled “Add the refs to the main.ts of the host library”

Update the libs/storybook-host/.storybook/main.ts file as shown below:

libs/storybook-host/.storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { mergeConfig } from 'vite';
const config: StorybookConfig = {
stories: ['../src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: {
name: '@storybook/react-vite',
options: {},
},
refs: {
'my-angular-app': {
title: 'My Angular App',
url: 'http://localhost:4401',
},
'my-react-app': {
title: 'My React App',
url: 'http://localhost:4402',
},
},
viteFinal: async (config) =>
mergeConfig(config, {
plugins: [nxViteTsPaths()],
}),
};
export default config;

Now, you can serve your host Storybook, as well as the hosted, composed Storybooks. You need to serve the composed Storybooks first, and then your host.

nx run storybook-host-angular:storybook

and on a different terminal:

nx run storybook-host-react:storybook

and then on a different terminal serve the host:

nx run storybook-host:storybook

You should now be able to see your host Storybook on your default port (usually 4400) and you should also be able to see your composed Storybooks in the sidebar.

You can now build and deploy the Storybook host.

You can build the Storybook with:

nx run storybook-host:build-storybook

Here's the catch. You will need to build and deploy the composed Storybooks independently as well, and make sure that they are available on the URLs that you have configured in your host Storybook's main.ts. In our example above, the host Storybook would be looking for the composed Storybooks on http://localhost:4401 and http://localhost:4402. So you need to build and deploy:

  • storybook-host-angular on some URL, let's say https://my-angular-stories.my-company.com/
  • storybook-host-react on some URL, let's say https://my-react-stories.my-company.com/

and then change the host Storybook's main.ts file to point to these URLs accordingly:

libs/storybook-host/.storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { mergeConfig } from 'vite';
const config: StorybookConfig = {
stories: ['../src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: {
name: '@storybook/react-vite',
options: {},
},
refs: {
'my-angular-app': {
title: 'My Angular App',
url: 'https://my-angular-stories.my-company.com/',
},
'my-react-app': {
title: 'My React App',
url: 'https://my-react-stories.my-company.com/',
},
},
viteFinal: async (config) =>
mergeConfig(config, {
plugins: [nxViteTsPaths()],
}),
};
export default config;

As you can see, the Storybook Composition solution would work for a small number of composed apps, but it can become quite cumbersome to manage if you have too many apps that you want to compose into your host Storybook.

  • Workspaces using multiple frameworks
  • Workspaces that have multiple Storybooks that they want to view in one single interface
  • Workspaces that use component libraries in multiple frameworks and want to showcase all their components in one single Storybook instance