Search justacoding.blog. [Enter] to search. Click anywhere to close.

November 22nd, 2021

10 Common React Mistakes And How To Avoid Them

When first starting out with a new language, framework or tool — it’s generally considered a good idea to look at some of the more common pitfalls that other developers are experiencing.

You can then try to actively avoid making these same mistakes for yourself.

Here’s a list of 10 of the more common React mistakes in this regard.

Some of the mistakes are fairly trivial and easy to fix or avoid, such as using class instead of className.

Others are a bit more nuanced, especially to the uninitiated, such as inadvertently mutating state directly instead of doing it the “React” way.

Let’s get to the mistakes, in no particular order!

1. Forgetting to cleanup component side-effects

Certain side-effects should be handled appropriately once the component unmounts.

For instance, what would happen if your component unmounts whilst it’s making a network request?

What would happen if your component’s state is meant to be updated after a given period of time, but the component unmounts during this period of time?

Look at this example:

const App = () => {
  const [message, setMessage] = useState("Waiting...")

  // 15 seconds after mount, perform a mutation in state...
  useEffect(() => {
    setTimeout(() => {
      setMessage("Message has been set")
    }, 15000)
  }, [])

  return (
    <h3>Message: {message}</h3>
  )
}

In this case, React would throw an error, like Can't perform a state update on an unmounted component.

This is a common React mistake, and it can definitely be a bit puzzling at first to the uninitiated.

As the message implies, setMessage would be invoked — but on a component that has been unmounted. This is clearly problematic, trying to update state on an unmounted component is a clear contradiction.

The way around this is to ensure that the necessary cleanup takes place once the component unmounts:

useEffect(() => {
  let timeoutId = setTimeout(() => {
    setMessage("Message has been set")
  }, 15000)
  
  // we need this cleanup function...
  return () => {
    clearTimeout(timeoutId)
  }
}, [])

Returning a function from the relevant useEffect is an optional mechanism used to specify some sort of cleanup functionality.

In this case, we’re cancelling (or clearing) the timer. This ensures that message isn’t mutated after unmount, and thus the no-op of updating an unmounted component’s state doesn’t cause any issues.

In short, if the component unmounts at some point between mounting and this 15 second timeout; the scenario will be handled gracefully.

As stated, specifying cleanup behaviour is optional in these useEffect hooks. However, it’s well worth considering potential scenarios that may require this cleanup on a per-component basis during your application’s development.

2. Using class instead of className

This one is short and sweet, and we can attribute this very common React mistake to muscle memory alone.

Writing:

<div class="myClass"></div>

Instead of:

<div className="myClass"></div>

This is a minor mistake that I’m sure the vast majority of React developers have made at one time or another!

3. Not supplying keys to items in a list

Here’s another common one.

Any time you output a list of any kind within your JSX code, you’ll need to supply a key prop to each child within this list:

const App = () => {
  const [fruits, setFruits] = useState(["apple", "orange", "pear"])

  return (
    <ul>
      {fruits.map((fruit, index) => {
        return <li key={index}>{fruit}</li>
      })}
    </ul>
  )
}

Without this key you’ll get a warning in the console: Warning: Each child in a list should have a unique "key" prop.

The reason this key is required in each case is so that React can handle performance optimizations under the hood.

It’s for the same reason that keys should be unique across each child within a list.

Typically, the key would be provided based on a unique identifier within your data structure, such as an ID. If you know this ID will always be unique per each item, this is definitely the recommended approach.

However, if you don’t have an identifier that is guaranteed to be unique per each item, you can use the item’s index in the list (as demonstrated above).

The caveat here is if you’re going to implement some kind of re-ordering mechanism. If this is the case, you’ll likely want to avoid using the indexes as per the React Lists and Keys documentation.

4. Mutating state directly

This React mistake is one of the more subtle ones, and it’s rife among beginners in particular.

It can play havoc with your application, producing (potentially) hard to recreate bugs and frustrating (unwanted) side-effects

Let’s look at an example pf state mutation, something you should most definitely avoid:

const [person, setPerson] = useState({ name: "Tom", age: 33 })

person.age = 34

Here, in this example, we are setting age on our person object directly — and not using the appropriate method (in this case, setPerson).

This is problematic for a number of reasons:

Firstly, these set state methods don’t always mutate the relevant piece of state immediately. They are asynchronous in nature, meaning it may be some time before the operation actually takes place.

To coincide with this, the set state operations may also be batched (React handles this under the hood in an attempt to optimize performance).

The behaviour in the example provided contradicts these two points entirely. The behaviour in the example is synchronous, not asynchronous. So you may be able to imagine why these two concepts don’t play nicely together in this context.

In short, it’s possible to directly set the age directly — only for that update to be overridden by a subsequent setPerson call later on. This can lead to a whole host of buggy side-effects and complications which can definitely be frustrating to debug as your application grows in size.

The solution is to simply always use the relevant state-setting function in each case:

setPerson({ ...person, age: 34 })

You can see that I’m using the spread operator here to ensure that I do not modify the person object at all.

Instead, a “new” person object is passed to setPerson and the state update is as clean and as concise as it can be, and we don’t need to worry about any of the aforementioned side-effects!

5. Using a lot of inline styles

The best way to style React components is hotly debated, and there are various different avenues that one can go down in this regard.

However, using a lot of inline styles is almost certainly a mistake. Inline styles look a little bit like this:

<div style={{ backgroundColor: "red", fontSize: "2em" }}>

So simply put, these are effectively CSS styles applied to the element directly via the style prop.

Inline styles do have their usages — but relying on them too much will undoubtedly make your components unwieldy and hard to manage and maintain in the long run.

It’s much better to opt for a more scalable approach. There’s nothing wrong with using regular CSS in this regard, but a solution like styled-components would be considered the most optimal approach by many.

6. Getting caught out by conditional rendering issues

It’s easy to fall prey to conditional rendering issues like this one:

const showCount = true
let count = 0

// in your render method...
{showCount && count && <h5>Your age is {count}</h5>}

React mistakes like this one lead to unexpected output. You may expect the output here to be Your age is 0, for example.

However, due to the way JavaScript evaluates these kind of expressions — instead, the only output will be 0.

That’s because count is evaluating to false here. The intention of the expression was perhaps just to check if count is actually set. This is erroneous and leads to the issue described.

React returns the falsy expression (in this case count) instead of the desired h5.

Note, this isn’t the case if count is anything other than a falsy expression:

const showCount = true
let count = 15

// in your render method...
{showCount && count && <h5>Your age is {count}</h5>}

Both showCount and count are truthy, now — so the output will be Your age is 15.

Check out this article for a more in-depth look at this topic. The article is centered around the or operator, but applies equally to the and operator depicted in this example.

7. Not supplying dependencies to the useEffect hook

If your useEffect is initialized as follows:

useEffect(() => {
  console.log("Count has changed: " + count)
})

count will be logged to the console on every render, regardless of it the value (count) has actually changed or not. This is because no second parameter (ie. the dependencies array) has been passed.

A more optimal approach in this case would be to only execute the functionality defined in this hook when count has actually changed:

useEffect(() => {
  console.log("Count has changed: " + count)
}, [count])

Now this useEffect will only fire if count has actually changed since the last render.

As you can see, it’s simply a matter of supplying the second parameter to the useEffect hook. This parameter should be an array of dependencies required by the hook.

8. Inadvertently using stale state

As far as React mistakes go, this can be one of the more frustrating ones to deal with at first.

It’s best demonstrated with a simple example.

Assume you have a count integer in state and you want to increment it (by 1) a specific number of times:

const App = () => {
  const [count, setCount] = useState(0)
  
  const incrementCount = () => {
    for (let i = 0; i < 3; i++) {
      // this will use state state...
      setCount(count + 1)
    }
  }
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementCount}>Increment count</button>
    </div>
  )
}

You may expect the final count value to be 3 — but this is not the case.

In this example, the count we are passing to setCount isn’t referring to the current value of count — but an old value, instead. In React, this is what is known as stale state.

We’re calling setCount 3 times in quick succession via the foor loop, but the operation to set state is actually asynchronous. It’s very important to remember this.

This means that the count variable hasn’t actually been updated in state for each iteration of the loop. So we’re incrementing an old value, not the current one, each time.

In short, every time this for loop runs – React thinks count is equal to 0. It hasn’t had chance to set the refreshed or modified value, yet.

This can be easily rectified by doing the following:

setCount(previousCount => previousCount + 1)

You can pass a function to setCount, instead, which allows you to access the previous value and subsequently perform your update based on this.

Now, count refers correctly to the expected value, not the stale one.

So it’s incremented correctly in each iteration of the for loop, resulting in a final count of 3.

This type of issue, and in fact most React mistakes that revolve around the incorrect usage of state, can be a headache to resolve. For this reason, keeping in mind the asynchronous nature of setting state in React will pay dividends in the end.

9. Confusing types when passing props (ie. strings with numbers)

Generally speaking, many mistakes can occur through the misuse of types.

This is definitely true when working in React, specifically when passing props to a component:

const ExampleComponent = ({ number }) => {
  return (
    <h1>{number === 10 ? "It's 10" : "It's not 10" }</h1>
  )	
}

const App = () => {
  return (
    <ExampleComponent number={"10"} />
  )
}

In this example, we’re passing a string where the receiving component is expecting a number.

As we’re doing a strict comparison this will lead to issues. Of course, as the comparison is strict, both operands must be of the same type, which isn’t the case here.

Although this is a simple example, it’s surprisingly easy to fall into this kind of trap. These errors are fairly trivial and easy to fix once caught; but still a very common occurrence as far as React mistakes go.

Of course, one could (and should) use TypeScript to nullify a massive amount of issues in general (including this one). But that is a topic for another day.

In terms of a workaround to this particular issue, you could use !isNaN or something similar to verify that the prop is what you are really expecting. You could also use loose comparisons inside (==) but that may not be optimal.

All sorts of weird behaviour can happen if you inadvertently mix strings and numbers in JavaScript! Concatenating is one example of this, whereby you + would join a string/number combination instead of adding the operands together.

10. Creating god components

Creating components that are too big and too complex is certainly one of the classic React mistakes, particular among beginners.

At first, if your application works — it’s only natural to assume that your components are fine and fit for purpose.

However, you’ll soon realize that components that are too complex become hard to maintain in the long run. This ultimately makes your application harder to manage and harder to scale. The code becomes unwieldy and needlessly difficult to work with.

Instead, focus on small, modular components that have a specific job.

Think of components like building blocks.

Where possible, they should be reusable. It should be straightforward to drop the components into other positions within your application, and they should still function as expected or required.

In conclusion

Thanks for reading, I hope you’ve enjoyed this article!

We’ve covered a few of the more common React mistakes. Some are trivial; others are a bit more nuanced and can be a bit harder to spot.

Similarly — I have an article that is more geared towards common JavaScript mistakes in general. Please feel free to check that one out.

Thanks for reading!

Have any questions? Ping me over at @justacodingblog
← Back to blog