React is one of the most unopinionated frontend frameworks in existence. From the selection of states, androuting to managing your code structure, React does not inherently provide any guidelines. Comparatively, Angular provides a much better insight into where and how the building blocks should be placed in the code. This puts React developers in a difficult position at the start of a project. Regardless of our experience, we all find it extremely difficult to formulate the perfect codebase structure at the beginning of a new project.
In general, React project structures are often iteratively evolved alongside the project's scope and complexity. When new libraries are added, such as Redux and React Router, the initial structure needs to be refactored to accommodate the added complexity. With pressure on deadlines for the project's completion, the refactoring gets stuck in backlog until the project is completely unmaintainable.
In this guide, we will explore several directory structures that are used in production-grade applications and analyze the pros and cons of each. It is important to keep in mind that no single structure universally fits every project. Depending on the project's size, scope, complexity, and future aspects, the most suitable structure varies. Thus, this guide can help as a starting point in choosing the correct initial structure so that future refactoring is minimal.
Let's use a model project to evaluate our different codebase organizations. We will be looking at a codebase of a blog frontend, where we have the following features:
1. Articles (or blog posts), categories, and users
2. A home page that shows a list of categories and a list of articles
3. A category page that shows category-specific information and articles
While the feature set looks simple at a glance, the actual codebase will span across multiple files and directories. We also assume that we use Redux to manage our application state.
First, we'll explore the most common and easiest structure in use. I call it the flat structure since it has minimal directory nesting and is quite straightforward. It follows the principle of separating the logic and view in the root level and then adding Redux related directories to the mix.
1└── src
2 ├── actions
3 │ ├── articleActions.js
4 │ ├── categoryActions.js
5 │ └── userActions.js
6 ├── api
7 │ ├── apiHandler.js
8 │ ├── articleApi.js
9 │ ├── categoryApi.js
10 │ └── userApi.js
11 ├── components
12 │ ├── ArticleComponent.jsx
13 │ ├── ArticleListComponent.jsx
14 │ ├── CategoryComponent.jsx
15 │ ├── CategoryPageComponent.jsx
16 │ └── HomePageComponent.jsx
17 ├── containers
18 │ ├── ArticleContainer.js
19 │ ├── CategoryPageContainer.js
20 │ └── HomePageContainer.js
21 ├── index.js
22 ├── reducers
23 │ ├── articleReducer.js
24 │ ├── categoryReducer.js
25 │ └── userReducer.js
26 ├── routes.js
27 ├── store.js
28 └── utils
29 └── authUtils.js
Directory functions, in brief, include the following:
HomePageContainer
would have ArticleListComponent
as well as CategoryComponent
. authUtils
would have functions to process the JWT token from the API to determine the user scopes. store.js is simply the Redux store and the routes.js aggregates all routes together for easy access.
Note: Defining all routes in a single file has been a deprecated as a practice, according to new React Router docs. It promoted segregating routes into components for better readability. Check React Router Docs for a better understanding.
With the above understanding, let's analyze why and why not to use a flat structure.
commentAction
to actions, commentReducer
to reducers, CommentComponent
to component, and CommentContainer
to containers. HomePageContainer
. With the above issues, we could do a slight improvement by introducing the pages
directory as a way of providing some organization.
1└── src
2 ├── actions
3 │ ├── articleActions.js
4 │ ├── categoryActions.js
5 │ └── userActions.js
6 ├── api
7 │ ├── apiHandler.js
8 │ ├── articleApi.js
9 │ ├── categoryApi.js
10 │ └── userApi.js
11 ├── components
12 │ └── ArticleComponent.jsx
13 ├── containers
14 │ └── ArticleContainer.js
15 ├── index.js
16 ├── pages
17 │ ├── CategoryPage
18 │ │ ├── CategoryPageContainer.js
19 │ │ └── components
20 │ │ └── CategoryPageComponent.jsx
21 │ └── HomePage
22 │ ├── components
23 │ │ ├── ArticleListComponent.jsx
24 │ │ ├── CategoryComponent.jsx
25 │ │ └── HomePageComponent.jsx
26 │ └── HomePageContainer.js
27 ├── reducers
28 │ ├── articleReducer.js
29 │ ├── categoryReducer.js
30 │ └── userReducer.js
31 ├── routes.js
32 ├── store.js
33 └── utils
34 └── authUtils.js
Now with the above improvement, the directory structure provides some context into the actual positioning of the various components in the app. At a glance, it is clear that HomePageComponent
, ArticleListComponent
, and CategoryComponent
are part of the HomePage
. As an important side effect, now the things that remain on the components and containers directory at the root level are the shared components that do not directly belong to any one page. So, we could go one step further and group them into a common
directory.
1└── src
2 ├── actions
3 │ └── ...
4 ├── api
5 │ └── ...
6 ├── common
7 │ ├── components
8 │ │ └── ArticleComponent.jsx
9 │ └── containers
10 │ └── ArticleContainer.js
11 ├── index.js
12 ├── pages
13 │ └── ...
14 ├── reducers
15 │ └── ...
16 ├── routes.js
17 ├── store.js
18 └── utils
19 └── ...
This looks much better. If your app does not has a huge application state, and is rather view- and logic-heavy, the above structure should work. It provides significant clarity and maintainability. But if your Redux code is also growing with the rest of the features, you will soon find that you need a better organization for the state as well.
The view-state split improves upon the previous structure to simply give a better organization to the state. It separates the view and logic-heavy components from the state component, but introduces additional structuring within the state.
1└── src
2 ├── api
3 │ ├── apiHandler.js
4 │ ├── articleApi.js
5 │ ├── categoryApi.js
6 │ └── userApi.js
7 ├── common
8 │ ├── components
9 │ │ └── ArticleComponent.jsx
10 │ └── containers
11 │ └── ArticleContainer.js
12 ├── index.js
13 ├── pages
14 │ ├── CategoryPage
15 │ │ ├── CategoryPageContainer.js
16 │ │ └── components
17 │ │ └── CategoryPageComponent.jsx
18 │ └── HomePage
19 │ ├── components
20 │ │ ├── ArticleListComponent.jsx
21 │ │ ├── CategoryComponent.jsx
22 │ │ └── HomePageComponent.jsx
23 │ └── HomePageContainer.js
24 ├── routes.js
25 ├── state
26 │ ├── article
27 │ │ ├── articleActions.js
28 │ │ └── articleReducer.js
29 │ ├── category
30 │ │ ├── categoryActions.js
31 │ │ └── categoryReducer.js
32 │ ├── middleware.js
33 │ ├── store.js
34 │ └── user
35 │ ├── userActions.js
36 │ └── userReducer.js
37 └── utils
38 └── authUtils.js
Changes in the above structure are simple. The state is now nested with one more level where actions and reducers of a particular application feature are grouped. With this, finding where the changes need to be done for a particular feature is visible at once. For example, if your API decides to send articles tags and now you want to show them in your Articles components, you first edit the api
, then the state
, and finally update the ArticleComponent
.
state
directory.utils
directory, which again separates the portion of feature code from feature component code.While this structure has a few issues, it can be accommodated for the majority of project use cases. But if the project has a lot of moving parts and the development team is large, the above issues start to become blocking points—especially if the project is being developed by a distributed team (open source projects are prime examples). Then we need a better organization that allows developers to work on individual application features without disrupting the entire codebase.
I was first introduced to the application feature-based split through Node Best Practices by Yoni Goldberg. It is aimed at providing structure for nodejs projects, which are equally unopinionated. It provides a scalable model to overcome the common issues in using MVC pattern on node backends. In brief, it advises splitting directories by application features rather than code functions. For example, in our app, we have the following three features:
These are also known as domains. By splitting them as such, we could group all functional code related to an application feature inside a directory so that a developer can concentrate only on the particular directory. Let's explore how we could fit the pattern to our app:
1├── api
2│ ├── apiHandler.js
3│ ├── articleApi.js
4│ ├── categoryApi.js
5│ └── userApi.js
6├── article
7│ ├── Article.jsx
8│ ├── ArticleList.jsx
9│ └── state
10│ ├── articleActions.js
11│ └── articleReducer.js
12├── category
13│ ├── Category.js
14│ └── state
15│ ├── categoryActions.js
16│ └── categoryReducer.js
17├── category-page
18│ └── CategoryPage.jsx
19├── common
20│ └── state
21│ ├── commonActions.js
22│ └── commonReducers.js
23├── home-page
24│ ├── HomePageContainer.js
25│ └── HomePage.jsx
26├── index.js
27├── middleware.js
28├── routes.js
29├── store.js
30└── user
31 ├── authUtils.js
32 └── state
33 ├── userActions.js
34 └── userReducer.js
In the above structure, the following changes are made:
Article.js
is a combination of ArticleContainer
and ArticleComponent
.Even with the issues outlined above, this structure seems to be the most functional out of the options we discussed in the guide. After refactoring more than a few codebases using each of these options, I have fixed on it for any project with significant complexity. In a production-grade application, many smaller application features are required, including notifications, error feedback, centralized loading, auth handling, etc., and with a feature-based structure, adding and removing them is easy.
Structuring your React + Redux codebase at the beginning is a confusing task for most frontend developers. Since the framework itself does not provide strict guidelines, we are forced to use trial-and-error based methods to find the best-suited structure for the project. In this guide, we explored a few common methods of organization, analyzing the pros and cons of each. While one structure does not fit all different project requirements, we can reference the above structures as starting points. This greatly minimizes refactoring effort in the future.