12 Things NOT To Do When Building React Apps With Redux

Christopher T.

June 8th, 2019

Share This Post

When you're building out a react application, small projects can often be a little more flexible than large projects when it comes to code architecture. While there's nothing really wrong with building a small application with best practices intended for larger applications, it may be unnecessary to apply all the big decisions. The smaller the application is, the more it becomes "okay" to be lazy.

However, some of the best practices in this article are recommended to be applied with any sized react applications.

If you've never had experience building an application in production, then this article can help you prepare for the next large scaled application you build. The worst thing that could happen to you is building an application at your job and realizing that you have to refactor a lot of the code architecture to be more scalable and maintainable---especially if you're missing unit tests!

Trust me. I've been there. I was given several tasks to complete ____ by ____. At first, I thought everything was going smooth and perfect. I thought that just because my web application worked and still stayed fast that I was doing an excellent job in developing and maintaining my code. I knew how to use redux and make the UI components interact normally. Reducers and actions were an easy concept to me. I felt invincible.

Until The Future Creeped Up.

A couple months and 15+ features later, things were becoming out of control. My code utilizing redux was no longer easy to maintain.

"Why?" you may ask.

"Weren't you invincible?"

Well, I thought so too. It was a ticking time bomb waiting for a disaster to happen. Redux has the amazing ability to keep things maintainable if used correctly in a larger sized project.

Read along to find out what not to do if you're planning on building scalable react web applications.

1. Placing Actions and Constants Into One Place

You might see some redux tutorials out there placing constants and all of the actions into one place. However, it can quickly become a hassle as the app gets bigger. Constants should be in a separate location like ./src/constants so that there is one place to search and not in multiple locations.

In addition, its definitely okay to create a separate actions file representing what or how it is going to be used with, encapsulating directly related actions. If you were building a new arcade/RPG game introducing a warrior, sorceress and archer class, it will be a lot more maintainable if you placed your actions like this:

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

Rather than something like:

src/actions/classes.js

If the app gets really big, its probably a better approach to go with something like this:

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

A bigger picture that includes other actions using that approach would then look like this if we were to separate them as demonstrated:

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

An example of how the sorceress actions would look like:

src/actions/sorceress/skills

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,
})

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 }) => {
    // Grab the slot in our equipment screen to grab the staff reference
    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 why we do this is because there will always be new features to add, and you must prepare for them as your files become more bloated!

It may feel redundant in the beginning but these approaches will begin to shine the more the project gets larger.

2. Placing Reducers Into One Place

When my reducers start looking like this:

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
  }
}

This can obviously create a big mess very quickly, so its best to keep your state structure simple and flattened as possible or try to compose all of your reducers instead.

A neat trick is to create a higher order reducer that generates reducers, mapping each wrapped reducer to an object mapping from action types to handlers.

3. Naming Your Variables Poorly

Naming your variables sounds like a simple no-brainer, but it can actually be one of the most difficult things to be good at when writing code.

It's essentially a clean coding practice... and the reason this term even exists is because its so important to apply in practice. Poorly naming your variables is a good way to let your team members and your future self suffer!.

Have you ever tried to edit someones code and end up having a hard time trying to understand what the code is trying to do? Have you ever run someone's code and it turned out to function differently than what you had expected?

I'm willing to bet that the author of the code was applying dirty code practices.

The worst situation to be in in this scenario is having to go through this in a large application where it's commonly happening in multiple areas.

Let me give you a real life experience of a situation that I was in:

I was editing an existing react hook from the app code when I received a task to add and show additional information about each doctor when a patient clicks on them. When they choose (click) on a doctor, the doctors information gets picked up from the table row so that they can attach the information onto the next request to the backend.

Everything was going fine, except I was unnecessarily spending more time than I should have when I was searching for where that part was in the code.

At this point in my head I was looking for words like info, dataToSend, dataObject, or anything relating to the data that was just gathered. 5-10 minutes later I found the part that implemented this flow, and the object it was placed in was named paymentObject. When I think of payment objects, I think of CVV, last 4 digits, zip code, etc. Out of the 11 properties, only three were related to paying: charge method, payment profile id, and coupons.

And it didn't help that it was way too awkward trying to blend in my changes afterwards.

In short, try to refrain from naming your functions or variables like this:

import React from 'react'

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

  // Notify what?
  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. Changing The Data/Type Structure Mid-Way

One of the biggest mistakes I've ever made was to change the data/type structure of something during an already-established flow of the app. The new data structure would have been a huge boost in performance as it utilized object lookups to snatch data in memory instead of mapping over arrays. But it was too late.

Please don't do this unless you really know all of the areas that are going to be affected.

What are some of the consequences?

If something changes from an array to an object, multiple areas of the app are at risk of being unfunctional. I made the biggest mistake to think that I had every part of the app planned out in mind that would be affected by a structured data change, but there will always be that one spot left behind that was missed.

6. Developing Without Using Snippets

I used to be an Atom fan, but I switched to VScode because of how quick it was compared to Atom--while still supporting tons and tons of features without a noticeable loss of speed.

If you're using VSCode, I highly recommend you to download an extension called Project Snippets. This extension lets you declare custom snippets for each workspace for you to use for that project. It works exactly like the built in User Snippets feature that comes in vscode by default, except you create a .vscode/snippets/ folder inside your project like so:

vscode project snippets workspace react project

7. Ignoring Unit/E2E/Integration Tests

As the app grows larger it becomes scarier to edit existing code without any sort of tests in place. You might end up editing a file located at src/x/y/z/ and decide to push the changes to production, however if the change affects another part of the app and you didn't notice, the bug will stay there until a real user catches it while they are browsing through your pages since you won't have any tests to alert you beforehand.

8. Skipping the Brainstorming Phase

Developers often skip the brainstorming phase because they aren't coding, especially when they're given a week to develop a feature. However, from experience this is the most important step and will save you and your team a lot of time in the future.

Why Bother Brainstorming?

The more complex an application, the more developers have to manage certain parts of the app. Brainstorming helps eliminate the amount of times you refactor code, because you already planned out what could go wrong. Often times developers are hardly given the time to sit back and apply all the neat practices to enhancing the app further.

This is why brainstorming is important. You think of all the code design in architecture and the enhancements you'd need so you can tackle them all from the start with a strategical approach. Don't fall into the habit of being overly confident and planning it all in your head. If you do, you won't be able to remember everything. Once you do something wrong, more things will go wrong like a domino effect.

Brainstorming will make it a little easier for your team as well. If one of them ever gets stuck on a task, they can refer to the brainstorming they had from the beginning and it's possibly already there.

The notes you take in brainstorming ideas can also serve you and your team as an agenda and help in easily providing a consistent sense of your current progress when developing the application.

9. Not Determining the UI Components Beforehand

If you're going to start building out your app, you should decide on how you want your app to look and feel. Several tools are available to help you with creating your own mockups.

A mockup tool I hear about often is Moqups. It's fast, does not require any plugins and is built in HTML5 and JavaScript.

Doing this step is very helpful in giving you both the information and data that is going to be on the pages you create. Developing your app will be much more of a breeze.

10. Not Planning The Data Flow

Almost every component of your application will be associated with some kind of data. Some will use its own source of data, but most of them will be provided from a location higher up in the tree. For parts of your application where data is shared with more than one component, it's a good idea to make that data available higher up in the tree where it will act as a centralized state tree. This is where the power of redux comes to the rescue :)

I advise to make a list of how the data is going to flow throughout your application. This will help you create a more firm mental and written models of your app. Based on these values, your reducer should easily be established from it.

11. Not Utilizing Accessor Functions

When the app gets bigger, so does the amount of components. And when the number of components increase, so does the number of times you use selectors (react-redux ^v7.1) or mapStateToProps. If you find your components or hooks often selecting state slices like useSelector((state) => state.app.user.profile.demographics.languages.main) in several parts of your application, it's time to start thinking about creating accessor functions in a shared location where the components/hooks can import and use from. These accessor functions can be filterers, parsers, or any other data transformation functions.

Here are some examples:

src/accessors

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

connect version

src/components/ViewUserLanguages

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

const ViewUserLanguages = ({ mainLanguages }) => (
  <div>
    <h1>Good Morning.</h1>
    <small>Here are your main languages:</small>
    <hr />
    {mainLanguages.map((lang) => (
      <div>{lang}</div>
    ))}
  </div>
)

export default connect((state) => ({
  mainLanguages: getMainLanguages(state),
}))(ViewUserLanguages)

useSelector version

src/components/ViewUserLanguages

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

const ViewUserLanguages = ({ mainLanguages }) => {
  const mainLanguages = useSelector(getMainLanguages)

  return (
    <div>
      <h1>Good Morning.</h1>
      <small>Here are your main languages:</small>
      <hr />
      {mainLanguages.map((lang) => (
        <div>{lang}</div>
      ))}
    </div>
  )
}

export default ViewUserLanguages

It's also very important to keep these functions immutable--free of side effects. To find out why, click here.

12. Not Controlling The Flow In Props With Destructuring And Spread Attributes

What are the benefits in using props.something versus something?

Without destructuring

const Display = (props) => <div>{props.something}</div>

With destructuring

const Display = ({ something }) => <div>{something}</div>

With destructuring, you're not only making your code more readable for yourself and others but you're also making straight forward decisions on what goes in and what goes out. When other developers edit your code in the future, they don't have to scan through each line of code in your render method to find all of the props that the component is using.

You also benefit from the ability to declare a default props right from the beginning without having to add any more lines of code:

const Display = ({ something = 'apple' }) => <div>{something}</div>

You might have seen something like this before:

const Display = (props) => (
  <Agenda {...props}>
    {' '}
    // forward other props to Agenda
    <h2>Today is {props.date}</h2>
    <hr />
    <div>
      <h3>Here your list of todos:</h3>
      {props.children}
    </div>
  </Agenda>
)

This is not only a little bit harder to read, but there is also an unintentional bug occurring in this component. If App also renders children, you have props.children being rendered twice. This causes duplicates. When working with a team of developers other than yourself there are chances that these mistakes can happen by accident especially if they aren't careful enough.

By destructuring props instead, the component can get straight to the point and decrease chances of unwanted bugs:

const Display = ({ children, date, ...props }) => (
  <Agenda {...props}>
    {' '}
    // forward other props to Agenda
    <h2>Today is {date}</h2>
    <hr />
    <div>
      <h3>Here your list of todos:</h3>
      {children}
    </div>
  </Agenda>
)

Conclusion

That is all, folks! I hope these tips helped you and shoot me a comment/message for any questions and/or concerns! See you next time!


Tags

javascript
react
redux
scalable
best practice
anti pattern
vscode
naming

Subscribe to the Newsletter
Get continuous updates
© jsmanifest 2021