Introduction
When I began my first React application, I struggled to decide how I wanted to organize my code. I knew that I was free to structure it in any way I wanted, but was unsure of what best practice was. After doing much research, I was forced to conclude that there was no accepted best practice. You can find many opinions out there, and today I am adding my own. With my first application, I came to a solution that I thought was pretty solid at the time, but I have since developed many more applications and have honed my approach with time. I am confident that I have landed on a structure that will be flexible and manageable as a project grows.
Goals
Before getting into the solution, let’s talk a bit about the aspects that should be addressed when developing a structure. The following are the questions that I attempted to address.
-
Accessibility & Understanding - Can a developer hop into the code base and immediately correlate it to what they see in the UI?
-
Flexibility - Can features easily be moved around as project requirements change?
-
Future Growth & Scalability - Does the structure support easily making additions as new requirements emerge?
-
Minimizing Stale Code - Is it easy for code to get lost and left behind as features come and go?
As I go through the solution, I will address each of these questions.
Solution
At a high level, the primary tenant for this solution is to organize by feature, not by component type. So what does this mean? Let's start with a simple Dashboard webpage with a table. Each row in the table has a View action that, when clicked, takes you to another page with a graph.
Here is an example of how you might organize this page by component type.
├── components
│ ├── App.js
│ ├── graphs
│ │ └── IndividualGraph.js
│ ├── pages
│ │ └── Dashboard.js
│ └── tables
│ └── MeowCountTable.js
├── enum
│ └── EnabledEnum.js
├── index.js
└── serviceWorker.js
Notice that when glancing at these files, you are not aware of their relationship with each other. This structure is something that I commonly see, but as a project grows in size, it becomes difficult to maintain. Next, we organize the same project by feature.
├── application
│ ├── App.js
│ ├── Dashboard.js
│ ├── app
│ └── dashboard
│ ├── MeowCountTable.js
│ └── meowCountTable
│ └── IndividualGraph.js
├── enum
│ └── EnabledEnum.js
├── index.js
└── serviceWorker.js
This structure helps us achieve our above-stated goals. Notice that there are still some shared elements (enum in this case) at the root level that do remain organized by type. In any project, there will need to be some parts that are shared among many areas of the application. Now, let’s compare the feature organized solution against the questions presented above.
1) Accessibility & Understanding
Let's say we need to modify the graph that displays when we click on a row in the Meow Count table on the Dashboard page. If our directory is organized by feature, we can assume the component is under application > dashboard > meowCountTable
in the codebase. This means that for any component, a developer knows where it's located based on where it shows up on the UI. Also, the inverse is true. When presented with an MR, a developer will know exactly where to go to test out the changes. There is no need to search through routes to figure out where the new feature shows up on the page.
2) Flexibility
As features are now grouped, they become transportable in case requirements change. On the topic of flexibility, I think it is important to mention how import statements are setup. In a basic React application, typically, all import statements would use relative paths, which can create some issues with portability when including shared elements such as enums. Ideally, a mix of absolute paths and relative paths based on context would be best. So let’s define the distinction between the two. We want to use an absolute path when the resource we are requesting is not part of the components feature set. And we want to use a relative path if it is. Take MeowCountTable.js
, for example. IndividualGraph.js
is a child of MeowCountTable.js
, and let's say that it also uses the EnabledEnum.js
enum. To keep this code portable, we want the import statements to be as follows.
import React from 'react';
import EnabledEnum from 'enum/EnabledEnum.js';
import { IndividualGraph } from './meowCountTable/IndividualGraph';
The MeowCountTable
could now be moved to a different area of the application without needing to make any changes to its import statements.
Note: To make absolute paths available, you need to add the following to your
jsconfig.json
in the root of your project./jsconfig.json { "compilerOptions": { "baseUrl": "src" }, "include": ["src"] }
3) Future Growth & Scalability
As a project increases in scale, the number of features grows. Structuring by feature allows for a more uniform increase in the complexity of directories as files get added. As a developer, this is helpful because the section of code you are currently working within is not any more convoluted when a project is sized at hundreds of components compared to a dozen.
4) Minimizing Stale Code
Hopefully, you can now see how a feature-based structure helps minimize stale code. This is the case simply because of the combination of all of the points above. If the code is easier for a developer to navigate, it is easier for them to maintain and keep clean as changes in requirements take place.
Reusable Components
At this point, you may ask what about components that get reused many times, like a <Button />
for example. As mentioned above, there is still a place for having some elements organized by type at the top level. You could add a directory for shared components within src, and this would satisfy the solution outlined here. However I prefer to use Storybook. It is an open-source tool for developing components in isolation. I definitely encourage you to read through what Storybook has to offer. So, including Storybook, our final structure looks like
├── application
│ ├── App.js
│ ├── Dashboard.js
│ ├── app
│ └── dashboard
│ ├── MeowCountTable.js
│ └── meowCountTable
│ └── IndividualGraph.js
├── enum
│ └── EnabledEnum.js
├── storybook
│ ├── index.js
│ └── components
│ └── button
│ ├── buton.js
│ └── buton.stories.js
├── index.js
└── serviceWorker.js
Community
If you have questions, comments, concerns, please voice them on Twitter.