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 🍵