As a React developer, I’ve encountered numerous challenges and learned from my mistakes. These experiences have taught me valuable lessons about improving development processes.
React is a powerful library for building dynamic user interfaces, but its flexibility can sometimes lead to common pitfalls, especially for those new to the framework.
In this guide, we’ll cover the top 5 mistakes developers make in React and actionable tips to write better, more efficient code.
Let’s dive in!
1. Mutating State
To illustrate this issue, let’s look at a component designed to display a list of items and allow users to add or remove them:
import { useState } from "react";
const Home = () => {
const [items, setItems] = useState(['item1', 'item2']);
const [itemToAdd, setItemToAdd] = useState('');
function wrongHandleAddItem(item) {
items.push(item);
setItems(items);
}
function goodHandleAddItem(item) {
if (item.length === 0) return;
const newArray = [...items, item];
setItems(newArray);
setItemToAdd('');
}
function removeItem(item) {
const itemIndex = items.indexOf(item);
if (itemIndex !== -1) {
const newArray = [...items];
newArray.splice(itemIndex, 1);
setItems(newArray);
}
}
return (
<div style={{ padding: '3rem', maxWidth: '12rem' }}>
<div>
{items.map((item, key) => (
<div key={key} style={{ marginTop: '0.5rem', display: 'flex', justifyContent: 'space-between' }}>
{item}
<button onClick={() => removeItem(item)}>-</button>
</div>
))}
</div>
<div style={{ marginTop: '1rem' }}>
<input value={itemToAdd} onChange={(e) => setItemToAdd(e.target.value)} />
<button onClick={() => wrongHandleAddItem(itemToAdd)} style={{ marginLeft: '1rem' }}>+</button>
</div>
</div>
);
};
export default Home;
The wrongHandleAddItem method tries to modify the state directly:
function wrongHandleAddItem(item) {
items.push(item);
setItems(items);
}
This approach violates a core React principle: do not mutate the state directly. React depends on the identity of state variables to determine when to re-render components. Mutating an array does not change its identity, so React fails to recognize the update.
To fix this, create a new array using the spread operator … and update the state:
function goodHandleAddItem(item) {
if (item.length === 0) return;
const newArray = [...items, item];
setItems(newArray);
setItemToAdd('');
}
This ensures React recognizes the state update and re-renders the component.
2. Not Generating Unique Keys for Lists
React requires a unique key for each list item to track changes efficiently. A common mistake is neglecting to provide a key when rendering lists:
items.map((item) => (
<div>
{item}
<button onClick={() => removeItem(item)}>-</button>
</div>
));
This will trigger a warning in the console. A simple fix is to add an index as the key:
items.map((item, index) => (
<div key={index}>
{item}
<button onClick={() => removeItem(item)}>-</button>
</div>
));
However, using the index isn’t always ideal for dynamic lists, as it can lead to rendering issues. A more robust solution is to generate unique IDs, for example, using crypto.randomUUID():
const newItem = { id: crypto.randomUUID(), value: item };
const newArray = [...items, newItem];
setItems(newArray);
And render it like this:
items.map((item) => (
<div key={item.id}>
{item.value}
<button onClick={() => removeItem(item)}>-</button>
</div>
));
This ensures React handles list updates accurately.
3. Using async Directly in useEffect
A common scenario is fetching data on component mount using useEffect. A common mistake is trying to use async directly in the useEffect callback:
useEffect(async () => {
const url = "/api/data";
const res = await fetch(url);
const data = await res.json();
console.log(data);
}, []);
This will throw an error because useEffect does not accept asynchronous functions. The correct approach is to define an async function inside useEffect and call it:
useEffect(() => {
async function fetchData() {
const url = "/api/data";
const res = await fetch(url);
const data = await res.json();
console.log(data);
}
fetchData();
}, []);
4. Accessing State Before Re-render
React’s state updates are asynchronous, meaning they don’t immediately update the state variable. Consider this example:
function goodHandleAddItem(item) {
const newArray = [...items, item];
setItems(newArray);
console.log(items); // This logs the old value
}
Here, items still holds the old value because the update hasn’t been applied yet. Instead, use the new array directly:
function goodHandleAddItem(item) {
const newArray = [...items, item];
setItems(newArray);
console.log(newArray); // Logs the correct value
}
5. Misusing State in Consecutive Updates
State updates in React may be batched, causing issues when relying on the current state in consecutive updates:
setCount(count + 1);
setCount(count + 1);
Both calls use the same initial value of count, leading to an incorrect final state. The solution is to use the updater function, which takes the previous state as an argument:
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
This ensures the updates are applied correctly, even in batched operations.
Conclusion
Avoiding these common mistakes will help you build more reliable, maintainable React applications and enhance your understanding of React’s core principles.
Happy coding!
If you found this guide helpful, let me know in the comments. Whether you’re a beginner or an experienced React developer, your feedback is always appreciated!