DEV Community

Cover image for How I structure my React projects
Jeffrey Yu
Jeffrey Yu

Posted on • Edited on

How I structure my React projects

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:

  1. A better understanding of how files are connected and work together
  2. Easier maintenance as the project scales, avoiding restructuring and modifying all the routes and import paths
  3. Higher productivity (better readability, finding the source of bugs, etc.)
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

/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
Enter fullscreen mode Exit fullscreen mode

/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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

/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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
vhs profile image
vhs

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.

Collapse
 
jeffreythecoder profile image
Jeffrey Yu

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.