Screaming ArchitectureEvolution of a React folder structure and why to group by features right away

Johannes KettmannPublished on 

React folder structures have been debated for years due to React's unopinionated approach, leading developers to ask, "Where should I put my files? How should I organize my code?"

I've researched the most popular approaches to organizing React projects:

  • Grouping by file type like components, contexts, and hooks
  • Grouping by pages with global folders for contexts, hooks, etc
  • Grouping by pages with colocation of related components, contexts, and hooks
  • Grouping by features

This post explores how these folder structures evolve in a growing codebase, the problems they can cause, best practices, and a challenge to turn a design from the React Job Simulator into a feature-based folder structure.

For an example project with the final feature-based folder structure, check out this GitHub repository.

We'll focus on the big picture, not every detail. To juice this story up and illustrate these concepts we'll follow the (slightly satiric) journey of an imaginary startup building the next big thing (a todo app).

Prototype: Group by file types

Our startup has a grand vision: Disruption, conquering the world, you know the drill. But we start small. For our first version, a simple list of todo items should suffice.

According to the React docs we shouldn’t spend more than 5 minutes deciding on a folder structure. And the simplest solution in the docs is the "group files by their types" approach. Components go in the components folder, hooks in the hooks folder, and contexts in the contexts folder. We create a folder per component containing styles, tests, and more.

└── src/
├── components/
│ │ # I'm omitting the files inside most folders for readability
│ ├── button/
│ ├── card/
│ ├── checkbox/
│ ├── footer/
│ ├── header/
│ ├── todo-item/
│ └── todo-list/
│ ├── todo-list.component.js
│ └── todo-list.test.js
├── contexts/
│ │ # no idea what this does but I couldn't leave this folder empty
│ └── todo-list.context.js
└── hooks/
│ # again no idea what this does but I couldn't leave this folder empty
└── use-todo-list.js

This is a simple, uncomplicated way for beginners to start. However, it won't stay this simple for long.

Can you do me a favor: Why are you interested in React folder structures?

The results are not shared with a third party. They are only tracked anonymously with Plausible.io.

Investment: More files → nesting

We need to impress investors with new features, so we decide to support editing of todo items. We add a form to edit todos and a modal to display the form.

└── src/
├── components/
│ ├── button/
│ ├── card/
│ ├── checkbox/
│ │ # this modal shows a form to edit a todo item
│ ├── edit-todo-modal/
│ ├── footer/
│ ├── header/
│ ├── modal/
│ ├── text-field/
│ │ # here is the form that is shown by the modal
│ ├── todo-form/
│ ├── todo-item/
│ │ # the edit modal is shown on top of the todo list
│ └── todo-list/
│ ├── todo-list.component.js
│ └── todo-list.test.js
├── contexts/
│ ├── modal.context.js
│ └── todo-list.context.js
└── hooks/
├── use-modal.js
├── use-todo-form.js
└── use-todo-list.js

The components folder is getting crowded. We try grouping and colocating components:

└── src/
├── components/
│ ├── edit-todo-modal/
│ │ ├── edit-todo-modal.component.js
│ │ ├── edit-todo-modal.test.js
│ │ │ # colocate -> todo-form is only used by edit-todo-modal
│ │ ├── todo-form.component.js
│ │ └── todo-form.test.js
│ ├── todo-list/
│ │ │ # colocate -> todo-item is only used by todo-list
│ │ ├── todo-item.component.js
│ │ ├── todo-list.component.js
│ │ └── todo-list.test.js
│ │ # group simple ui components in one folder
│ └── ui/
│ ├── button/
│ ├── card/
│ ├── checkbox/
│ ├── footer/
│ ├── header/
│ ├── modal/
│ └── text-field/
├── contexts/
│ ├── modal.context.js
│ └── todo-list.context.js
└── hooks/
├── use-modal.js
├── use-todo-form.js
└── use-todo-list.js

This structure provides a better overview by colocating child components with parents and grouping generic UI components in the ui folder.

The cleaner structure becomes apparent when we collapse the folders:

└── src/
├── components/
│ ├── edit-todo-modal/
│ ├── todo-list/
│ └── ui/
├── contexts/
└── hooks/

