Lucas AllenGo to homepage

Colocating Architecture and NextJS

Colocation. If you haven't heard of this organizational strategy before, then you are in for a treat. It is called "colocation" because the architecture strives to keep everything (components, in our React/Next context) as close as possible to the page or parent component as possible. The smallest bits and pieces are... co-located with their consumers.

We will first go over this strategy in some detail, illustrating how it's done and how I tend to use it. Then I'll point out some of the obvious benefits and downsides before diving in to how it can be done within a NextJS application.

If you're already familiar with colocation then I encourage you to skim on down.

Colocation

We're going to use a basic React application structure to illustrate this method, but it could easily apply to other contexts. In abstract terms, all this architecture assumes is that your code base has files that consume other files. Consumer files and provider files, let's say, such that any given file may be both or either.

Consider a React application that has the following views: Sales, Accounts, Login, and Dashboard. These are fairly common views for any internal web application. Given some ability to alias the src path, we can begin to sketch the following directory tree:

- src/
| - Accounts/
| - Dashboard/
| - Login/
| - Sales/
... other config files or somesuch

Now for any Accounts exclusive components, we can put them under Accounts. The same for any pages, constants, functions, types, etc. The key is to let the providing file be colocated to its cosumer(s) as possible.

One thing we neglected above is the situation in which you have a component or some other providing file that is used in many of those contexts. A Button component is one such example. In these cases, I use a simple Utility folder to hold all generalized components.

Let's update the above structure.

...
- src/
| - Accounts/
  | - constants.ts
  | - components/
    | - AccountCard/
      | - constants.ts
      | - index.tsx
      | - types.ts
    - index.tsx
    - types.ts
  ...
  - Utility/
  | - services/
    | - logout.ts
    | - impersonate.ts
    - components
    | - Button/
      | - constants.ts
      | - index.tsx
      | - types.ts

This structure does a lot of work for us. If an engineering team ever has a new dev join them on this codebase, and they were asked to add some features for the Accounts module or update the AccountCard, the new developer would not have to run around the codebase following every grep'd keyword. Almost everything they need will be nearby, ready to grab from the shelf.

What if the AccountCard was to be removed? Simple. Remove it's folder. Any code that exclusively provided for the AccountCard is now removed, as well. Nothing unused remaining. And, as usual, anything that depended on the AccountCard will throw a build error or will get caught amongst your diligently written tests.

Likewise for an entire section. It's trivial thanks to this structure to remove an entire module from your web application.

Colocating your files according to their dependence on one another has made your codebase easy to understand and easy to maintain.

As far as I can tell as of this writing, the only major downside remaining is that Utility section. In this case, there may be utility providers that get left behind even after no other file is consuming their contents. Given what generally fits into a Utility file, that should be a rare case, but it does also mean the old way still remains though it is contained to the Utility blackbox.

Colocation in NextJS

Enabling the colocation architecture in NextJS is as simple as renaming your page files and updating the next.config.js file.

We need to add the pageExtensions attribute in the config. Updating this attribute tells NextJS which file extensions to look for when creating routes and when deciding which file is a page. In your NextJS config file:

// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ["page.ts", "page.tsx"],
  // This doesn't have to be page -- it can be "foo-buz-baz.ts" if you like
};

export default nextConfig;

Make sure to update all of your page files from [pageName].tsx to [pageName].page.tsx. Another gotcha: the _app.tsx and _document.tsx files also need to be updated so that you have _app.page.tsx and _document.page.tsx.

Normally your NextJS app structure looks like this:

...
- src/
| - constants/
| - components/
  | - ArticleCard/
  | - ArticleHeader/
| - lib/
| - pages/
  | - _app.tsx
  | - _document.tsx
  | - index.tsx
  | - articles/
    | - index.tsx
    | - [slug].tsx
| - types/

But after the above updates, we're free to colocalize the structure providing all of the benefits mentioned earlier.

...
- src/
| - components/
  | - ArticleCard (it may be used in other places in addition
                   to the home page)
  - lib/
  - pages/
  | - _app.page.tsx
  | - _document.page.tsx
  | - index.page.tsx
  | - types.ts
  | - articles/
    | - components/
      | - ArticleHeader/
        ...
    | - index.page.tsx
    | - [slug]/
      | - constants.ts
      | - index.page.tsx
      | - types.ts

That's it! You now have everything you need to adopt the colocation structure in your NextJS app. Happy coding 🍵