How maintanable is your React App?

How maintanable is your React App?

What if your application were large but still easy to maintain and support? I’ll show you how to achieve that by thinking modular.

Have you noticed that there are many guides and tutorials on getting started with React for Web? So have I. Usually the instructions are trying to show you how to build your TODO app, which was likely generated by the create-react-app tool, using common libraries such as React Router and Redux.

Actually, that’s a great start because when we’re learning something new and unfamiliar, we should first start from the simplest example. After a few weeks of developing, if you don’t stick to some patterns, you’ll probably end up with multiple files inside the same folder or even worse: few files with thousand lines. Both scenarios could cost you maintainability in Long Term Support.


Modules

I really like this awesome word. In programming, the meaning of Modular is to design your application in interchangeable pieces of independent functionality that contain everything necessary to perform their assigned tasks, in other words: these pieces of your application can be pulled in or out without much effort. Of course, they can crosstalk or depend on each other, but remaining interchangeable is a premise, so we’ll have to be careful when designing their relationships.


Get to work

Enough talking, right? Let’s get into it!

Sample Application

I’ve designed a sample application for teaching purposes that is available to show all members of a Slack. Let’s take a look at the module’s design and how their structures are independently designed:

Modules

📌 App Module

  • Components
  • Routers

📌 API Module

  • Redux Actions

📌 Members Module

  • Components
  • Containers
  • Navigation Menu Item
  • Redux Actions
  • Redux Reducer
  • Redux Selectors
  • Routers

📌 About Module

  • Components
  • Navigation Menu Item
  • Routers

Folder Structure

.
├── src
│   ├── Config.js
│   ├── assets
│   │   └── logo.jpg
│   ├── index.css
│   ├── index.js
│   ├── modules
│   │   ├── about
│   │   │   ├── components
│   │   │   │   └── About
│   │   │   ├── index.js
│   │   │   ├── menu.js
│   │   │   └── router.js
│   │   ├── api
│   │   │   ├── actions.js
│   │   │   ├── reducer.js
│   │   │   ├── selectors.js
│   │   │   └── index.js
│   │   ├── app
│   │   │   ├── components
│   │   │   │   ├── App
│   │   │   │   ├── Navigation
│   │   │   │   ├── Button
│   │   │   │   ├── Error
│   │   │   │   └── Loading
│   │   │   ├── containers
│   │   │   │   ├── App
│   │   │   │   ├── Navigation
│   │   │   ├── index.js
│   │   │   └── router.js
│   │   ├── index.js
│   │   └── members
│   │       ├── actions.js
│   │       ├── components
│   │       │   ├── MemberCard
│   │       │   └── MembersList
│   │       ├── containers
│   │       │   └── MembersList
│   │       ├── index.js
│   │       ├── menu.js
│   │       ├── reducer.js
│   │       ├── router.js
│   │       └── selectors.js
│   ├── reducer.js
│   ├── serviceWorker.js
│   └── store.js
├── static.json
└── yarn.lock

Essentially, we have a similar folder structure and files as with the modules. The goal here is to have isolation throughout the modules while still following the DRY approach; that’s why we have reducer files for each module that use this feature, for example. To better understand their modularity, let’s dive deeper into some files and folders. Since each module exports similar files we’re able to get all the modules routes at once by doing something like this:

import { getModulesMetavalues } from 'moduleReader';

getModulesMetavalues('routes');

The logic behind the getModulesMetavalues is actually pretty simple, it just iterates a specific key from all of the registered modules:

import * as modules from 'modules';

export const getModulesMetadata = (key) =>
  Object.keys((modules)).reduce((acc, module) => {
    if (typeof modules[module] !== 'object') {
      console.warn(`Provide a object export for module ${module}`);
    }
    if (key in modules[module]) {
      return { ...acc, [module]: modules[module][key] };
    }
    return acc;
  }, {});

export const getModulesMetavalues = (key) =>
  Object.values(getModulesMetadata(key)).flat();

So essentially, our main module file (e.g. src/modules/members/index.js) must export the routes and other features like this:

