Antonello Zanini I'm a software engineer, but I prefer to call myself a technology bishop. Spreading knowledge through writing is my mission.

Optimize React apps using a multi-layered structure

8 min read 2465

React Logo Over a Background with Balancing Rocks

React projects can easily become a mess. Losing track of where files are located is extremely common, which can lead to significant inefficiencies during development. So how can you improve and organize your React projects? By organizing all of your project’s files in a multi-layered architecture.

As a result, you will always know how to place — and find — each file. This is a game-changer. It will not only make you a more efficient developer, but it will also make your project much more maintainable.

Plus, by turning projects into organized architectures, your entire team could benefit as well. This is because different people could work at different layers, avoiding overlap and potential overhead.

Let’s look at why you’ll want to use multi-layered structures in React.

How multi-layered structures optimize React applications

Reusing code

Imagine all of your React projects sharing the same multi-layered file structure, allowing you to maximize your time. Instead of adapting components or files imported from other codebases to your target project, this shared file structure allows you to import any kind of file or code snippet from one project to another like a simple copy-and-paste operation. This encourages code reuse, making it easier and less time-consuming for you to build your app.

For example, let’s say you have several mathematical problems in one of your projects. By using this file structure, you aren’t tempted to spread all the required utility functions throughout your codebase.

Instead, it incentivizes you to define a math-utils.js file where you can store all of your math utilities. Plus, by externalizing elements serving the same purpose in the same file, you are making them more reusable. In fact, if you face similar issues in a future project, you can simply copy the math-utils.js file defined previously and paste it into your utils layer.

Avoiding code duplication

The consequence of imposing a multi-layered structure is that every file, function, or piece of code belongs in a particular place. This means that before writing something new, you should look in its assumed folder first, which discourages code duplication and a reduction of the build size.

A bundle with a lot more code than is needed will obviously be larger than needed. This is not good for browsers, which have to download and then render the code.

So, avoiding code duplication makes your application cleaner, easier to build, and faster when it’s rendered. For example, whenever you need to retrieve the data returned from a particular API, you may be tented to follow this approach:

We made a custom demo for .
No really. Click here to check it out.

function FooComponent1(props) {     
      // ...

      // retrieving data
      fetch(`/api/v1/foo`)
      .then((resp) => resp.json())
      .then(function(data) {
          const results = data.results;
          // handling data ...
        })
        .catch(function(error) {
          console.log(error);
        });

      // component logic ...
}

export default FooComponent1
function FooComponent2(props) {
      // ... 

      // retrieving data
      fetch(`/api/v1/foo`)
      .then((resp) => resp.json())
      .then(function(data) {
          const results = data.results;
          // handling data in a different way than FooComponent1 ...
        })
        .catch(function(error) {
          console.log(error);
        });

      // different component logic from FooComponent1 ...
}

export default FooComponent2

As you can see, FooComponent1 and FooComponent2 are affected by a code duplication issue. In particular, the API endpoint is being duplicated, forcing you to update it every time it changes.

This is not a good habit. Instead, you should map the API into the API layer as follows:

export const FooAPI = {

    // ...

    // mapping the API of interest
    get: function() {
        return axiosInstance.request({
            method: "GET",
            url: `/api/v1/foo`
        });
    },

    // ...

}

And then use it in where needed to avoid code duplication:

import React from "react";
import {FooAPI} from "../../api/foo";
//...

function FooComponent1(props) {
      // ... 

      // retrieving data
      FooAPI
      .get()
      .then(function(response) {
          const results = response.data.results;
          // handling data ...
        })
        .catch(function(error) {
          console.log(error);
        });

      // component logic ...
}

export default FooComponent1
import React from "react";
import {FooAPI} from "../../api/foo";
//...

function FooComponent2(props) {
      // ... 

      // retrieving data
      FooAPI
      .get()
      .then(function(response) {
          const results = response.data.results;
          // handling data in a different way than FooComponent1 ...
        })
        .catch(function(error) {
          console.log(error);
        });

      // different component logic from FooComponent1 ...
}

export default FooComponent2

Now, the API endpoint is saved in only one place, as it should always be.

Harnessing team synergy

Working with a well-known file structure means that development teams member are all on the same page and use it the same way.

Whenever a team member maps a new API, or creates a new utils file or a new component, for example, any team member will be able to use it immediately. And, because each file is logically placed in a particular folder that everyone knows how (and where) to access, communication overhead is reduced or completely eliminated, thus streamlining the company’s development processes.

A potential side effect of building multi-layered structures

Creating npm modules can be complex

There’s one potential downside to having such an organized file architecture: the files are spread across many folders, which forces you to uses relative imports intensively. This represents a well-known problem when creating npm modules.

Relative imports are prone to breaking, especially when trying to encapsulate part of your codebase to make it publishable as stand-alone modules. On one hand, this can certainly represent an additional challenge when trying to create an npm package.

On the other, this can easily be avoided by turning relative imports into non-breakable absolute imports as described here. Such a technique allows you to turn this:

import UserComponent from "../../components/UserComponent";
import userDefaultImage from "../../../assets/images/user-default-image.png";
import {UserAPI} from "../../apis/user";

Into this:

importUserComponentfrom "@components/UserComponent";
import userDefaultImage from "@assets/images/user-default-image.png";
import {UserAPI} from "@apis/user";

So, using many relative imports should not be seen as a real problem.

Now, let’s see how to design an efficient multi-layered architecture for your projects in React and JavaScript.

Creating multi-layer architecture

First, let’s take a look at the final result of multi-layer architecture. Keep in mind that each layer of our architecture should be enclosed in a specific folder. As expected, giving these folders the same names as the layers the architecture consists of is highly recommended. This way, retrieving a file becomes intuitive and fast. You will also always know where to place a new file, which is a great plus.

This is what the final structure looks like:

Final Folder Structure
As you can see, each layer of the architecture is represented by a folder.

Now, let’s dive into each of the layers the presented architecture consists of.

1. API layer

By harnessing a promise-based HTTP client such as Axios, you can define a function for each API your application depends on, encapsulating all the logic required to call it. This way, you can divide where API requests are defined from where they are actually used. Let’s see how such a layer can be built with a simple example:

const axiosInstance = axios.create({
   baseURL: 'https://yourdomain.org'
});

export const UserAPI = {
    getAll: function() {
        return axiosInstance.request({
            method: "GET",
            url: `/api/v1/users`
        });
    },
    getById: function(userId) {
        return axiosInstance.request({
            method: "GET",
            url: `/api/v1/users/${userId}`
        });
    },
    create: function(user) {
        return axiosInstance.request({
            method: "POST",
            url: `/api/v1/users`,
            data: user
        });
    },
    update: function(userId, user) {
        return axiosInstance.request({
            method: "PUT",
            url: `/api/v1/users/${userId}`,
            data: user,
        });
    },
}

Now, you can import the UserAPI object to make the call where you need, just like this:

import React, {useEffect, useState} from "react";
import {UserAPI} from "../../api/user";
//...

function UserComponent(props) {
    const { userId } = props;

    // ...

    const [user, setUser] = useState(undefined)

    // ...

    useEffect(() => {
        UserAPI.getById(userId).then(
            function (response) {
                // response handling
                setUser(response.data.user)
            }
        ).catch(function (error) {
            // error handling
        });
    }, []);

    // ...
}

export default UserComponent;

By employing Promise technology, you can collect all your API requests definition in the same place. But let’s not lose sight of the goal. This article is not meant to show what you can achieve with the API layer, and you can follow this and this for further reading.

API Layer Folder
Organizing all the files that belong to the API layer in the same folder.

Instead, we’ll focus on the fact that with this approach, you will be able to define a completely dedicated layer to API requests. This will have all the logic to deal with API requests saved in just one place, as opposed to duplicating the entire logic to call an API every time you need it.

2. Asset layer

Your React project may depend on files that are not strictly part of your codebase, such as multimedia files. Having a place to store them all is best practice, as you should clearly divide them from your code. Please note that such a layer can be organized in sub-folders based on types or extensions to keep it organized:

Sample Asset Layer
A sample asset layer organized by type.

Furthermore, this is a good place to store all your translation files required in an international application employing an internationalization framework, such as i18next.

All Translation Files
Keeping all the i18next translation files in the translations folder.

3. Component layer

This is where you should put all your components. You should have one folder per component, named in the same way as the component itself. Such a folder usually contains only two files: index.js and index.css. They represent where the React component is defined and where its styling is contained, respectively.

Sample Component Layer
A sample component layer in action.

Because your components might be large, you should think about organizing them in a nested structure. Finding a good criterion for allocating them into sub-folders is not always a simple task.

At the same time, keep in mind that you can always change it on the fly, rearranging the structure in which your components are stored as you wish.

4. Constant layer

A common practice is to define all the constants used in your application in a single constans.js file. In a small project, this approach might seem like the best solution. Inevitably, as your file becomes larger, it’ll slowly turn into a mess.

This is why you should split your constants across multiple files. For example, in an international application, you could think about storing all the custom-defined keys used by i18next in a specific file.

This way, all the constants related to i18n would be in the same file, making them easier to be managed. Similarly, you can follow the latter approach in other circumstances, as in the example below:

Splitting All Application Constants
Splitting all application constants into four different files.

5. Redux Layer

This is an optional layer, and you should define it only if your application uses Redux. As stated in the official documentation, Redux is a predictable state container for JavaScript applications. If you have ever worked with Redux, you should know how many files it requires.

Getting lost in the multitude of Redux reducers and Redux actions is common, especially for the latter. Plus, defining actions involves boilerplate code. This is why using a library, such as redux-actions, to manage all your actions is recommended. Then, you will be able to simply obtain an organized structure by arranging your files as follows:

Sample Redux Layer
A sample Redux layer composed by two folders and one file.

As shown above, you can put all your Redux reducers in the reducers folder, your Redux actions into the actions folder, and define a top-level index.js file containing your store and persist logic. If required, you can even organize the files contained in the aforementioned two folders in subfolders.

6. Route Layer

Your project may already have all your routes covered by your application stored in a routes.js file. Defining a file containing all your routes to gather them in one place and separate them from when they are actually used.

Again, for small projects, this approach might be enough. The problem lies in the fact that you cannot predict how big your application will grow. This is why you should define a layer designed to contain all your routing files and logic. A trick to easily split your routes.js file into many files is to create one for each of the level routing paths used by your application.

Sample Route Layer
A sample route layer made up of four files.

7. Utility Layer

This is the place to store all of the custom-defined utility functions your entire codebase depends on. While you may have your functions stored in a single utilis.js file, as the size of your project grows, you could be forced to split it into multiple files.

This is why having a dedicated layered for this may become unavoidable. Instead of waiting for this to occur, get ahead of the game and build a new layer as follows:

Splitting Starting File
Splitting a starting utils.js file into two different files.

8. View Layer

This is the top layer where you’ll store all the pages your applications consist of. Please note that this is the only layer that may require files imported from any other layer mentioned before.

In case your project has a large amount of pages, you should organize them into sub-folders. A good criterion to achieve this is to replicate the routing structure, just as you would do in a PHP project. For instance, all the pages sharing the https://yourdomain.dom/users/ base path should be placed into the Users folder.

Sample View Layer Structure
A sample view layer structure with the criterion presented in the example.

Remaining files in your multi-layered structure

Keep in mind that such an architecture may not be enough to cover all the files that make up your codebase. This is why there will always be additional files that you might not know where to place.

Following such an organized structure allows you to reduce them to a few places. A good rule of thumb is that if you have fewer than three files, you should leave them in the top-level folder of your project, or where you think they belong. Otherwise, you can always design new layers meeting your needs.

Conclusion

In this article, we looked at how to (and why you should) organize a React project to make it more efficient. By placing your files in folders representing a multi-layered architecture, you can turn your project into a much more organized structure.

With such an approach, each file will have its own place. This will enable you and your team to set architecture standards, make your entire codebase more robust and maintainable, and streamline your development process.

Thanks for reading! I hope that you found this article helpful. Feel free to reach out to me with any questions, comments, or suggestions.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Antonello Zanini I'm a software engineer, but I prefer to call myself a technology bishop. Spreading knowledge through writing is my mission.

4 Replies to “Optimize React apps using a multi-layered structure”

  1. I think I disagree with some of the structures shown. I’ve always found feature focused layers to be much easier to work on (vs type based layers).

    The reason being, most units of work tends to be feature focused. If I am working on Foo, I don’t want to open 5 different layers for reducers, actions, views, blah, blah. This encourages commits to have wide surface area.

    By organizing by feature, commits will have low surface area. You would modify foo/action.js, foo/reducer.js, foo/view.js, foo/blah.js. Everything is in 1 layer.

    This is much easier for both the reader and writer.

  2. You assume that only view layer should use other layers? Let’s look on this example. I have component with user details and it is in component layer. I want to use it on many views and I don’t want to duplicate code so I want to pass only user ID to component, but in this case I have to use API request inside component and finally I use other layer in component layer. Where I should use a API request? Each time I want to use user details component and pass result to component?

  3. Hi, I am the author. Thanks for your feedback!

    I would like to add that the best architecture depends on what you are working on. Based on my experience, such a structure works quite well, but it is not the only one possible. The one you presented is a very good alternative, for sure. I have worked with this file structure in many projects, with other team members, and we are all satisfied.

    At the same time, I don’t exclude the possibility that other architectures and file structures might be better than this one, especially in contexts other than those that helped me conceive it.

  4. Hi, I am the author. Thanks for your feedback!

    The view layer is the only one that might need to import files from all other layers. For example, the component layer should depend on the view layer, while the other way around is feasible. I hope that’s clear now!

Leave a Reply