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:
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).
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.
The results are not shared with a third party. They are only tracked anonymously with Plausible.io.
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/
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:
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.
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:
todo-list
component lives in the home
folder.└── src/├── components/├── hooks/└── pages/├── create-todo/├── home/├── login/├── privacy/├── signup/└── terms/
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:
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).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.
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:
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.
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:
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:
MyComponent.js
, write my-component.js
.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.
Consider this design for an error logging tool like Sentry:
The key entities are:
How would you create a feature-based folder structure for this design? (Check the solution below, but give it a try first!)
└── src/├── features/│ ├── alerts/│ ├── issues/│ │ # this contains the settings│ ├── organization/│ ├── projects/│ │ ├── index.js│ │ ├── project-card.js│ │ └── project-list.js│ ├── ui/│ │ ├── index.js│ │ ├── card/│ │ ├── header/│ │ ├── footer/│ │ ├── side-navigation/│ │ └── tag/│ └── users/└── pages/├── alerts.js├── issues.js├── projects.js├── settings.js└── users.js