11 tips for using Redux when developing React applications

Original author: Christopher T.
  • Transfer
When it comes to developing React applications, in terms of code architecture, small projects are often more flexible than large ones. There is nothing wrong with creating such projects using practical guidelines aimed at larger applications. But all this, in the case of small projects, may be simply unnecessary. The smaller the application, the more “condescending” it refers to the use of simple solutions in it, possibly non-optimal, but not requiring a lot of time for their implementation. Despite this, I would like to note that some of the recommendations that will be given in this material are aimed at React applications of any scale.





If you've never created a production application, then this article can help you prepare for the development of large-scale solutions. Something like this could very well become one of your next projects. The worst thing that can happen to a programmer is when he works on a project and realizes that he needs to refactor large amounts of code to improve the scalability and maintainability of the application. Everything looks even worse if there were no unit tests in the project before refactoring.

The author of this material asks the reader to take his word for it. He has been in similar situations. So, he got several tasks that needed to be solved in a certain time. At first, he thought that everything he did was excellent. The source of such thoughts was that his web application, after making changes, continued to work, and at the same time continued to work quickly. He knew how to use Redux, how to establish normal interaction between user interface components. It seemed to him that he deeply understood the concepts of reducers and actions. He felt invulnerable.

But here the future crept up.

After a couple of months of working on the application, more than 15 new features were added to it. After that, the project got out of control. The code that used the Redux library has become very difficult to maintain. Why did it happen so? At first, didn’t it seem that the project expected a long and cloudless life?

The author of the article says that, by asking similar questions, he realized that he had planted a time bomb in the project with his own hands.

The Redux library, if used correctly in large projects, helps, as such projects grow, to keep their code in a supported state.

Here are 11 tips for those who want to develop scalable React applications using Redux.

1. Do not place the action code and constants in one place


You might come across some Redux tutorials in which constants and all actions are placed in the same place. However, this approach, as the application grows, can quickly lead to problems. Constants need to be stored separately, for example, in ./src/constants. As a result, to search for constants, you have to look at only one folder, and not several.

In addition, the creation of separate files storing actions looks completely normal. Such files encapsulate actions directly related to each other. Actions in a single file, for example, may have similarities in terms of what and how they are used.

Suppose you are developing an arcade or role-playing game and creating classes warrior(warrior), sorceress(sorceress) andarcher(archer). In such a situation, you can achieve a high level of code support by organizing the actions as follows:

src/actions/warrior.js
src/actions/sorceress.js
src/actions/archer.js

It will be much worse if everything falls into one file:

src/actions/classes.js

If the application becomes very large, then it might be even better to use approximately the following structure of code splitting into files:

src/actions/warrior/skills.js
src/actions/sorceress/skills.js
src/actions/archer/skills.js

Only a small fragment of such a structure is shown here. If you think more broadly and consistently use this approach, you will end up with something like this set of files:

src/actions/warrior/skills.js
src/actions/warrior/quests.js
src/actions/warrior/equipping.js
src/actions/sorceress/skills.js
src/actions/sorceress/quests.js
src/actions/sorceress/equipping.js
src/actions/archer/skills.js
src/actions/archer/quests.js
src/actions/archer/equipping.js

Here's what the action from the file src/actions/sorceress/skillsfor the object might look like sorceress:

import { CAST_FIRE_TORNADO, CAST_LIGHTNING_BOLT } from '../constants/sorceress'
export const castFireTornado = (target) => ({
  type: CAST_FIRE_TORNADO,
  target,
})
export const castLightningBolt = (target) => ({
  type: CAST_LIGHTNING_BOLT,
  target,
})

Here is the contents of the file src/actions/sorceress/equipping:

import * as consts from '../constants/sorceress'
export const equipStaff = (staff, enhancements) => {...}
export const removeStaff = (staff) => {...}
export const upgradeStaff = (slot, enhancements) => {
  return (dispatch, getState, { api }) => {
    // Обратиться к слоту на экране обмундирования для того чтобы получить ссылку на посох волшебницы
    const state = getState()
    const currentEquipment = state.classes.sorceress.equipment.current
    const staff = currentEquipment[slot]
    const isMax = staff.level >= 9
    if (isMax) {
      return
    }
    dispatch({ type: consts.UPGRADING_STAFF, slot })
    api.upgradeEquipment({
      type: 'staff',
      id: currentEquipment.id,
      enhancements,
    })
    .then((newStaff) => {
      dispatch({ type: consts.UPGRADED_STAFF, slot, staff: newStaff })
    })
    .catch((error) => {
      dispatch({ type: consts.UPGRADE_STAFF_FAILED, error })
    })
  }
}

The reason we organize the code in this way is because new features are constantly being added to projects. This means that we need to be prepared for their appearance and at the same time strive to ensure that the files are not overloaded with code.

At the very beginning of work on the project, this may seem unnecessary. But the larger the project becomes, the stronger the strength of such an approach will be felt.

2. Do not place reducer code in one place


When I see that the code of my reducers turns into something similar to the one shown below, I understand that I need to change something.

const equipmentReducers = (state, action) => {
  switch (action.type) {
    case consts.UPGRADING_STAFF:
      return {
        ...state,
        classes: {
          ...state.classes,
          sorceress: {
            ...state.classes.sorceress,
            equipment: {
              ...state.classes.sorceress.equipment,
              isUpgrading: action.slot,
            },
          },
        },
      }
    case consts.UPGRADED_STAFF:
      return {
        ...state,
        classes: {
          ...state.classes,
          sorceress: {
            ...state.classes.sorceress,
            equipment: {
              ...state.classes.sorceress.equipment,
              isUpgrading: null,
              current: {
                ...state.classes.sorceress.equipment.current,
                [action.slot]: action.staff,
              },
            },
          },
        },
      }
    case consts.UPGRADE_STAFF_FAILED:
      return {
        ...state,
        classes: {
          ...state.classes,
          sorceress: {
            ...state.classes.sorceress,
            equipment: {
              ...state.classes.sorceress.equipment,
              isUpgrading: null,
            },
          },
        },
      }
    default:
      return state
  }
}

Such code, no doubt, could very quickly lead to a lot of mess. Therefore, it is best to maintain the structure of work with the state in the simplest possible form, aiming at the minimum level of their nesting. You can, instead, try to resort to the composition of reducers.

A useful trick in working with reducers is to create a higher order reducer that other reducers generate. Here you can read more about it.

3. Use informative variable names


Naming variables, at first glance, may seem like an elementary task. But in fact, this task may be one of the most difficult.

The selection of variable names is generally relevant to practical guidelines for writing clean code. The reason why there is such a thing as a “variable name” in general is because this aspect of code development plays a very important role in practice. Unsuccessful selection of variable names is a sure way to harm yourself and your team members in the future.

Have you ever tried to edit someone else’s code and at the same time encountered difficulties in understanding what exactly this code does? Have you ever run a foreign program and found that it does not work as expected?

I would argue to prove that in such cases you have encountered the so-called "dirty code".

If you have to deal with similar code in large applications, then this is just a nightmare. Unfortunately, this happens quite often.

Here is one case from life. I edited the React hook code from one application and at that moment they sent me a task. It was to implement in the application the ability to display additional information about doctors. This information should have been shown to the patient who clicks on the doctor’s profile picture. It was necessary to take it from the table, it had to get to the client after processing the next request to the server.

This was not a difficult task, the main problem I encountered was that I had to spend too much time finding where exactly what I needed was located in the project code.

I looked in the code according to the info, dataToSend, dataObject, and others who, in my view, connected with the data received from the server. After 5-10 minutes, I managed to find the code responsible for working with the data I needed. The object in which they found themselves was namedpaymentObject. In my opinion, an object related to payments may contain something like a CVV code, credit card number, payer zip code, and other similar information. The object I discovered had 11 properties. Only three of them were related to payments: payment method, payment profile identifier and a list of coupon codes.

The situation did not improve either because I had to make changes to this object that were required to solve the task before me.

In short, it is recommended that you refrain from using obscure names for functions and variables. Here is an example code in which the function name notifydoes not reveal its meaning:

import React from 'react'
class App extends React.Component {
  state = { data: null }
  // Кого уведомляем-то?
  notify = () => {
    if (this.props.user.loaded) {
      if (this.props.user.profileIsReady) {
        toast.alert(
          'You are not approved. Please come back in 15 minutes or you will be deleted.',
          {
            position: 'bottom-right',
            timeout: 15000,
          },
        )
      }
    }
  }
  render() {
    return this.props.render({
      ...this.state,
      notify: this.notify,
    })
  }
}
export default App

4. Do not change data structures or types in already configured application data streams


One of the biggest mistakes I've ever made was changing the data structure in an already configured application data stream. The new data structure would bring a huge performance boost, since it used fast methods to search for data in objects stored in memory, instead of iterating over arrays. But it was too late.

I ask you not to do this. Perhaps something like this can only be afforded to someone who knows exactly what parts of the application this can affect.

What are the consequences of such a step? For example, if something was first an array, and then became an object, this can disrupt the operation of many parts of the application. I made a huge mistake believing that I was able to remember all the places in the code that could be affected by a change in the presentation of structured data. However, in such cases, there is always some piece of code that is affected by the change, and which no one remembers.

5. Use snippets


I used to be a fan of the Atom editor, but switched to VS Code due to the fact that this editor was incredibly fast compared to Atom. And he, at his speed, supports a huge number of different possibilities.

If you also use VS Code, I recommend installing the Project Snippets extension . This extension allows the programmer to create custom snippets for each workspace used in a project. This extension works in the same way as the Use Snippets mechanism built into VS Code. The difference is that when working with Project Snippets, a folder is created in the project .vscode/snippets/. It looks like the following figure.


The contents of the .vscode / snippets / folder

6. Create unit, end-to-end and integration tests


As the application grows in size, it becomes more frightening for the programmer to edit code that is not covered by tests. For example, it may happen that someone edited the code stored in src/x/y/z/, and decided to send it to production. If at the same time the changes made affect those parts of the project that the programmer did not think about, then everything may end in an error that a real user will encounter. If there are tests in the project, the programmer will know about the error long before the code gets into production.

7. Brainstorm


Programmers, in the process of introducing new features into projects, often refuse to brainstorm. This happens because such an activity is not related to writing code. This happens especially often when very little time is allocated for the task.

And why, by the way, do you have to brainstorm during the development of applications?

The fact is that the more complex the application becomes, the more attention programmers have to pay to its individual parts. Brainstorming helps reduce the time it takes to refactor code. After they are held, the programmer is armed with knowledge of what may go wrong during the completion of the project. Often, programmers, while developing an application, do not even bother to think at least a little about how to do everything in the best way.

That is why brainstorming is very important. During such an event, the programmer can consider the architecture of the code, think about how to make the necessary changes to the program, trace the life cycle of these changes, and create a strategy for working with them. It’s not worth it to make it a habit to keep all plans exclusively in your own head. This is what programmers do who are overly confident. But remembering absolutely everything is simply impossible. And, as soon as something is done wrong, problems will appear one after another. This is the principle of dominoes in action.

Brainstorming is also useful in teams. For example, if in the course of work someone runs into a problem, he may turn to the materials of the brainstorming session, since the problem that arose with him could well have already been considered. The notes that are made during the brainstorming session may well play the role of a plan for solving the problem. This plan allows you to clearly assess the amount of work performed.

8. Create application mockups


If you are going to start developing the application, you need to make a decision about how it will look and how users will interact with it. This means that you will need to create an application layout. You can use various tools for this.

Moqups is one of the app mockup tools I often hear about. This is a fast tool created using HTML5 and JavaScript and does not impose special requirements on the system.

Creating a mock application greatly simplifies and speeds up the development process. The layout gives the developer information about the relationship between the individual parts of the application, and about what kind of data will be displayed on its pages.