import * as components from './components';
import * as containers from './containers';
import * as actions from './actions';
import menu from './menu';
import reducer from './reducer';
import routes from './routes';
import * as selectors from './selectors';

export { components, containers, actions, menu, reducer, routes, selectors };

This files and nomenclature are just convention among modules to make them easier to predict but you’re free to export anything else from a module.

Files and Folders

Each file has its own responsibility and utility. Let’s take a look:

router.js

The router file is supposed to export a simple Route component from the React Router library. This way, you can get them to use as children inside the Switch component:

// src/modules/app/containers/App/index.js

import mapProps from 'map-props';
import { App } from 'modules/app/components';
import { getModulesMetavalues } from 'moduleReader';

export default mapProps({
  children: () => getModulesMetavalues('routes')
})(App);
// src/modules/app/components/App/index.js

render() {
  return (
    <Switch>{this.props.children}</Switch>
  );
}

The menu file is intended to help you with creating dynamic menu links. All you need to do is to return a simple React component containing your link:

// src/modules/members/menu.js
import { MembersMenu } from './components';

export default MembersMenu;
// src/modules/app/containers/Navigation/index.js

import mapProps from 'map-props';
import { Navigation } from 'modules/app/components';
import { getModulesMetavalues } from 'moduleReader';

export default mapProps({
  children: () => getModulesMetavalues('menu')
})(App);

reducer.js

The reducer file is supposed to return only a single reducer. If you have to have more than a single tree-leaf, use the combineReducers function.

import { combineReducers } from 'redux-immutable';
import { buildRequestReducer } from 'modules/api/reducer';

const list = buildRequestReducer('fetchMembers');

export default combineReducers({ list });

selectors.js

The selectors file is supposed to have getters from the redux state tree. Try to follow the DRY approach by composing and reusing other selectors with reselect library.

// src/modules/members/selectors.js
import { createSelector } from 'reselect';

export const getMembers = state => state.members;

export const getMembersList = createSelector(getMembers, state => state.list);

actions.js

The actions file is supposed to have both sync and async actions with a descriptive name of what they’re expected to do. Try also to follow the DRY approach by reusing other modules actions whenever you can:

// src/modules/members/actions.js

import { buildRequestAction } from 'modules/api/actions';

export const fetchMembersList = buildRequestAction('fetchMembers');

/components

The components folder is supposed to have only presentational components. i.e. they don’t know how to load states and are main focused on working with a good properties support.

/containers

The containers folder is supposed to have the stateful components. i.e. they’re focused on fetching data to apply on the Components.

Interchangeability between modules

Usually modules have some dependency on other modules and to achieve that we have got to make sure that we’re doing it correctly.

Files imports

Consider using absolute paths like this if you’re trying to import a file inside or outside of your module. It makes moving files across modules easier:

import { buildRequestAction } from 'modules/api/actions';

Components structure

Usually, we’ll have multiple files related to the same component (e.g. styles, tests, etc) and to better organize, that I like to group them into the same folder. Instead of creating a file components/MembersList.js, I create a file components/MembersList/index.js which makes it easier to put all related files together.

Extra → Test files

I really like the approach of having a test file alongside the implementation file. Instead of having two files across different folders like:

src/modules/members/__tests__/actions.js
src/modules/members/actions.js

I prefer to have them contained inside the same folder to make finding the files easily in my code editor:

src/modules/members/actions/index.js
src/modules/members/actions/test.js
src/modules/members/actions/e2e.test.js

Final Thoughts

I believe that by implementing a modular approach you will have a well-defined pattern to make your application and its functionalities easily understood by other developers. Also, when we’re thinking about LTS we should stick to patterns that are easily recognized or your application could easily get messed up because every developer will try to apply their own approach there.

Choose what difficulties you should deal with and that way you don’t let the difficulties choose you.

Do you wanna know more how do we do this in Trio?

In Trio, we help small and mid-sized companies by providing them with software engineering teams on demand. Eliminating the process of selection and hiring, we allocate to you the teams of highly-qualified software engineering teams that match perfectly your company’s needs. Managing a remote team doesn’t have to be hard. Do you want to know more about working with Trio?

Tell us about your project → https://trio.dev

Posted on by Dhyego Calota