Growth: We need pages

Our startup continues to grow. We launched the app to the public and have a handful of users. Of course, they start complaining right away. Most importantly:

Our users want to create their own todo items!

So we add a second page for creating todos via a form. Luckily we can reuse the existing edit todo form. That’s amazing because it saves precious resources of our developer team.

We also need user authentication and to move the shared todo form to the components folder again.

└── src/
├── components/
│ │ # we now have multiple pages
│ ├── create-todo-page/
│ ├── edit-todo-modal/
│ ├── login-page/
│ │ # this is where the todo-list is now shown
│ ├── home-page/
│ ├── signup-page/
│ │ # the form is now shared between create page and edit modal
│ ├── todo-form/
│ ├── todo-list/
│ │ ├── todo-item.component.js
│ │ ├── todo-list.component.js
│ │ └── todo-list.test.js
│ └── ui/
├── contexts/
│ ├── modal.context.js
│ └── todo-list.context.js
└── hooks/
│ # handles the authorization
├── use-auth.js
├── use-modal.js
├── use-todo-form.js
└── use-todo-list.js

What do you think about the folder structure now? There are a few issues.

First, the components folder is getting crowded, but we may not be able to avoid this while keeping the structure flat. So let's disregard this problem.

Second (and more crucial), the components folder contains a mix of different components:

  • pages (entry points to the app, important for new devs)
  • complex components with potential side effects (e.g., forms)
  • simple UI components like a button.

The solution: Create a separate pages folder. Move all page components and their children there. Keep only components used on multiple pages in the components folder.

└── src/
├── components/
│ │ # the form is shown on the home and create todo page
│ ├── todo-form/
│ │ # we could also ungroup this folder to make the components folder flat
│ └── ui/
├── contexts/
│ ├── modal.context.js
│ └── todo-list.context.js
├── hooks/
│ ├── use-auth.js
│ ├── use-modal.js
│ ├── use-todo-form.js
│ └── use-todo-list.js
└── pages/
├── create-todo/
├── home/
│ ├── home-page.js
│ │ # colocate -> the edit modal is only used on the home page
│ ├── edit-todo-modal/
│ └── todo-list/
│ ├── todo-item.component.js
│ ├── todo-list.component.js
│ └── todo-list.test.js
├── login/
│ # don't forget the legal stuff :)
├── privacy/
├── signup/
└── terms/

This cleaner structure helps new developers identify all the pages and provides an entry point to investigate the codebase or debug the application. Many developers use a similar structure, like Tania Rascia and Max Rozen.

But our startup aims to conquer the world, so we can't just stop here.

You don't feel "job-ready" yet?
Working on a full-scale production React app is so different from personal projects. Especially without professional experience.
Believe me! I've been there. That's why I created a program that exposes you to
  • a production-grade code base
  • realistic tasks & workflows
  • high-end tooling setup
  • professional designs.

World Domination: Colocation

Our todo app is now a top player, boasting a 5-star rating. As our team and codebase grow, we face some challenges.

└── src/
├── components/
├── contexts/
│ ├── modal.context.js
│ ├── ... # imagine more contexts here
│ └── todo-list.context.js
├── hooks/
│ ├── use-auth.js
│ ├── use-modal.js
│ ├── ... # imagine more hooks here
│ ├── use-todo-form.js
│ └── use-todo-list.js
└── pages/

The global hooks and contexts folders become crowded, and complex components' code scatters across multiple folders, making it harder to track dependencies.

Our solution: colocation! We move contexts and hooks next to their components whenever possible.

└── src/
├── components/
│ ├── todo-form/
│ └── ui/
├── hooks/
│ │ # not much left in the global hooks folder
│ └── use-auth.js
└── pages/
├── create-todo/
├── home/
│ ├── home-page.js
│ ├── edit-todo-modal/
│ └── todo-list/
│ ├── todo-item.component.js
│ ├── todo-list.component.js
│ ├── todo-list.context.js
│ ├── todo-list.test.js
│ │ # colocate -> this hook is only used by the todo-list component
│ └── use-todo-list.js
├── login/
├── privacy/
├── signup/
└── terms/

