How to organize large React applications and make their components modular

In this article, I will discuss the approach used when building and constructing large React applications. one of the best features of React is how it gets out of the way and is hardly descriptive in terms of file structure. As a result, you will find many questions on Stack Overflow and similar sites asking how to build applications.

This is a very presumptuous topic and there is no right way to do it. In this article, I will introduce you to the decisions made when building React applications: picking tools, structuring files, and breaking components into smaller pieces.

Generating tools and finishing

Once you get into webpack and master the concepts, you really have incredible power. I use Babel to compile my code, including React-specific transformations (like JSX) and webpack-dev-server to serve my site locally. I personally haven’t found much benefit from hot reloading, so I’m very happy with webpack-dev-server and its automatic page refreshes.

I use the ES module first introduced in ES 2015 (via Babel translations) to import and export dependencies. This syntax has been around for a while, and although webpack can support CommonJS (aka Node style import), it made sense for me to start using the latest syntax.

In addition, webpack can remove invalid code from bundles using the ES2015 module, which is not perfect but is a very handy feature that will become even more beneficial as the community moves towards publishing code to npm in ES2015. Most of the web ecosystem has shifted to ES modules, so this is the obvious choice for every new project I start. If you don’t want to use webpack , most tools will want it to support those tools as well, including other bundles like Rollup.

Folder Structure

For all React applications, there is no one correct folder structure. (As with the rest of this article, you should change it to your liking.) But the following worked well for me.

To keep things organized, I put all my application code in a folder called src. It contains only the code that ends with the final bundle, and nothing more. This is useful because you can tell Babel (or any other tool that works on your application code) to look at only one directory and make sure it doesn’t handle any code that isn’t needed. Other code, such as webpack configuration files, is located in appropriately named folders. For example, my top-level folder structure usually contains.

- src => app code here
- webpack => webpack configs
- scripts => any build scripts
- tests => any test specific code (API mocks, etc.)

Typically, the only files that will be at the top level are index.html, package.json and any point files like .babelrc. Some people like to include Babel configuration package.json, but I find that those files get large on larger projects with many dependencies, so I like to use . eslintrc, .babelrc, etc.

React Components

The tricky bit once you have the src folder is determining how to construct the components. In the past, I’ve put all my components in one big folder, such as src/components, but I find that in larger projects, this can quickly overwhelm you.

The common trend is to have folders for “smart” and “dumb” components (also called “containers” and “representative “), but personally, I’ve never found explicit folders work for me. While I do have components roughly divided into “smart” and “dumb” components (which I will describe in more detail below), there is no specific folder for each component.

We have grouped the components according to the area of the application where they are used, with the core folder being the generic components used throughout (buttons, headers, footers, etc. that are generic and reusable). The rest of the folders are mapped to specific areas of the application. For example, we have a folder named cart, which contains all the components related to the shopping cart view, and a folder named listings, which contains the code used to list the items that the user can buy on the page.

Grouping into folders also means that you can avoid adding prefixes to the application areas used by the components. For example, if we have a component that can display the total cost of a user’s shopping cart, which many people generally like to prefer to name CartTotal, I might prefer to use Total because I am importing it from the Cart folder.

import Total from '../cart/total'
// vs
import CartTotal from '../cart/cart-total'

The extra prefix is a bit clearer, especially if you have two to three similarly named components, and this technique usually avoids additional repetition of names.

Preferred capital letter extensions for jsx

Many people use capital letters in the file to name React components to distinguish them from regular JavaScript files. So, in the above import, the file will be CartTotal.js or Total.js. I tend to use lowercase files with an underscore as a separator, and to differentiate, I use the .jsx extension for React components. So, therefore, I stick with cart-total.jsx.

This has the small benefit of being able to search only React files by limiting the pairs of files that can be searched, and .jsx can even apply specific webpack plugins to those files as needed.

Whichever naming convention is chosen, it’s important to stick with it. With the combination of conventions in the codebase, it will soon become a nightmare for you to navigate as it grows. You can .jsx to enforce this convention using the rules in eslint-plugin-react.

One React component per file

According to the previous rule, we follow the convention of a React component file and that component should always be exported by default.

Typically, our React file is shown below.

import React from 'react'

export default function Total(props) {
  …
}

For example, in cases where a component must be wrapped to connect it to a Redux datastore, the fully wrapped component will be the default export.

import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'

export default function Total(props) {
  …
}

export default connect(() => {…})(Total)

You will notice that we still export the original components. This is useful for testing, where you can use “normal” components without having to set up Redux in your unit tests.

By leaving the component as the default export, you can easily import the component and know how to use it without having to look up the exact name. The downside to this approach is that the importer can call the component as needed. Again, we have a convention for this: imports should be named after files. So, if you want to import total.jsx, the component should be imported as Total. user-header.jsx becomes UserHeader, and so on.

It is worth noting that the rule of one component per file is not always followed. If you end up building a small component to help you render some of your data and only use it in one place, it is often easier to keep it in the same file as the component that uses it. There is a cost to keeping components in separate files: there are more files, more imports, and generally more to follow as a developer, so consider whether it’s worth it. Like most of the suggestions in this article, they are rules with exceptions.

“smart” and “dumb” React components

I briefly mentioned the separation of the “smart” and “dumb” components, which we insist on in our code base. Although we can’t identify it by splitting it into folders, you can roughly divide our application into two types of components.

  • The “smart” component that handles data, connects to Redux, and handles user interaction
  • the “dumb” component that is given a set of props and presents some data to the screen

These components make up the bulk of our application, and you should always prefer them if possible. They are easier to use, less troublesome, and easier to test.

Even if we have to create “smart” components, we try to keep all the JavaScript logic in its own file. Ideally, components that have to handle data should give the data to some JavaScript that can handle it, so that the manipulation code can be tested separately from React, and can be mocked as needed when testing React components.

Avoid big render methods

Although this is often used to refer to methods defined on renderReact-like components, it still exists when talking about functional components, as you should be aware of components that render unusually large HTML fragments.

One thing we try to do is to have many smaller React components instead of fewer larger ones. A good guide to oversized components is the size of the rendering function. If it gets clunky, or you need to split it up into many smaller render functions, it may be time to consider abstracting a function.

This is not a hard-and-fast rule. You and your team need to know the size of the component you’re happy with before you pull out more components, but the size of the component render function is a good metric. You can also use the number of props or items in the state as another good metric. If a component is going to use seven different props, it may be an indication that it is doing too much.

Always use prop-type

React allows you to record the names and types of properties that are expected to be given to the component using the component’s prop-types package.

By declaring the names and types of expected props, and whether they are optional, you can have more confidence that you have the correct property when using the component, and spend less time debugging the property name or giving it the wrong type if you forget. You can use the eslint-plugin-react PropTypes rule to force this action.

While it may be futile to spend some time adding these, when you do, you will find it’s not too bad when you reuse components written six months ago.

Redux

We also use Redux in many applications to manage the data in the application, and how to construct Redux applications is another very general problem with many different insights.

For us, the winner is Ducks, a proposal that puts the actions, reducers and action creators for each part of the application in a single file. Again, while this is the approach that worked for us, the choice and adherence to conventions is paramount here.

Rather than having reducers.js and actions.js, each of which contains code related to each other, Ducks Systems believes it makes more sense to combine related code into one file. Suppose you have a Redux store with two top-level keys user and posts. your folder structure is shown below.

ducks
- index.js
- user.js
- posts.js

index.js will contain the code to create the main reducer (possibly using combineReducersRedux for this), and then place all the code in user.js where posts.js is, usually as follows.

// user.js

const LOG_IN = 'LOG_IN'

export const logIn = name => ({ type: LOG_IN, name })

export default function reducer(state = {}, action) {
  …
}

This eliminates the need to import actions and action creators from different files, and keeps different parts of the store’s code next to each other.

Standalone JavaScript Module

Although the focus of this article is on React components, when building React applications, you will find yourself writing a lot of code that is completely separate from React. This is one of the things I like most about the framework: much of the code is completely separate from the components.

I recommend this whenever you find components filled with business logic that could be moved out of the component. In my experience, we have found that a folder called lib or services works well here. The exact name is irrelevant, but all you need is a folder full of “non-reactive components”.

Sometimes these services export a set of functions, and sometimes they export objects for related functions. For example, we have services/local-storage.js, which provides a small wrapper for the local window.localStorage API.

// services/local-storage.js

const LocalStorage = {
  get() {},
  set() {},
  …
}

export default LocalStorage

Excluding logic from such a component brings many benefits.

  • You can test this code in isolation, without rendering any React components.
  • Within your React component, you can root the service and return the data needed for a particular test.

Quizzes

As mentioned above, we have tested our code very extensively and have grown to rely on Facebook’s Jest framework as the best tool to get the job done. It is very fast, excels at handling large numbers of tests, can be run quickly in monitor mode and gives you quick feedback, and has some handy features to test React immediately. I’ve covered this extensively on SitePoint before, so I won’t go over it here, but I will discuss how to build tests.

In the past, I’ve worked on having a separate tests folder that contains everything for all tests. So if you had src/app/foo.jsx, you would also have tests/app/foo.test.jsx. In reality, as the application gets larger, this makes finding the right files more difficult, and if you move files into src, you usually forget to move them into test, and the structure is out of sync. Also, if one of the files tests needs to be imported into the file src, it will end up being a very long import. I’m pretty sure we’ve all run into this problem.

import Foo from '../../../src/app/foo'

If the directory structure is changed, these will be very difficult to use and very difficult to fix.

Instead, placing each test file next to its source file avoids all these problems. To distinguish them, we add the suffix .spec to the tests (although others use .test or simply add it), -test but they coexist with the source code, otherwise with the same name.

- cart
  - total.jsx
  - total.spec.jsx
- services
  - local-storage.js
  - local-storage.spec.js

As the folder structure changes, it is easy to move the correct test files and this is also very obvious when the files do not have any tests, so you can find and fix these problems.

Conclusion

One of the best features of the React framework is how it allows you to make most decisions around tools, build tools and folder structure, and you should embrace that decision. I hope this article has given you some ideas on how to approach larger React applications, but you should take my ideas and adapt them to fit your own and your team’s preferences.

Leave a Reply