Company Logo

Bisht Bytes

Monorepo alternative to sharing routes for NextJs 14 in 2024

Published On: 28 Jul 2024
Reading Time: 11 minutes

Overview


The idea of this article is to show how you can achieve code sharing at page level without setting up a monorepo using Nextjs 14 colocation capability. We will be relying on Git Submodule to create a sharable code with one or more routes along with their nested routes.

Note, this solution is to share code at page level and not sharing individual components, although this solution can be extended to do that as well based on what you keep as part of the Git Submodule.

Premise

With NextJs 14 its now possible to colocate your route code within your route directory. This code can be:

  • page route files
  • api route files
  • components files
  • styles files
  • utility lib files
  • provider and context files
  • other files used by the route

On top of this, your nested routes also can be colocated.

So now your route and its nested routes can be a standalone codebase. And this allows us to very easily share these routes with another project which want to have their routes as well.

Issues

To be honest, making standalone codebase is a choice that needs to be made at the inception of the project which rarely happens. So most likely you have codebase which has dependencies from across the directories in the project.

But these issues can still be addressed after a bit of refactoring, to make code sharing possible.

Refactoring

To refactor the code, we have to make use of the following:

  • NextJs 14 colocation (for the route being shared)
  • Environment Variables
  • Path aliases
  • Dynamic Imports
  • Server Actions
  • Route Groups

We will go into details of each of the above.

Scenario

Lets say we have the following structure in our two projects, which will share one of the page and its nested pages.

You could have routes with or without route groups. But since we want to colocate the code for a given route, we will rely on route groups to create segregation for code to be shared. (This is not necessary as you could still point to code outside if you want, but you will have to know what are the external dependencies of route being shared and manage this for each project using this shared codebase).

project1 
├── app
│   ├── page1
│   ├── page2
│   ├── page3
│   │   ├── path-a
│   │   └── path-b
│   ├── layout.tsx
│   └── page.tsx
├── public
│   ├── images
│   └── assets
├── node_modules
├── package.json
├── package-lock.json
└── .gitignore


project2 
├── app
│   ├── page1
│   ├── page2
│   ├── <insert-page3-here>
│   ├── layout.tsx
│   └── page.tsx
├── public
│   ├── images
│   └── assets
├── node_modules
├── package.json
├── package-lock.json
└── .gitignore

To share the page3 code from project1 to project2, we need to make sure that we colocate all the code needed by page3 which will include:

  • root layout
  • authentication
  • global state
  • utility functions
  • components
  • etc

Solution

Now coming to out solution, we want to make the route as less dependent (or completely independent) of the code outside the route we want to keep sharable.

With Route Group (Recommended)

Now before moving the pages create a seperate route group if not one already. This allows you to group more than one page to be part of the Git Submodule and can easily be pulled during the build processes.

If you dont want to do this, you will have to write scripts that creates the right directory structure after Git Submodule has been pulled during the build.

project1 
├── app
│   ├── page1
│   ├── page2
│   ├── (common-code)
│   │   └── page3
│   │       ├── path-a
│   │       ├── path-b
│   │       ├── layout.tsx (create seperate for route group)
│   │       └── page.tsx (create seperate for route group)
│   ├── layout.tsx
│   └── page.tsx
├── public
│   ├── images
│   └── assets
├── node_modules
├── package.json
├── package-lock.json
└── .gitignore

Here (common-code) is the directory that we will make the Git Submodule of. This will allow us to share more than one page. Also, we can directly pull the Git Submodule and the routes in the desired location at one go (see how at Working with Git Submodule).

Without Route Group (Optional)

You could achieve code sharing without route groups as well. But this has challenges with how git submodule will be initialized. This is due to the fact that once a git submodule is pulled, it will wrap all the code within under one folder. This means if you share more than one page, you will have to write a script to fix the directory hierarchy.

Path Alias

Using path alias allows the page being shared nested at different level possible. Also it eases the refactoring over time as well.

So although not absolutely necessary, you can use path aliases across all the files present within route code being shared.

Using code from outside route directory

You could have few dependencies that are imported from outside the route codebase. For example, page3 route can have the layout using @/component/Layout which is outside @/app/page3/layout.tsx.

But if your route code relies on code outside route, it needs to:

  • either have this dependency code in both project. that means both project need to maintain @/component/Layout for the both the projects
  • or if a given dependency is applicable to only one project, you could use dynamic import import('@/component/Layout')

Environment Variables

This brings up to another major consideration, which is to cater to differences between apps when they share common route code.

  • To address the differences, we will rely on a environment variable process.env.APP_NAME.
  • And more environment variables could also be used to create different setting, values, references, etc for eacg project using the common page route.

Server Actions

Server actions if using backend api's directly without relying on route could turn very helpful in what we aim to achieve here. If this is not the case with you, you might have to replicate the api route handlers as well.

Git Submodule

Make sure the pages (eg. page3 here) being shared are refactored such that:

  • page3 has imports only from within routes being made common between projects.
  • imports outside this directory if present within page3 route, needs to be present within all projects using the common code.
  • use dynamic imports if you need import a dependency, only in a certain project based on environment variable process.env.APP_NAME.
  • also you can create different branches of code for each project based on environment variable process.env.APP_NAME.
function getLoginData() {
  if (process.env.APP_NAME === 'project1') {
    getProject1LoginData()
  }
  if (process.env.APP_NAME === 'project2') {
    getProject2LoginData
  }
  return null
}

Once refactoring is done:

  • create a Git Submodule with this code
  • remove these routes from the original project
  • add the Git Submodule to the project

Working with Git Submodule

To add git submodule, you have to run the following command:

git submodule add <repo-url> "app/(common-code)"

Once you have added the git submodule, it will generate two artifacts:

  • .gitmodules file
  • directory where git submodule code will be initialized
[submodule "app/(common-code)"]
	path = app/(common-code)
	url = <repo-utl>

You will have to learn to work with Git Submodules though. I have couple of articles about the same added to the references.

References


Page Views: -