We eliminate the global contexts folder, leaving only the global hooks folder with use-auth. This structure lets us grasp all files belonging to a feature at once.

However, there are still issues:

  1. The "todo" entity code spreads across multiple folders.
  2. It's unclear that the todo-list component lives in the home folder.
└── src/
├── components/
├── hooks/
└── pages/
├── create-todo/
├── home/
├── login/
├── privacy/
├── signup/
└── terms/

Exit: Group by Features

As we sell our billion-dollar startup, our users demand new features. They want separate projects for their todo items (like work and grocery list). We add a "project" entity and make changes to our pages and components.

└── src/
├── components/
│ ├── todo-form/
│ │ # is now shared between home and project page
│ ├── todo-list/
│ │ ├── todo-item.component.js
│ │ ├── todo-list.component.js
│ │ ├── todo-list.context.js
│ │ ├── todo-list.test.js
│ │ └── use-todo-list.js
│ └── ui/
└── pages/
├── create-project/
├── create-todo/
│ # shows now a list of projects and an overview of all todos
├── home/
│ ├── index.js
│ ├── edit-todo-modal/
│ └── project-list/
├── login/
├── privacy/
│ # shows a list of todos belonging to a project
├── project/
├── signup/
└── terms/

This still looks quite clean, but there are issues:

  • In the pages folder, it's not clear that this app has todos, projects, and users. Our brain first needs to process folder names like like create-todo (todo entity) or login (user entity) and separate them from the unimportant stuff (e.g. privacy and terms).
  • It feels arbitrary to place components in the shared components folder based on their usage. You need to know where and how many times a component is used to find it.

Let's adjust the folder structure and group files by feature.

"Feature" is a broad term. In this case, we'll use entities (todo, project, user) and a ui folder for components like buttons, form fields, and so on.

└── src/
├── features/
│ │ # the todo "feature" contains everything related to todos
│ ├── todos/
│ │ │ # this is used to export the relevant modules aka the public API (more on that in a bit)
│ │ ├── index.js
│ │ ├── create-todo-form/
│ │ ├── edit-todo-modal/
│ │ ├── todo-form/
│ │ └── todo-list/
│ │ │ # the public API of the component (exports the todo-list component and hook)
│ │ ├── index.js
│ │ ├── todo-item.component.js
│ │ ├── todo-list.component.js
│ │ ├── todo-list.context.js
│ │ ├── todo-list.test.js
│ │ └── use-todo-list.js
│ ├── projects/
│ │ ├── index.js
│ │ ├── create-project-form/
│ │ └── project-list/
│ ├── ui/
│ │ ├── index.js
│ │ ├── button/
│ │ ├── card/
│ │ ├── checkbox/
│ │ ├── header/
│ │ ├── footer/
│ │ ├── modal/
│ │ └── text-field/
│ └── users/
│ ├── index.js
│ ├── login/
│ ├── signup/
│ └── use-auth.js
└── pages/
│ # all that's left in the pages folder are simple JS files
│ # each file represents a page (like Next.js)
├── create-project.js
├── create-todo.js
├── index.js
├── login.js
├── privacy.js
├── project.js
├── signup.js
└── terms.js

We introduce index.js files to each folder as "barrel files" or "the public API" of a module or a component. This new "group by features" folder structure addresses previous issues.

Discussion: Feature-Driven Folder Structure and Screaming Architecture

In his article Screaming Architecture, Bob Martin says architectures should tell readers about the system, not the frameworks used. Our initial folder structure grouped files by type:

└── src/
├── components/
├── contexts/
└── hooks/

This screams, "I'm a React app." In contrast, our final feature-driven folder structure:

└── src/
├── features/
│ ├── todos/
│ ├── projects/
│ ├── ui/
│ └── users/
└── pages/
├── create-project.js
├── create-todo.js
├── index.js
├── login.js
├── privacy.js
├── project.js
├── signup.js
└── terms.js

This says, "Hey, I'm a project management tool" and aligns with Uncle Bob's vision.

Additionally, this structure offers two entry points (through features or pages) which makes it easier for new developers to learn the codebase. Plus, it eliminates global contexts and hooks folders, reducing potential dumping grounds.

