
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 index
es 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.