1. Home
  2. Articles
  3. javascript

How to Effectively Set Up NextJS with a Component Library using Monorepos

JavaScript||

9 min read

A monorepo is a repository containing multiple related resources managed from an individual repository. Using NPM workspaces, we can build dynamic component libraries that numerous applications can consume, all retained in one version-controlled repository.

Introduction

Monorepo's have been gaining traction over the years. Having worked with them for over a year in production environments, I can say one thing: they beat git submodules!

A typical monorepo will contain multiple packages and projects that are closely related. We can work more efficiently using a monorepo in a way that allows us to move away from:

NPM supports workspaces, which effectively allows for a monorepo structure. You'll need to use NPM v7 and above to set up a workspace.

Recently, I embarked on building a suite of components for a NextJS project. I wanted to ensure that I could actively develop components (facilitated by Storybook) so that my NextJS projects could consume those components.

Historically, NextJS projects have always supported a components folder. However, NextJS restricts this to the application itself. I aim to treat my component library like any other 3rd-party library I'd want to import into my projects. An architecture of this manner is easily achievable using a mono repo.

Initialise Project Structure

Our first step is to set up a project structure supporting a monorepo. Create a folder where your monorepo can live:

1mkdir my-monorepo && cd my-monorepo

Great. Our next step is to create a package.json file. We can bootstrap one quickly by running the following:

1npm init -y

That should produce the following output:

1{
2 "name": "my-monorepo",
3 "version": "1.0.0",
4 "description": "",
5 "main": "index.js",
6 "scripts": {
7 "test": "echo \"Error: no test specified\" && exit 1"
8 },
9 "keywords": [],
10 "author": "",
11 "license": "ISC"
12}

Let's add some items to our package.json before installing dependencies. We'll need to add a workspaces key. Doing so informs NPM where our projects and packages sit.

NPM comes with a nifty command for adding workspaces; run the below commands and follow the instruction wizard that each command outputs:

1npm init -w ./packages/component-library
2npm init -w ./projects/monorepo-site

Executing the above will run npm install for you as well. At this point, we will see our first fundamental differences between a traditional repo and a monorepo:

A package.json exists in the root and within every "workspace." You may notice that there is only one occurrence of node_modules and package-lock.json. Now, all workspace dependencies get managed from the root folder.

Next up, we'll look into how to install workspace-specific dependencies.

Install dependencies

When installing package or project dependencies, we can still leverage npm install - however, this needs to be run from the root folder and requires that we pass a -w flag to the command. Let's run through installing NextJS.

1npm install --save next react react-dom -w monorepo-site

Run the command above from the root of your project folder, where node_modules and the package-lock.json can be found. The only thing to call out here is that the value passed to -w must match the name field of one of your project or packages package.json. In this example, the name field of projects/monorepo-site/package.json is monorepo-site.

If all was successful, you should see the dependencies added to projects/monorepo-site/package.json.

1"dependencies": {
2 "next": "^13.0.6",
3 "react": "^18.2.0",
4 "react-dom": "^18.2.0"
5}

Repeat this process for any necessary dependencies in packages/component-library as well.

Setup Components and StoryBook

Please note that this is not a tutorial on how to get set up with Storybook.

Let's take some steps to get Storybook set up correctly. We'll also create a dummy component to verify that our setup works. As StoryBook isn't a dependency of either packages or projects, we can install it in the root of our monorepo:

1npm install --save-dev @storybook/react @storybook/manager-webpack5 @storybook/builder-webpack5 @storybook/addon-postcss

Let's also add the build commands to the root monorepo package.json:

1"storybook": "start-storybook -p 6006",
2"build-storybook": "build-storybook"

Next, we'll need to create the .storybook folder; let's do this and add it to the root of the monorepo. You'll want to add two files to this folder:

  1. main.js
  2. preview.js

main.js is where we configure StoryBook's settings:

1module.exports = {
2 "stories": [
3 "../packages/component-library/**/*.stories.jsx",
4 ],
5 "addons": [ {
6 name: '@storybook/addon-postcss',
7 options: {
8 styleLoaderOptions: {},
9 cssLoaderOptions: {
10 modules: true,
11 sourceMap: true,
12 importLoaders: 1,
13 },
14 postcssLoaderOptions: {
15 implementation: require('postcss'),
16 },
17 },
18 }
19 ],
20 "framework": "@storybook/react",
21 "core": {
22 "builder": "@storybook/builder-webpack5"
23 }
24}

In our main.js file above - we tell StoryBook where to look for component stories. We also include the @storybook/addon-postcss plugin that supports PostCSS. The extra options you see listed above enabled PostCSS Modules. The last two keys: framework and core, allow us to configure StoryBook to use Webpack v5.

In preview.js, add the following:

1export const decorators = [
2 (Story, context) => {
3 return (
4 <div style={{
5 padding: '1rem',
6 display: 'flex',
7 justifyContent: 'center'
8 }}
9 >
10 <div>
11 <Story {...context} />
12 </div>
13 </div>
14 );
15 }
16];

The preview.js file allows us to customize the appearance of stories without affecting the component logic itself. Hence, the name "decorator". I've just added some basic styling here to center the button.

Next, we need to flesh out our Button component. In package/component-library, add a new folder called Button; in that folder, add the following files:

  1. Button.module.css - handles the styling of the button
  2. Button.stories.jsx - handles the button story
  3. Button.jsx - the button component itself

You can find the contents of these files in the repo. Great, we need to see if we can import our component into NextJS.

Setup a NextJS project

We need to return to NextJS quickly to tie everything together meaningfully. First, add a pages folder to the NextJS site and the scripts needed in package.json to build NextJS.

To add the scripts, go to projects/monorepo-site/package.json and replace the current scripts field with:

1"scripts": {
2 "dev": "next dev",
3 "build": "next build",
4 "start": "next start",
5 "lint": "next lint"
6}

In the pages folder, add an index.js file with the following contents:

1const Home = () => {
2 return (
3 <main>
4 <h1>Hello! This is a NextJS + StoryBook Monorepo</h1>
5 </main>
6 );
7}
8
9export default Home

Add the Component to NextJS

Now, it's time to import our component into NextJS. Let's import the button into projects/monorepo-site/pages/index.js:

1import Button from '../../../packages/component-library/Button/Button.jsx'

When you run npm run dev, you'll likely run into an error. NextJS does not transpile ES6 JavaScript from node_modules or any other local folder outside its reach.

To get around this problem, configure a property in next.config.js called transpileModules:

1export const nextConfig = { transpileModules: ['component-library'] }

Note that the workspace name, component-library, is passed, not the path to the folder on the disk. You'll need to restart your dev server.

Your custom Button component now lives in NextJS and StoryBook and can be iterated upon and tracked in the repo!

You may run into trouble with NextJS asking for React v18 if you use NextJS 13. This tutorial was written using NextJS v12 and React v17.0.2

Additional Scripts for Convenience

To run our build commands, we'd have to cd into every project or package. Doing so can be time-consuming, so let's add the following scripts to the package.json found in the root:

1"scripts": {
2 "build": "npm run build --workspaces --if-present",
3 "dev:monorepo-site": "npm run dev -w monorepo-site"
4}

Let's run through these two commands as they differ slightly. Running npm run build in the root will allow us to fire off individual build scripts in all workspaces. If the build task isn't in a package or project package.json, it will simply skip it.

If, at some point, this particular example scaled to have three production sites, all with a build command, we could run npm run build from the root, and for each workspace, the build task will execute. Pretty nifty!

npm run dev:monorepo-site is slightly different - this command targets one workspace and fires off the script defined within that workspace (as long as it exists). Doing so allows us to spin up the dev server for monorepo-site from the root without navigating to the folder.

Conclusion

Let's review where we're at:

At this point, we could easily add a new site project or extend our component library. We could also add other packages that may deal with custom server functionality, CMS integrations, or API support. The possibilities are endless. Now it's time for you to try!

Profile of Daine

Written by Daine Mawer. Thanks for reading! Im always posting new content. If you liked what you read, please subscribe to my RSS feed or follow me on Github, Twitter or LinkedIn. Im also always on the look out for new oppurtunities, engagements, contract work or just coffee! So please dont hesitate to reach out.