Our folder structure is clean, descriptive, and adaptable. Starting with a feature-driven folder structure can help keep an app organized long-term.

For more on feature-driven folder structures, check out these resources:

You don't feel "job-ready" yet?
Working on a full-scale production React app is so different from personal projects. Especially without professional experience.
Believe me! I've been there. That's why I created a program that exposes you to
  • a production-grade code base
  • realistic tasks & workflows
  • high-end tooling setup
  • professional designs.

Best Practices

Absolute Imports

Instead of using relative imports like this:

import { Button } from "../../ui/button";

Use absolute imports to avoid guesswork and simplify refactoring:

import { Button } from "@features/ui/button";

Set up absolute imports with a jsconfig.json or tsconfig.json file.

{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@features/*": ["src/features/*"]
}
}
}

For more information, check out detailed walkthroughs for React and Next.js.

index.js as Public API

In our final structure, we added an index.js to each feature and component folder.

└── src/
├── features/
│ ├── todos/
│ │ │ # this is used to export the relevant modules aka the public API
│ │ ├── index.js
│ │ ├── create-todo-form/
│ │ ├── edit-todo-modal/
│ │ ├── todo-form/
│ │ └── todo-list/
│ │ │ # the public API of the component (exports the todo-list component and hook)
│ │ ├── index.js
│ │ ├── todo-item.component.js
│ │ ├── todo-list.component.js
│ │ ├── todo-list.context.js
│ │ ├── todo-list.test.js
│ │ └── use-todo-list.js
│ ├── projects/
│ ├── ui/
│ └── users/
└── pages/

These files are often called barrel files and act as the public API of a module or component.

For example, in features/todo/todo-list/index.js we re-rexport the TodoList component and useTodoList hook:

export { TodoList } from "./todo-list.component";
export { useTodoList } from "./use-todo-list";

The file feature/todo/index.js exports everything from its subfolders:

export * from "./create-todo-form";
export * from "./todo-list";
// ... and so on

Why does this help?

Suppose you want to render TodoList in pages/home. Instead of a nested import:

import { TodoList } from "@features/todo/todo-list/todo-list.component";

Import from the todo feature directly:

import { TodoList } from "@features/todo";
...

Benefits:

  1. Cleaner appearance.
  2. Developers don't need to know inner folder structure to use components.
  3. You can define which components to expose. Only export items in index files for external use; others are internal/private.
  4. You can move, rename, or refactor files inside a feature folder as long as the public API remains unchanged.

kebab-case for File and Folder Names

I used to name component files with PascalCase (e.g., MyComponent.js) and functions/hooks with camelCase (e.g., useMyHook.js). Then I switched to a MacBook.

During refactoring, I renamed myComponent.js to MyComponent.js. It worked locally, but the CI on GitHub complained, saying the import statement was broken:

import MyComponent from "./MyComponent";

Hours of debuggine followed. Turns out, MacOS has a case-insensitive file system, so MyComponent.js and myComponent.js are the same. Git didn't recognize the change, but the CI on GitHub used a Linux image, which is case-sensitive, causing issues.

To avoid this, use kebab-case for file and folder names:

  • Instead of MyComponent.js, write my-component.js.
  • Instead of useMyHook.js, write use-my-hook.js.

Next.js uses this by default, and Angular includes it in its style guide. Kebab-case can save you and your team some headaches.

Challenge: Structuring a Project Based on a Design

Consider this design for an error logging tool like Sentry:

The key entities are:

  • organization
  • projects
  • users
  • issues.

How would you create a feature-based folder structure for this design? (Check the solution below, but give it a try first!)

Sneak peak(find help here if you need it)
You don't feel "job-ready" yet?
Working on a full-scale production React app is so different from personal projects. Especially without professional experience.
Believe me! I've been there. That's why I created a program that exposes you to
  • a production-grade code base
  • realistic tasks & workflows
  • high-end tooling setup
  • professional designs.
Improve your health as a software developer
After years working in front of a screen, my health was at an all-time low. I gained weight, my back hurt, and I was constantly tired. I knew I had to make a change. That's why I started Office Walker. I want to help you improve your health and productivity as a software developer.