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

Johannes KettmannPublished on 

React folder structures... a topic that has been around for ages. But still React’s unopinionated approach frequently raises questions: “Where should I put my files? How should I organize my code?” Honestly, even with years of experience, I found myself asking the same questions.

So I went out to get an overview of the most popular approaches to organizing React projects. Based on my research these are:

  • grouping by file type (e.g. separate folders for components, contexts, 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 write-up reflects my observations of these folder structures evolving in a growing codebase and the problems they can cause. It also includes a short list of best practices and a challenge to turn a design from my upcoming course into a feature-based folder structure.

We won't lay out every detail but rather take a big-picture perspective. In other terms: Where we put our App.js file is less important than the overall approach to organizing files.

To juice this story up we’ll follow the (slightly satiric) journey of a new startup through different stages and a growing codebase. The ingenious idea: We’ll build the next todo app!

Prototype: Group by file types

Obviously, we have a great vision for our startup. Disruption, conquering the world, you know the drill. But everyone has to start small.

So we begin with the React docs. We read that we shouldn’t spend more than 5 minutes deciding on a folder structure. OK, so let’s quickly take inventory:

As the first version of our todo startup, a simple list of todo items would do. That should get us some early pre-seed investment, don't you think?

The simplest folder structure for this case seems to be the “group files by their types” option mentioned in the React docs. This makes our lives easy: Components go in the components folder, hooks in the hooks folder, and contexts in the contexts folder. And since we’re not cavemen we create a folder per component that contains styles, tests, and what not as well.

└── 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 looks pretty simple. And for someone new to programming, this is a great and uncomplicated way to get started. No need to overthink it.

But as you can guess it won't stay this simple for long.

Investment: More files → nesting

Our todo app works great but we’re running out of money. It’s time to get investors on board! Which means we need to show progress. And the best way to show progress is adding new features, right?

Geniuses that we are we have an idea: Why not support editing of todo items? Awesome! We just need a form to edit the todos and maybe 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

Not too bad but the components folder is getting crowded. It’s also slightly annoying that related folders like checkbox and text-field (both form fields) or edit-todo-modal and todo-form (parent and child) are so far apart.

Maybe we could group and colocate 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

With this folder structure, it’s easier to get an overview of the important functionality. We removed clutter from the components folder in two ways:

  1. By colocating child components with their parents.
  2. By grouping the generic UI and layout 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!

With a bit of thinking, we find a simple solution: we add a second page where users can create todos via a form. Luckily we can reuse the form for editing todos. That’s amazing because it saves precious resources of our developer team.

Anyway, having custom todo items means we need a user entity and authentication. Since the todo form will now be shared between the “create todo page” and the “edit todo modal” we should move it up 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? I see a few problems.

First, the components folder is getting crowded again. But admittedly, we won’t be able to avoid this in the long run. At least if we want to keep our folder structure somewhat flat. So let's disregard this problem.

Second (and more importantly), the components folder contains a mixture of different kinds of components:

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

The solution: We create a separate pages folder. We move all the page components and their children there. Only components that are shown on multiple pages stay 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/

To me, this looks much cleaner. When a new developer joins the company it’s now easy for them to identify all the pages. This gives them an entry point to investigate the codebase or debug the application.

This seems to be a popular folder structure that many developers use. Here are two examples:

But since the goal of our startup is to conquer the world we obviously can't just stop here.

World Domination: Colocation

We’ve grown into a serious business. The world’s most popular todo app (according to it’s 5-star rating). Everyone wants to pour money into our startup. Our team grows and with it our codebase.

└── 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/

Sorry, I ran out of creativity. You get the point: The global hooks and contexts folders get crowded.

At the same time, the code for the more complex components is still scattered over multiple folders. The component may live somewhere in the pages folder, using a shared component in the components folder and relying on business logic in the contexts and hooks folders. With a growing codebase, this makes it a lot harder to track down dependencies between files and promotes intertwined code.

Our solution: colocation! Whenever possible we move the contexts and hooks next to the components where they are used.

└── 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 got rid of the global contexts folder. Unfortunately, there’s no good place to put the use-auth file so the global hooks folder stays for now. No drama, but the fewer global folders the better. They quickly turn into a dumping ground.

The most important advantage of this folder structure: We can grasp all the files that belong to a feature at once. No need to look into 5 different folders to find the code for a single component.

But at the same time, there are still some problems:

  1. The code related to the “todo” entity is spread over multiple folders. Which will become a bit messy once we start adding more entities.
  2. Would you guess that the todo-list component lives in the home folder just from looking at the folder structure?
└── src/
├── components/
├── hooks/
└── pages/
├── create-todo/
├── home/
├── login/
├── privacy/
├── signup/
└── terms/

Exit: Group by Features

Our dreams come true: we’re about to sell our startup for billions. We created a unicorn 🦄 FAANGT.

But with success comes responsibility: our users demand new features. Again. Most importantly they want to create different projects to keep their todo items for work separate from the todo items on their grocery list. Who could have guessed...

Our solution: we add a new “project” entity that contains a list of todo items.

We decide to add two new pages. One to create a project and one to show the project including its todos. The home page has to change as well. It should show a list of all projects as well as a list of all todos.

That means the todo-list component is now used on two pages so it has to move to the common components folder

└── 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 I can see two problems:

  • Looking at the pages folder it’s not immediately clear that this app has todos, projects, and users. We can understand it but we first need to process folder names like create-todo (todo entity) or login (user entity) and separate them from the unimportant stuff (e.g. privacy and terms).
  • It feels arbitrary that some components exist in the shared components folder just because they are used on multiple pages. You need to know where and in how many places a component is used to understand in which folder you can find it.

At this point you might think: you can simply open a file by its name with the help of your IDE (e.g. by hitting “Ctrl + P” in VS Code). True. But that doesn't help much if you don't remember the name in the first place. So from my perspective, it's always good if you can navigate a codebase in multiple ways.

Let’s adjust the folder structure one last time and group our files by feature.

“Feature” is a pretty broad term and you’re free to choose whatever that means to you. In this case, we go for a combination of entities (todo, project, and user) as well as 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

Note that we introduced index.js files to each folder. These are often referred to as the public API of a module or a component. If you don’t know what that means you can find a more detailed explanation further below.

But first, let’s discuss the new “group by features” folder structure.

Discussion: Feature-Driven Folder Structure and Screaming Architecture

In his article Screaming Architecture Bob Martin says:

Your architectures should tell readers about the system, not about the frameworks you used in your system. If you are building a health-care system, then when new programmers look at the source repository, their first impression should be: “Oh, this is a heath-care system”.

Let’s remember our initial folder structure where we grouped our files by type:

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

Does this tell us something about the system or the framework? This folder structure screams: “I’m a React app.”

What about 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

We have no idea which framework was used. But this folder structure jumps at you and screams “Hey, I'm a project management tool”.

That’s looks pretty much like what Uncle Bob describes.

Apart from the descriptive architecture, the features and pages give a developer two different entry points to the application.

  • If we need to change a component and only know that it’s on the home page open pages/home.js and click through the references.
  • If we need to change the TodoList but don’t know where it’s used we simply open the features/todo folder and we’ll find it somewhere inside.

And finally, we got rid of the global contexts and hooks folders. We can still re-introduce them if necessary. But at least for the moment, we removed these potential dumping grounds.

I personally am very happy with this folder structure. We could continue for a bit and clean up the folder structure within a feature. For example, the todo folder currently looks a bit messy. Alan Alickovic with his awesome example project Bulletproof React suggests separating the files inside each feature by file type (as we did in the beginning).

But from my perspective, our current folder structure is sufficiently clean and descriptive. Due to the self-contained nature of the “features” it should be easy to refactor if necessary. At the same time, our folder structure is simple enough to use in a project from the start. It may save us some headaches in the long run.

From my experience, many projects evolve in a similar way as described on this page. But due to time pressure, the developers never have the chance to clean up the folder structure. So the project ends up in a mess of different approaches. Starting with a feature-driven folder structure can help keeping the app clean over the long run.

If you'd like to take a deep dive into the feature-driven folder structure here is a list of more resources:

Best practices

Absolute imports

Let's say we want to render a button in the todo list component inside the file features/todo/todo-list. By default we would use a relative import:

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

Managing the relative paths with ../.. can become annoying especially during refactoring sessions when you move files around. It also quickly turns into guesswork to figure out how many .. are required.

As an alternative, we can use absolute imports.

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

Now it doesn't matter where you move the TodoList component. The import path will always be the same.

With Create React App absolute imports are very easy to set up. You just add a jsconfig.json file (or tsconfig.json for TypeScript) and define the paths aliases:

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

You can find more detailed walkthroughs here for React and here for Next.js.

index.js as public API

In our final folder structure, we added an index.js to each feature and component folder. Here a quick reminder:

└── 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/

As mentioned, these index.js files are often referred to as the public API of a module or a component.

But what does that mean?

Here is an example of how the index file in the folder features/todo/todo-list might look like:

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

The file simply imports and exports some modules. Here is an even shorter version:

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

And the file feature/todo/index.js just exports everything from its subfolders.

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

How does that help us?

Imagine you want to render the TodoList component inside the file pages/home. Instead of importing from the nested folder like this

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

we can simply import from the todo feature directly.

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

This has a few benefits:

  1. It looks nicer.
  2. A developer doesn't need to know the inner folder structure of a feature to use one of its components.
  3. You can define which components etc. you want to expose to the outside. Only the things you export in your index files should be used in other parts of the app. The rest is internal/private. Hence the name “public API”.
  4. You can move around, rename, or refactor everything inside a feature folder as long as the public API stays the same.

kebab-case for file and folder names

Like many others, I used to name component files with PascalCase (e.g. MyComponent.js) and functions/hooks with camelCase (e.g. useMyHook.js).

Until I switched to a MacBook.

During a refactoring session, I renamed a component file called myComponent.js to the correct format MyComponent.js. Everything worked locally but for some reason, the CI on GitHub started complaining. It claimed that the import statement below was broken.

import MyComponent from "./MyComponent";

Turns out, MacOS is a case-insensitive file system by default. MyComponent.js and myComponent.js are the same thing. So Git never picked up the change in file name. Unfortunately, the CI on GitHub used a Linux image. And this one is case-sensitive. So according to my CI the file didn’t exist while my local machine said everything was OK.

It took me hours to understand this. And apparently, I'm not the only one who ran into this problem:

The solution: use kebab-case for your file and folder names. For example:

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

This is what Next.js uses by default. Angular included it in its coding styleguide. I don't see a reason why not to use kebab-case but it might save you or a teammate of yours some headaches.

Challenge: How would you structure a project based on this design?

This is a design of an error logging tool for web apps (e.g. like Sentry) from my upcoming course.

  • The entity at the foundation of this app is an “organization”.
  • Each organization has projects and users assigned to it.
  • Each project has issues (e.g. errors that are sent from an organization’s website).
  • Each of the top items in the left navigation represents a page.

How would you turn this design into a feature-based folder structure? (You can find my solution below. Don’t peak.)

Sneak peak(find help here if you need it)