How to organize a large React application and make it scalable

Original author: Jack Franklin
  • Transfer


One of the best features of React is that it does not impose any restrictions on the file structure of the project. Therefore, there are so many questions on StackOverflow and similar resources on how to structure React applications. This is a very controversial topic. There is no one right way. We offer to understand this issue with the help of an article by Jack Franklin , in which he talks about the approach to structuring large React applications. Here you will find out what decisions can be made when creating React applications: about choosing tools, structuring files and breaking down components into smaller parts.

Build and Code Validation Tools


Webpack is a great tool for collecting projects. Despite its complexity, the fact that the team did a great job on version 2 and the new documentation site greatly simplifies matters. As soon as you take Webpack with a clear concept in mind, you really have an incredibly powerful tool. You can use Babel to compile the code, including for React-specific conversions: for example, JSX and webpack-dev-server for local "hosting" of the site. Perhaps HMR will not bring any big benefits, so it will be enough to use webpack-dev-server with its automatic page refresh.

Also, for import and export of dependencies, we will use the syntax of ES2015 modules (which is transformed by Babel). This syntax has been around for a long time, and although Webpack supports CommonJS (Node-style import syntax), it's best to use the very latest and best. In addition, Webpack can remove dead code from the bundle using ES2015 modules, which, although not ideal, is a very convenient feature that will become more useful when the community moves to publish code in npm in the ES2015 standard.

Configuring Webpack Modules Resolution


The only thing that can be disappointing when working with large projects with an attached file structure is the determination of the relative paths between the files. You will find that you have a lot of code that looks something like this:

import foo from './foo'
import bar from '../../../bar'
import baz from '../../lib/baz'

When you create your application using Webpack, you can specify the directory in which Webpack should search for the file if it cannot find it itself. This allows you to determine the base folder to which the entire import belongs. For example, you can always put your code in the src directory. And you can make Webpack always look in this directory. This is done in the same place where you inform Webpack of any other file extensions that you may be using, for example jsx:

// inside Webpack config object
{
  resolve: {
    modules: ['node_modules', 'src'],
    extensions: ['.js', '.jsx'],
  }
}

The default value for resolve.modules is ['node_modules'], so you need to add it as well, otherwise Webpack will not be able to import files installed using npm or yarn.

After that, you can always import files relative to the src directory:

import foo from './foo'
import bar from 'app/bar' // => src/app/bar
import baz from 'an/example/import' // => src/an/example/import

Although this ties up your Webpack application code, this is probably a profitable compromise because it makes it easier for you to execute code and makes it much easier to add imports.

Directory structure


There is no single correct directory structure for all React applications. As with the rest of this article, you should change the structure to suit your preferences. One example of a well-functioning structure is described below.

The code lives in src


To keep things organized, put all the application code in a directory called src. It contains only code, which is reduced to the final bundle, and nothing more. This is useful because you can tell Babel (or any other code processing tool) to just look in one directory and make sure that it is not processing any code that it does not need. Other code, such as Webpack configuration files, is located in the appropriate directory. For example, a top-level directory structure may contain:

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

Typically, the only files at the top level are index.html, package.json, and any dotfiles such as .babelrc. Some people prefer to include the Babel configuration in package.json, but in large projects with many dependencies these files can become too large, so it is advisable to use .eslintrc, .babelrc, etc.

By storing the application code in src, you can also use the setting resolve.modulesmentioned above, which makes importing easier.

React Components


Having decided on the src directory, you need to decide how to structure the components. If all of them are placed in one large folder, such as src / components, then in large projects it is very quickly cluttered.

A common trend is the availability of separate folders for smart and stupid components (also known as container and presentation components), but such an explicit division is not always useful. And although you probably have components that can be classified as “smart” and “stupid” (more on that below), it is not necessary to create folders for each of these categories.

We grouped components based on the areas of the application in which they are used, along with the core directory for common components that are used everywhere (buttons, headers and footers - components that are universal and reusable). Other directories correspond to specific areas of the application. For example, we have a catalog called cart, which contains all the components associated with the shopping cart, and a catalog called listings, which contains code for lists of things that users can buy on the page.

Grouping by directories also means that you can avoid unnecessary prefixes pointing to the application area in which the components are used. For example, if we have a component that displays the total cost of the user's basket, you can call it Total, not CartTotal, because it is imported from the cart directory:

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

This rule can sometimes be violated: sometimes an additional prefix can bring additional clarity, especially if you have 2-3 similarly named components. But often this method avoids duplicate names.

Jsx extension instead of capital letters


Many people use uppercase letters in the names of React components to distinguish them from regular JavaScript files. Thus, in the aforementioned import, the files will be called CartTotal.js, or Total.js. But you can stick to lowercase letters with hyphens as separators, that is, to distinguish React components, use the .jsx: cart-total.jsx file extension.

This gives a slight additional advantage: you can easily search only your React files, limiting the search in files by .jsx, and you can even apply specific Webpack plugins to them if necessary.

Whatever file naming convention you choose, it's important to stick to it. Having a combination of several conventions in your application will quickly become a nightmare in which you will have to somehow navigate.

Only one component in a file


