Collocation on the Frontend Web: Why It Matters and How I Do It in Next.js (or any frontend framework at all)

"Put things where they’re used." That’s the golden rule of collocation in frontend development, and once I embraced it fully, my Next.js projects became easier to scale, debug, and maintain.


TLDR;

Collocation makes it easier to find everything related to a feature, component or page in your code, whether you want to make an update or remove an entire feature. Your project’s features are more modular and reusable, and onboarding a new developer to your codebase becomes less stressful. 😉

What is Collocation?

Collocation in frontend web development simply means placing related files and logic close to each other, or in the same folder or context where they are actually used.

So instead of scattering styles in one folder, components in another, and tests in another, you keep everything related to a single feature or component tightly grouped.

Think of it like packing for a trip. You don’t keep your toothbrush in your garage and your passport in the kitchen. You group things by purpose. It’s the same idea here, just that we’re now “packing” our code ☺️.


Why Collocation Matters on the Frontend

In a modern frontend stack, especially with frameworks like Next.js, collocation is not just a pattern. It’s a best practice. Why?

  • Discoverability: Easier to find everything related to a component or page.

  • Cognitive simplicity: No need to mentally map files across the entire tree.

  • Testing is easier: Test files sit next to the components they test.

  • Scoped styles and logic: CSS Modules, hooks, and utils don’t leak across the app.

  • Encapsulation: Your features are modular and reusable.


A Typical Next.js Project Without Collocation

Here’s a simple (and painful) structure I’ve seen too many times:

/components  
  Navbar.tsx  
  Button.tsx

/styles  
  Navbar.module.css  
  Button.module.css

/pages  
  index.tsx  
  about.tsx

/tests  
  Navbar.test.tsx  
  Button.test.tsx

Now imagine maintaining this at scale. 🥲

With Collocation

Here’s how I typically structure my Next.js apps now using collocation:

my-next-app/
├── src/
│   ├── app/                 # Top-level route components
│   │   └── login/
│   │       ├── layout.tsx
│   │       └── page.tsx
│   │   └── assets/                # Global static assets (images, fonts)
│   │       ├── logo.svg                
│   │       └── favicon.ico
│   │   ├── layout.tsx
│   │   ├── loading.tsx
│   │   └── page.tsx
│   ├── components/            # Shared components
│   │   └── Button/
│   │       ├── Button.tsx
│   │       ├── Button.module.css
│   │       └── Button.test.tsx
│   ├── features/              # Feature-specific modules
│   │   └── auth/
│   │       ├── components/
│   │       │   ├── Login.tsx
│   │       │   ├── Login.module.css
│   │       │   └── Login.test.tsx
│   │       ├── hooks/
│   │       │   └── useAuth.ts
│   │       ├── services/
│   │       │   └── authService.ts
│   │       └── types.ts
│   ├── hooks/                 # Global reusable hooks
│   │   └── useMediaQuery.ts
│   ├── layouts/               # Layout components (e.g., MainLayout)
│   │   └── MainLayout.tsx
│   ├── types/                 # Global type declarations
│   │   └── index.d.ts
│   ├── utils/                 # Utility functions
│   │   └── formatDate.ts
│   └── middleware.ts          # Middleware component
├── .gitignore
├── package.json
├── tsconfig.json
├── next-env.d.ts
└── next.config.mjs

Let’s break it down:

  • Each route (like /login) has its own folder with its page component and supporting components in the features folder. Now, this is because I prefer not to have my component and logic files in the app folder. You can also decide to move you feature-specific folder to the feature/route folder in the app directory if you prefer them that close.

    Like this;

      /app/login/
        page.tsx
        layout.tsx
        Login.tsx
        Login.module.css
        Login.test.tsx
        useAuth.ts
    
  • Styles, tests, and local hooks sit right next to the components they belong to.

  • Shared UI components or layout wrappers go into components folder.

  • Shared utils go in utils.

It’s clean, it’s local and, it scales!

Bonus: A Vite App With Collocation

my-vite-app/
├── public/                    # Static assets
│   └── favicon.svg
├── src/
│   ├── assets/                # Global static assets (images, fonts)
│   │   └── logo.svg
│   ├── components/            # Shared components
│   │   └── Button/
│   │       ├── Button.tsx
│   │       ├── Button.module.css
│   │       └── Button.test.tsx
│   ├── features/              # Feature-specific modules
│   │   └── auth/
│   │       ├── components/
│   │       │   └── LoginForm.tsx
│   │       ├── hooks/
│   │       │   └── useAuth.ts
│   │       ├── services/
│   │       │   └── authService.ts
│   │       └── types.ts
│   ├── hooks/                 # Global reusable hooks
│   │   └── useMediaQuery.ts
│   ├── layouts/               # Layout components (e.g., MainLayout)
│   │   └── MainLayout.tsx
│   ├── pages/                 # Top-level route components
│   │   └── Home/
│   │       ├── Home.tsx
│   │       └── Home.module.css
│   ├── routes/                
│   │   └── index.tsx
│   ├── types/                 # Global type declarations
│   │   └── index.d.ts
│   ├── utils/                 # Utility functions
│   │   └── formatDate.ts
│   ├── App.tsx                # Root component
│   ├── main.tsx               # Vite entry point
│   └── vite-env.d.ts          
├── .gitignore
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

A Collocation Example in Code

Say I have a Hero section in my homepage. Here’s what that folder might look like:

Hero.tsx

import styles from './hero.module.css';
import { useHeroData } from './useHeroData';

export default function Hero() {
  const { title, subtitle } = useHeroData();
  return (
    <section className={styles.hero}>
      <h1>{title}</h1>
      <p>{subtitle}</p>
    </section>
  );
}

useHeroData.ts

export function useHeroData() {
  return {
    title: 'Welcome to My Site',
    subtitle: 'Frontend, Fast, and Beautiful',
  };
}

Everything lives in one place. If I ever need to update or remove the Hero, I know exactly what to touch.

Also, if I need to remove an entire feature, I know where to go and not leave any unused code behind.


Pro Tip: Collocation ≠ Coupling

A lot of people confuse collocation with tight coupling. Nope.

You can still keep components modular and testable while collocating. You're just being smarter at putting tools next to the task.

PS: Coupling speaks to how much one component or module depends on another in a project. For your components to be tightly coupled means that you have components that are heavily connected or dependent on one another and changing one often breaks the other.

I’ll be writing more about this later! 🙃


Summary

Collocation is like flossing; everyone agrees it’s good for you, but only a few people do it consistently. Once I made it a rule in my Next.js workflow, things got really smoother.

🥂 Fewer bugs.
👌 Fewer mental jumps.
💪 Better developer experience.

If you’re starting a new project, collocate from day one. If you're refactoring an old one, do it incrementally and I promise you won’t regret it!

3
Subscribe to my newsletter

Read articles from Gospel Chinyereugo directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gospel Chinyereugo
Gospel Chinyereugo