Finding the correct path to import a component is always a big headache in React development. Laying out a proper structure for your React project ahead helps you and your team in many ways through the development process:
- A better understanding of how files are connected and work together
- Easier maintenance as the project scales, avoiding restructuring and modifying all the routes and import paths
- Higher productivity (better readability, finding the source of bugs, etc.)
- A clear organization that cures your OCD
Here is how I put my React project into a clean and practical structure.
Src
src
├── components
├── pages
├── slices
├── utils
├── App.js
├── index.js
├── routes.js
└── store.js
As common, App.js and index.js are entries of the React project, routes.js and store.js are entries of React-router and Redux. The four folders above are the essential lego bricks that hold up the project code.
Components
components
├── guards
│ └── AuthGuard.js
├── layout
│ └── NavBar
│ ├── components
│ │ ├── NavItem.js
│ │ └── NavSection.js
│ └── index.js
├── modules
│ └── cards
│ ├── ItemCard.js
│ └── UserCard.js
└── widgets
└── buttons
├── PrimaryButton.js
└── SecondaryButton.js
/components
contains global components and atomic or modular components.
Global components like AuthGuard.js
and NavBar
are parent components of all pages in the router. For example, AuthGuard.js
wraps around components that need authentication, checks if the user is authenticated, and jumps to the login page if not.
Atomic components like PrimaryButton.js are the smallest UI components that will be reused in modules and pages. Modular components like UserCard.js
are modules that contain multiple widgets as a component to serve a specific function, which are reused in more than one page.
Pages
pages
├── Login.js
└── account
├── index.js
├── profile
│ ├── components
│ │ ├── ProfileCover.js
│ │ └── ProfileDetail.js
│ └── index.js
└── settings
├── components
│ ├── AccountSettings.js
│ └── NotificationSettings.js
└── index.js
/pages
contains pages shown on the website. It should be structured in a similar way as the router to give you a better understanding of how the real website would be browsed. This is also similar to the Next.js approach.
For example, the outer folder /account
is an entrance on the navbar, which includes two pages profile and settings. Each page folder has an index.js
(the page itself), and contains modules that made up this page in the /components folder.
A clear way for me to organize code is that only reusable components are in /components
, while components built for a specific page are under /pages/[page-name]/components
.
It’s important to separate the components in pages once you find them reusable. It’s even better if you are taking a bottom-up approach and build the components first if you find them potentially reusable.
Slices
slices
├── itemSlice.js
└── userSlice.js
Now for the logic side. I use Redux Toolkit which allows me to handle Redux actions and reducers easily in one place called a “slice”. It also comes with many useful middlewares like createAsyncThunk.
For example, the userSlice.js
handles user authentication like this:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import setAuthToken from '../utils/setAuthToken';
// login action
export const login = createAsyncThunk('users/login', async (email, password) => {
const config = {
headers: { 'Content-Type': 'application/json' },
};
const body = JSON.stringify({ email, password });
try {
const res = await axios.post('/api/user/login', body, config);
await localStorage.setItem('token', res.data.token);
return res.data;
} catch (err) {
console.log(err.response.data);
}
});
const userSlice = createSlice({
name: 'userSlice',
initialState: {
loading: false,
user: null,
},
reducers: {},
extraReducers: {
// login reducers
[login.pending]: (state) => {
state.loading = true;
},
[login.fulfilled]: (state, action) => {
state.user = action.payload.user;
setAuthToken(action.payload.token);
state.loading = false;
},
[login.rejected]: (state) => {
state.loading = false;
},
},
});
export default userSlice.reducer;
/slices
basically contains all the Redux Toolkit slices. You can think of /slices
as a central place that governs the global state and the specified functions to modify it. Each slice that handles an aspect of the app’s global state should be separated into one file.
Utils
utils
├── objDeepCopy.js
└── setAuthToken.js
Lastly, /utils
contains files that deal with logic to fulfill a certain function. They are functional pieces commonly used in many places in the project.
For example, setAuthToken.js
gets a token and set or delete the x-auth-token
axios header. It’s used in userSlice.js above.
There are other structures based on different tech stacks. For example, you might want to have /contexts
and /hooks
folders if you are using useContext and useReducers instead of Redux.
This is only one possible structure style among many options, and definitely not the best one. After all, the best React project structure is the one that fits your development style, and you will finally find the one suitable after many adjustments.
Top comments (2)
Worth mentioning are import aliases, barrels and co-location. Aliases work together with barrels to make it easy to logically organize a project regardless of its physical structure. And co-location of files based around features tend to help eliminate the urge to create esoteric folders names. Here's a repo I recently stumbled upon which inspired me to change the way I structure my own projects. Perhaps it will inspire some of you as well.
Useful advice! Colocation is used in /components under each page to keep modules of the same page together. For aliases and barrels, I would set folder names in components clearly so I can directly import with physical path. Nevertheless, they are definitely useful as files in components scales.