Chapter 14. Application Structure for React.js

When building small hobby projects or trying a new concept or library, developers can start adding files to a folder without a plan or organizing structure. These could include CSS, helper components, images, and pages. A single folder for all resources becomes unmanageable as the project grows. Any respectably sized codebase should be organized into an application folder structure based on logical criteria. The decision on how to structure your files and application components could be a personal/team choice. It would also generally depend on the application domain and technology used.

This chapter mainly focuses on folder structures for React.js applications that could help in better management of our projects as they grow.

Introduction

React.js itself does not provide a guideline on structuring projects but does suggest a few commonly used approaches. Let’s look at these and understand their pros and cons before discussing folder structures for projects with added complexities and Next.js applications.

At the high level, you can group files in a React application in two ways:

Group by feature

Create folders for every application module, feature, or route.

Group by file type

Create folders for different types of files.

Let us look at this classification in detail.

Group by Module, Feature, or Route

In this case, the file structure would mirror the business model or the application flow. For example, if you have an ecommerce application, you will have folders for product, productlist, checkout, etc. The CSS, JSX components, tests, subcomponents, or helper libraries explicitly required for the product module reside in the product folder:

common/
  Avatar.js
  Avatar.css
  ErrorUtils.js
  ErrorUtils.test.js
product/
  index.js
  product.css
  price.js
  product.test.js
checkout/
  index.js
  checkout.css
  checkout.test.js

The advantage of grouping files by feature is that if there is a change to the module, all the affected files are colocated in the same folder, and the change gets localized to a specific part of the code.

The disadvantage is common components, logic, or styles used across modules should be identified periodically to avoid repetition and promote consistency and reuse.

Group by File Type

In this type of grouping, you would create different folders for CSS, components, test files, images, libraries, etc. Thus, logically related files would reside in different folders based on the file type:

css/
  global.css
  checkout.css
  product.css
lib/
  date.js
  currency.js
  gtm.js
pages/
  product.js
  productlist.js
  checkout.js

The advantages of this approach are:

  • You have a standard structure that you can reuse across projects.

  • Newer team members with little knowledge of the application-specific logic can still find files for something like styles or tests.

  • Common components (such as date pickers) and styles imported in different routes or modules can be changed once to ensure that the effect is seen across the application.

The disadvantages are:

  • A change in logic for a specific module would likely require changes in files across different folders.

  • As the number of features in the application grows, the number of files in different folders would increase, making it difficult to find a specific file.

Either of these approaches could be easy to set up for small- to mid-sized applications with a small number of files (50 to 100) per folder. For larger projects, however, you may want to go for a hybrid approach based on the logical structure of your application. Let us look at some of the possibilities.

Hybrid Grouping Based on Domain and Common Components

Here you would group all common components required across the application in a Components folder and all application flow-specific routes or features in a domain folder (the name could be domain, pages, or routes). Every folder can have subfolders for specific components and related files:

css/
  global.css
components/
  User/
    profile.js
    profile.test.js
    avatar.js
  date.js
  currency.js
  gtm.js
  errorUtils.js
domain/
  product/
    product.js
    product.css
    product.test.js
  checkout/
    checkout.js
    checkout.css
    checkout.test.js

Thus, you can combine the advantages of both “Group by file type” and “Group by feature” by colocating related files, which change together frequently and common reusable components and styles used across the application.

Depending on the complexity of the application, you can modify this to a flatter structure without subfolders or a more nested structure:

Flatter structure

The following example illustrates a flatter structure:

    domain/
        product.js
        product.css
        product.test.js
        checkout.js
        checkout.css
        checkout.test.js
Nested structure

The following example shows a more nested structure:

    domain/
        product/
            productType/
                features.js
                features.css
                size.js
            price/
                listprice.js
                discount.js
Note

It’s best to avoid deep nesting with more than three to four levels because it becomes harder to write relative imports between folders or update those imports when the files are moved.

A variation to this approach is to create folders based on views or routes, in addition to those based on domain, as discussed here. A routing component can then coordinate the view to be displayed based on the current route. Next.js uses a similar structure.

Application Structure for Modern React Features

Modern React apps use different features such as Redux, stateful containers, Hooks, and Styled Components. Let’s see where the code related to these would fit in the application structure proposed in the previous section.

Redux

Redux documentation strongly recommends colocating logic for a given feature in one place. Within a given feature folder, the Redux logic for that feature should be written as a single “slice” file, preferably using the Redux Toolkit createSlice API. The file bundles {actionTypes, actions, reducer} to a self-contained, isolated module. This is also known as the “ducks” pattern (from Redux). For example, as given here:

/src
    index.tsx: Entry point file that renders the React component tree
    /app
        store.ts: store setup
        rootReducer.ts: root reducer (optional)
        App.tsx: root React component
    /common: hooks, generic components, utils, etc
    /features: contains all "feature folders"
    /todos: a single feature folder
        todosSlice.ts: Redux reducer logic and associated actions
        Todos.tsx: a React component

Another comprehensive example that uses Redux without creating containers or Hooks is available here.

Containers

If you have structured your code to categorize components into presentational components and stateful container components, you can create a separate folder for the container components. Containers let you separate complex stateful logic from other aspects of the component:

/src
    /components
        /component1
            index.js
            styled.js

    /containers
        /container1

You can find a complete structure for an app with containers in the same article.

Hooks

Hooks can fit into the hybrid structure just like any other type of code. You can have a folder at the app level for common Hooks that can be consumed by all React components. React Hooks used by only one component should remain in the component’s file or a separate hooks.js file in the component’s folder. You can find a sample structure here:

/components
    /productList
        index.js
        test.js
        style.css
        hooks.js

/hooks
    /useClickOutside
      index.js
    /useData
      index.js

Styled Components

If you are using Styled Components instead of CSS, you can have style.js files instead of the component-level CSS files mentioned earlier. For example, if you have a titlebar component, the structure would be something like this:

/src/components/button/
    index.js
    style.js

An application-level theme.js file would contain the values for colors to be used for background and text. A globals component could include definitions for common style elements that other components can use.

Other Best Practices

In addition to folder structure, some other best practices that you can consider when structuring your React applications are as follows:

  • Use import aliasing to help with long relative paths for common imports. This can be done using both Babel and webpack configurations.

  • Wrap third-party libraries with your API so that they can be swapped if required.

  • Use PropTypes with components to ensure type checking for property values.

Build performance depends on the number of files and dependencies. If you’re using a bundler such as webpack, a few suggestions for improving build times may be helpful.

When using a loader, apply it to only those modules that need to be transformed by it. For example:

const path = require('path');

module.exports = {
  //...
 module: {
   rules: [
     {
       test: /\.js$/,
       include: path.resolve(__dirname, 'src'),
       loader: 'babel-loader',
      },
    ],
  },
};

If you’re using hybrid/nested folder structures, the following example from webpack shows how to include and load files from different paths in the structure:

const path = require('path');

module.exports = {
  //...
  module: {
    rules: [
      {
       test: /\.css$/,
       include: [
          // Include paths relative to the current directory starting with
          // `app/styles` e.g. `app/styles.css`, `app/styles/styles.css`,
          // `app/stylesheet.css`
          path.resolve(__dirname, 'app/styles'),

         // add an extra slash to only include the content of the directory
         // `vendor/styles/`
         path.join(__dirname, 'vendor/styles/'),
        ],
      },
    ],
  },
};

Files that do not have an import, require, define, etc. to reference other modules need not be parsed for dependencies. You can avoid parsing them using the noParse option.

Application Structure for Next.js Apps

Next.js is a production-ready framework for scalable React apps. While you can use the hybrid structures, all the routes in the app must be grouped under the pages folder. (URL of the page = the root URL + relative path in the pages folder).

Extending the structures discussed previously, you can have folders for common components, styles, Hooks, and utility functions. Code related on account of the domain can be structured into functional components that different routes can use. Finally, you will have the pages folder for all routes. Here’s an example for this based on this guide:

--- public/
  Favicon.ico
  images/
--- common/
    components/
      datePicker/
        index.js
        style.js
    hooks/
    utils/
    styles/
--- modules/
    auth/
      auth.js
      auth.test.js
    product/
      product.js
      product.test.js
--- pages/
    _app.js
    _document.js
    index.js
        /products
      [id].js

Next.js also provides examples for many different types of apps. You can bootstrap these using create-next-app to create the template folder structure provided by Next.js. For example, to create the template for a basic blog app, use:

yarn create next-app --example blog my-blog

Summary

This chapter discussed multiple different options for structuring React projects. Depending on the size, type, and components used in the project, you can choose the one most relevant to yours. Sticking to a defined pattern for structuring a project would help you explain it to other team members and prevent projects from getting disorganized and unnecessarily complicated.

The next chapter is the concluding chapter of this book and provides additional links that might be helpful when learning about JavaScript design patterns.