9. Plan data flow in applications


Almost every component of your application will be associated with some data. Some components will use their own data sources, but most components receive data from entities above them in the component hierarchy. For those parts of the application in which the same data is shared by several components, it is useful to provide some centralized information storage located at the top level of the hierarchy. It is in such situations that the Redux library can provide invaluable assistance to the developer .

I recommend that while working on the application, draw up a diagram showing the ways in which the data moves in this application. This will help in creating a clear application model, moreover, we are talking about the code and the perception of the application by the programmer. Such a model will help, in addition, in the creation of reducers.

10. Use data access features


As the size of the application grows, so does the number of its components. And when the number of components grows, the same thing happens with the frequency of use of selectors (react-redux ^ v7.1) or mapStateToProps. Suppose you find that your components or hooks often access state fragments in different parts of the application using a construction like useSelector((state) => state.app.user.profile.demographics.languages.main). If so, that means you need to think about creating data access functions. Files with such functions should be stored in a public place from which components and hooks can import them. Similar functions can be filters, parsers, or any other functions for data transformation.

Here are some examples.

For example, the src/accessorsfollowing code may be present:

export const getMainLanguages = (state) =>
  state.app.user.profile.demographics.languages.main

Here is a version using connectthat may be located along the way src/components/ViewUserLanguages:

import React from 'react'
import { connect } from 'react-redux'
import { getMainLanguages } from '../accessors'
const ViewUserLanguages = ({ mainLanguages }) => (
  
   

Good Morning.

   Here are your main languages:    
   {mainLanguages.map((lang) => (      
{lang}
   ))}  
) export default connect((state) => ({  mainLanguages: getMainLanguages(state), }))(ViewUserLanguages)

Here is the version in which it is useSelectorlocated at src/components/ViewUserLanguages:

import React from 'react'
import { useSelector } from 'react-redux'
import { getMainLanguages } from '../accessors'
const ViewUserLanguages = ({ mainLanguages }) => {
  const mainLanguages = useSelector(getMainLanguages)
  return (
    
     

Good Morning.

     Here are your main languages:      
     {mainLanguages.map((lang) => (        
{lang}
     ))}    
 ) } export default ViewUserLanguages

In addition, strive to ensure that such functions are immutable, devoid of side effects. Find out why I give such a recommendation here .

11. Control the flow of data in properties using destructuring and spread syntax


What are the advantages of using construction props.somethingover construction something?

Here's what it looks like without using destructuring:

const Display = (props) => 
{props.something}

Here is the same, but with the use of destructuring:

const Display = ({ something }) => 
{something}

Using destructuring improves code readability. But this does not limit his positive impact on the project. Using destructuring, the programmer is forced to make decisions about what exactly the component receives and what exactly it outputs. This eliminates the need for anyone who has to edit someone else’s code to look at every line of the method renderin search of all the properties that the component uses.

In addition, this approach provides a useful opportunity to set default property values. This is done at the very beginning of the component code and eliminates the need to write additional code in the component body:

const Display = ({ something = 'apple' }) => 
{something}

You may have seen something like the following example before:

const Display = (props) => (
  
    {' '}
    // перенаправление других свойств компоненту Agenda
    

Today is {props.date}

   
   
     

▍Here your list of todos:

     {props.children}    
 
)

Such constructions are not easy to read, but this is not their only problem. So, there is an error. If the application also displays child components, it is props.childrendisplayed on the screen twice. If the work on the project is carried out in a team and the team members are not careful enough, the likelihood of such errors is quite high.

If you destruct properties instead, the component code will be clearer, and the likelihood of errors will decrease:

const Display = ({ children, date, ...props }) => (
  
    {' '}
    // перенаправление других свойств компоненту Agenda
    

Today is {date}

   
   
     

▍Here your list of todos:

     {children}    
 
)

Summary


In this article, we reviewed 12 recommendations for those who are developing React applications using Redux. We hope you find something here that is useful to you.

Dear readers! What tips would you add to the ones in this article?




Also popular now: