[Translation] 11 tips for those who use Redux when developing React-applications

[Translation] 11 tips for those who use Redux when developing React-applications


When it comes to developing React-applications, then in terms of code architecture, small projects are often more flexible than large ones. There is nothing wrong with creating such projects using practical recommendations aimed at larger applications. But all this, in the case of small projects, may simply be unnecessary. The smaller the application, the more “condescending” it is to use simple solutions in it, perhaps non-optimal, but not time-consuming to implement them.



Despite this, I would like to note that some recommendations that will be given in this material are aimed at React-applications of any scale.

If you have never created a production application, then this article can help you prepare for the development of large-scale solutions. Something like this may well be one of your next projects. The worst thing that can happen to a programmer is when he is working on a project and understands that he needs to refactor large amounts of code to improve the scalability and maintainability of the application. It 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 such situations. So, he got a few tasks that had to be solved for a certain time. At first, he thought he did everything perfectly. 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 make normal interactions between user interface components. It seemed to him that he deeply understood the concepts of reducer and action. He felt invulnerable.

But then the future crept in.

After a couple of months of work on the application, more than 15 new features were added to it. After that, the project went out of control. The code that used the Redux library has become very hard to maintain. Why did it happen so? Didn’t it at first appear that the project was expecting a long and cloudless life?

The author of the article says that, asking such questions, he realized that he had himself planted a time bomb in the project.

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

Here will be given 12 tips for those who want to develop scalable React-applications using Redux.

1. Do not place the code of actions and constants in one place


You might have come across some Redux guides 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 must be stored separately, for example, in ./src/constants . As a result, to search for constants you will have to look only into one folder, and not into several.

In addition, it seems completely normal to create separate files that store actions. Such files encapsulate actions directly related to each other. Actions in the same file, for example, may have similarities in terms of what and how they are used.

Suppose you develop an arcade or role-playing game and create the classes warrior (warrior), sorceress (sorceress) and archer (archer). In such a situation, a high level of code maintenance can be achieved by organizing 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 perhaps it would be even better to use approximately the following structure of breaking the code into files:

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

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

  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 an action from a src/actions/sorceress/skills file looks like for a sorceress object:

  import {CAST_FIRE_TORNADO, CAST_LIGHTNING_BOLT} from '../constants/sorceress'

 export const castFireTornado = (target) = & gt;  ({
  type: CAST_FIRE_TORNADO,
  target,
 })

 export const castLightningBolt = (target) = & gt;  ({
  type: CAST_LIGHTNING_BOLT,
  target,
 })  

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

  import * as consts from '../constants/sorceress'

 export const equipStaff = (staff, enhancements) = & gt;  {...}

 export const removeStaff = (staff) = & gt;  {...}

 export const upgradeStaff = (slot, enhancements) = & gt;  {
  return (dispatch, getState, {api}) = & gt;  {
//Refer to the slot on the uniform screen to get a link to the sorceress's staff
  const state = getState ()
  const currentEquipment = state.classes.sorceress.equipment.current
  const staff = currentEquipment [slot]
  const isMax = staff.level & gt; = 9
  if (isMax) {
  return
  }
  dispatch ({type: consts.UPGRADING_STAFF, slot})

  api.upgradeEquipment ({
  type: 'staff',
  id: currentEquipment.id,
  enhancements,
  })
  .then ((newStaff) = & gt; {
  dispatch ({type: consts.UPGRADED_STAFF, slot, staff: newStaff})
  })
  .catch ((error) = & gt; {
  dispatch ({type: consts.UPGRADE_STAFF_FAILED, error})
  })
  }
 }  

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

At the very beginning of the project, this may seem redundant. But the more the project becomes, the stronger the strength of this approach will be felt.

2. Do not place the code of viewers in one place


When I see that the code of my reducers turn into something like the one shown below, I realize that I need to change something.

  const equipmentReducers = (state, action) = & gt;  {
  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 a code, no doubt, can very quickly lead to a lot of confusion. Therefore, it is best to maintain the structure of work with the state in the simplest possible way, striving for the minimum level of their nesting. You can, instead, try to resort to the composition of reducer.

A useful technique in working with reduction gears can be the creation of a higher-order reduction gear that generates other reducers. Here you can read more about this.

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, in general, is related to practical recommendations for writing clean code. The reason why there is such a thing as “variable name” in general is that this aspect of code development plays a very important role in practice. Unsuccessful selection of variable names is a sure way to hurt yourself and your team members in the future.

Have you ever tried to edit someone else's code and did you encounter any difficulties in understanding what this code does? Have you ever run someone else’s program and find out that it doesn’t work as expected?

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

If you encounter such code in large applications, then this is just a nightmare. This, unfortunately, 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 consisted in realizing in the application the possibility of displaying additional information about doctors. This information should have been shown to a patient who clicks on the doctor's avatar. It was necessary to take it from the table, it should have been sent to the client after processing the next request to the server.

The task was simple, the main problem I encountered was that I had to spend too much time looking for exactly where I needed in the project code.

I searched the code for info , dataToSend , dataObject , and for others that, in my view, are associated with 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 appeared was named paymentObject . In my view, a payment-related object may contain something like a CVV code, credit card number, a payer's postal code, and other similar information. In the object I discovered there were 11 properties. Only three of them were related to payments: the method of payment, the identifier of the payment profile and the list of coupon codes.

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

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

  import React from 'react'

 class App extends React.Component {
  state = {data: null}

//Anyone notified?
  notify = () = & gt;  {
  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 ever made was a change in the data structure in an already configured application data stream. The new data structure would bring a huge performance boost, as it used fast data retrieval techniques in objects stored in memory, instead of iterating over arrays. But it was too late.

Please do not do this. Perhaps, only someone who absolutely knows about which parts of the application this may affect can afford something like this.

What are the consequences of such a step? For example, if something was first an array, and then became an object, it could disrupt many parts of the application. I made a huge mistake in believing that I could 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 code fragment 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, in comparison with Atom, was incredibly fast. And he, at his speed, supports a huge number of very different possibilities.

If you also use VS Code - I recommend installing the Project Snippets extension. This extension allows the programmer to create his own 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 in a project, they create a folder .vscode/snippets/. It looks like the following figure.


Content of the .vscode/snippets/
folder

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


As the size of the application grows, it becomes more and more terrible 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, in this case, the changes made affect those parts of the project that the programmer did not think about, everything could end in an error that the real user will encounter. If there are tests in the project, the programmer finds out about the error long before the code gets into production.

7. Brainstorm


Programmers, in the course of introducing new features into projects, often refuse to brainstorm. This is due to the fact that this activity is not related to writing code. Especially often this happens when the task is given quite a bit of time.

And why, by the way, do brainstorming work during application development at all?

The fact is that the more complex an application becomes, the more attention programmers have to pay to its individual parts.Brainstorming helps to reduce the time needed to refactor code. After they are held, the programmer turns out to be armed with the knowledge of what can go wrong during the finalization 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 code architecture, think about how to make the necessary changes to the program, trace the life cycle of these changes, create a strategy for working with them. It is not necessary to start the habit of keeping all plans solely in one’s own head. So do programmers who are overly confident. But to remember everything is simply impossible. And, once something is done wrong, problems will appear one after another. This is the principle of domino in action.

Brainstorming is also useful in teams. For example, if someone encounters a problem during the course of work, he may turn to brainstorming materials, since the problem he has had may well have been thought out. Notes that are made during brainstorming 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 layouts


If you are going to start developing the application, you need to decide how it will look and how users will interact with it. This means that you will need to create a mock app. For this you can use various tools.

Moqups is one of the tools for creating application layouts that I often hear about. This is a fast tool created by HTML5 and JavaScript tools and does not impose special system requirements.

Creating a layout of the application greatly simplifies and speeds up the development process. The layout gives the developer information about the relationship of the individual parts of the application, and what 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 kind of centralized storage of information located at the top level of the hierarchy. It is in such situations that the Redux library can provide invaluable assistance to the developer.

In the course of working on an application, I recommend drawing up a chart showing the ways in which data moves in this application. This will help in creating a clear model of the application, moreover, we are talking about the code, and the programmer's perception of the application. Such a model will also help in the creation of diluser.

10. Use data access functions


As the size of an 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 refer to state fragments in different parts of an application using constructs like useSelector ((state) = & gt; state.app.user.profile.demographics.languages.main) . If so, it means that 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, in src/accessors the following code may be present:

  export const getMainLanguages ​​= (state) = & gt;
  state.app.user.profile.demographics.languages.main  

Here is the version using connect , which can be located along the path src/components/ViewUserLanguages ​​:

  import React from 'react'
 import {connect} from 'react-redux'
 import {getMainLanguages} from '../accessors'

 const ViewUserLanguages ​​= ({mainLanguages}) = & gt;  (
  & lt; div & gt;
  & lt; h1 & gt; Good Morning. & lt;/h1 & gt;
  & lt; small & gt; Here are your main languages: & lt;/small & gt;
  & lt; hr/& gt;
  {mainLanguages.map ((lang) = & gt; (
  & lt; div & gt; {lang} & lt;/div & gt;
  ))}
  & lt;/div & gt;
 )

 export default connect ((state) = & gt; ({
  mainLanguages: getMainLanguages ​​(state),
 })) (ViewUserLanguages)  

Here is the version that uses useSelector , located at src/components/ViewUserLanguages ​​:

  import React from 'react'
 import {useSelector} from 'react-redux'
 import {getMainLanguages} from '../accessors'

 const ViewUserLanguages ​​= ({mainLanguages}) = & gt;  {
  const mainLanguages ​​= useSelector (getMainLanguages)

  return (
  & lt; div & gt;
  & lt; h1 & gt; Good Morning. & lt;/h1 & gt;
  & lt; small & gt; Here are your main languages: & lt;/small & gt;
  & lt; hr/& gt;
  {mainLanguages.map ((lang) = & gt; (
  & lt; div & gt; {lang} & lt;/div & gt;
  ))}
  & lt;/div & gt;
  )
 }

 export default ViewUserLanguages ​​ 

In addition, strive to ensure that such functions would be immutable, devoid of side effects. See here to find out why I’m giving this recommendation. .

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


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

Here’s how it looks without using destructuring:

  const Display = (props) = & gt;  & lt; div & gt; {props.something} & lt;/div & gt;  

Here is the same thing, but already using restructuring:

  const Display = ({something}) = & gt;  & lt; div & gt; {something} & lt;/div & gt;  

Using restructuring improves code readability. But his positive influence on the project is not limited to this. By applying destructuring, the programmer is forced to make decisions about what exactly the component receives, and what exactly it outputs. This saves those who have to edit someone else's code from having to look at every line of the render method in the search for 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 body of the component:

  const Display = ({something = 'apple'}) = & gt;  & lt; div & gt; {something} & lt;/div & gt;  

You may have seen something like the following example:

  const Display = (props) = & gt;  (
  & lt; Agenda {... props} & gt;
  {''}
//redirect other properties to the Agenda component
  & lt; h2 & gt; & lt; font color = "# 3AC1EF" & gt; Today is {props.date} & lt;/font & gt; & lt;/h2 & gt;
  & lt; hr/& gt;
  & lt; div & gt;
  & lt; h3 & gt; & lt; font color = "# 3AC1EF" & gt; Here your list of todos: & lt;/font & gt; & lt;/h3 & gt;
  {props.children}
  & lt;/div & gt;
  & lt;/Agenda & gt;
 )  

Such constructions are not easy to read, but this is not their only problem. So, there is an error here. If the application displays the child components, then props.children is displayed on the screen twice. If the project is being worked on as a team and the team members are not careful enough, the likelihood of such errors is quite high.

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

  const Display = ({children, date, ... props}) = & gt;  (
  & lt; Agenda {... props} & gt;
  {''}
//redirect other properties to the Agenda component
  & lt; h2 & gt; & lt; font color = "# 3AC1EF" & gt; Today is {date} & lt;/font & gt; & lt;/h2 & gt;
  & lt; hr/& gt;
  & lt; div & gt;
  & lt; h3 & gt; & lt; font color = "# 3AC1EF" & gt; Here your list of todos: & lt;/font & gt; & lt;/h3 & gt;
  {children}
  & lt;/div & gt;
  & lt;/Agenda & gt;
 )  

Totals


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

Dear readers! What tips would you add to what is shown in this article?

<< a>

Source text: [Translation] 11 tips for those who use Redux when developing React-applications