Following the previous rule, we adhere to the agreement that we should always have only one component in one file, and the component should always be the default export.

Usually our React files look like this:

import React, { Component, PropTypes } from 'react'
export default class Total extends Component {
  ...
}

In the case when we need to wrap the component in order to connect it, for example, to the Redux data store, the fully wrapped component becomes the default export:

import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
export class Total extends Component {
  ...
}
export default connect(() => {...})(Total)

Did you notice that we are still exporting the original component? This is really useful for testing when you can work with a “simple” component rather than configure Redux in your unit tests.

When exporting a component by default, it is easy to import the component and know how to get it, instead of looking for the exact name. One of the drawbacks of this approach is that the importing user can invoke the component at will. Once again, we have an agreement for this: import should be done by file name. Therefore, if you import total.jsx, then the component should be named Total. user-header.jsx becomes UserHeader, and so on.

Smart and Dumb React Components


Above we briefly mentioned the separation of components into “smart” and “stupid”. And although we do not put them in separate directories, you can broadly divide the application into these two types of components:

  • Smart components manipulate data, connect to Redux, and deal with user interaction.
  • Stupid components only provide a set of properties for displaying some data on the screen.

Silly components make up the bulk of our application, and if possible, you should always give them preference. They are easier to work with, fewer problems, and easier to test.

Even when we have to create smart components, we try to save all the JavaScript logic in a separate file. Ideally, data manipulating components should pass this data to some JavaScript, which will actually do it. Then the manipulation code can be tested separately from React, and you can do anything with it when testing the React component.

Avoid big render methods


One thing we strive for is to have many small React components, not a smaller number of larger ones. A good indicator that your component is getting too large is the size of the render function. If it becomes cumbersome, or you need to break it down into several smaller functions, then maybe it's time to think about separating the component.

This is not a strict rule; you and your team must clearly understand what is considered a “large” component for you before increasing their number. But render component function size is a good guide. You can also use the number of props or items in state as another good indicator. If a component accepts seven different props, this may be a sign that it is doing too much.

Always use prop-type


React allows, using the prop-types package, to document the names and types of properties that you expect to be passed to the component. Note that this is not the case in React 15.5; previously, proptypes were part of the React module.

When declaring the names and types of expected properties, as well as whether they are optional, you should feel confident in working with components and spend less time debugging if you forget the name of the property or assign it the wrong type. This can be achieved using the ESLint-React PropTypes rule.

It may seem that the time to add them will be wasted. But if you do this, you will thank yourself by going to reuse the component that you wrote six months ago.

Redux


We use Redux to manage data in many of our applications, and structuring Redux applications is another very common question that has many different opinions.

The winner for us is Ducks, which puts actions, reducer and action creators for each part of your application in one file.

Instead of having reducers.js and actions.js, each of which contains pieces of code for communication with each other, the Ducks system claims that it makes sense to group the linked code into a single file. Suppose you have a Redux store with two top-level keys, user and posts. Your folder structure will look like this:

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

index.js will contain the code that the main reducer creates, possibly using combineReducers from Redux, and in user.js and posts.js you put all the code for them, which usually looks like this:

// 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 allows you to store code nearby for different parts of your repository.

Standalone JavaScript Modules


Although the focus of this article was on React components, you can write a lot of code that is completely separate from React when creating a React application.

Each time you find a component with business logic that can be removed from the component, it is recommended that you do this. Usually a directory with the name lib or services works well - a specific name does not matter, but a directory full of "non-React components" is really what you need.

These services sometimes export a group of functions, or an object of related functions. For example, we have services/local-storageone that provides a small wrapper around the native API window.localStorage:

// services/local-storage.js
const LocalStorage = {
  get() {},
  set() {},
  ...
}
export default LocalStorage

Keeping your logic separate from such components has some really great advantages:

  • You can test this code in isolation, without having to render any React components.
  • In your React components, you can stub services to have the data you need for a particular test.

Tests


The Jest Facebook framework is a great testing tool. It very quickly and well copes with a lot of tests, it starts quickly in viewing mode, quickly gives you feedback and out of the box provides some convenient functions for testing React. Consider how you can structure your tests.

Someone will say that it is better to have a separate directory that contains all the tests for all tasks. If you have src / app / foo.jsx then there will also be tests / app / foo.test.jsx. But in practice, when the application grows, it makes it difficult to find the necessary files. And if you move files to src, you often forget to move them to test, and structures lose synchronization. In addition, if you have a file in tests where you need to import a file from src, you will get a very long import. Surely everyone came across:

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

It’s hard to work with, and it’s hard to fix it if you change the directory structure.

But placing each test file with the source file avoids all these problems. To distinguish them, we add the suffix .spec to our tests, although others use .test or just -test, but they all live next to source files with the same name:

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

As the directory structure changes, it is easy to move the correct test files. It is also immediately noticeable when the file has no tests. You can quickly identify these problems and fix them.

conclusions


There are many ways to achieve your goal, this can be said about React. One of the best features of the framework is how it allows you to make most decisions regarding tools, build tools, and directory structure. Hopefully this article has given you some ideas on how to structure your larger React applications.

Also popular now: