React is one of the most popular JavaScript libraries for building modern web applications. But even experienced developers make mistakes that can lead to performance issues, unexpected bugs, and poor user experience. In this guide, we’ll cover the most common mistakes developers make when using React—and how to avoid them like a pro.
- Mutating State Directly
❌ The Mistake:
Modifying state directly instead of using setState or the state setter function in hooks.
const [count, setCount] = useState(0);
// Incorrect ❌
count = count + 1;
✅ The Solution:
Always use the state updater function to ensure React re-renders properly.
setCount(count + 1);
Or if using previous state:
setCount(prevCount => prevCount + 1);
- Using useEffect Incorrectly
❌ The Mistake:
Forgetting the dependency array (causing infinite re-renders).
Adding too many dependencies (causing unnecessary renders).
useEffect(() => {
console.log("Runs on every render!"); // Infinite loop risk
});
✅ The Solution:
Use an empty dependency array [] to run the effect only once.
Only include necessary dependencies.
useEffect(() => {
console.log("Runs only once when the component mounts!");
}, []);
For fetching data:
useEffect(() => {
fetchData();
}, [someDependency]); // Runs only when someDependency changes
- Not Using Keys in Lists
❌ The Mistake:
Using array indexes as keys, which can cause incorrect reordering.
const items = ["Apple", "Banana", "Cherry"];
{items.map((item, index) => (
<li key={index}>{item}</li> // ❌ Bad practice
))}
✅ The Solution:
Use unique and stable IDs as keys.
const items = [{ id: 1, name: "Apple" }, { id: 2, name: "Banana" }];
{items.map(item => (
<li key={item.id}>{item.name}</li> // ✅ Good practice
))}
- Not Handling Asynchronous State Updates Properly
❌ The Mistake:
React batches state updates, so reading state immediately after updating it may give outdated values.
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // ❌ This logs the old value
};
✅ The Solution:
Use the functional updater to ensure correct values.
setCount(prevCount => prevCount + 1);
Now, prevCount always holds the latest state value.
- Not Optimizing Performance with useMemo and useCallback
❌ The Mistake:
Recomputing expensive calculations on every render.
const result = expensiveFunction(data);
✅ The Solution:
Use useMemo for expensive calculations and useCallback for functions.
const result = useMemo(() => expensiveFunction(data), [data]);
const memoizedFunction = useCallback(() => doSomething(), []);
- Ignoring Component Re-Renders
❌ The Mistake:
Passing objects/functions directly as props, causing unnecessary re-renders.
<ChildComponent data={{ name: "John" }} /> // ❌ Bad practice
✅ The Solution:
Use useMemo to avoid unnecessary re-renders.
const memoizedData = useMemo(() => ({ name: "John" }), []);
<ChildComponent data={memoizedData} />;
- Not Cleaning Up Effects
❌ The Mistake:
Leaving event listeners or subscriptions open, causing memory leaks.
useEffect(() => {
window.addEventListener("resize", handleResize);
});
✅ The Solution:
Return a cleanup function inside useEffect.
useEffect(() => {
const handleResize = () => console.log("Resized");
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
- Not Using Lazy Loading for Performance
❌ The Mistake:
Importing all components at once, increasing initial load time.
import HeavyComponent from "./HeavyComponent"; // ❌ Bad for performance
✅ The Solution:
Use React.lazy and Suspense for code-splitting.
const HeavyComponent = React.lazy(() => import("./HeavyComponent"));
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>;
- Not Handling Errors Properly
❌ The Mistake:
Not wrapping components in an error boundary.
<MyComponent /> // ❌ Crashes the whole app if an error occurs
✅ The Solution:
Use an Error Boundary to catch errors.
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error("Error caught:", error, info);
}
render() {
if (this.state.hasError) return <h2>Something went wrong.</h2>;
return this.props.children;
}
}
Use it in your app:
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
- Not Using PropTypes or TypeScript
❌ The Mistake:
Not validating props, leading to unexpected bugs.
const Button = ({ text }) => <button>{text.toUpperCase()}</button>; // ❌ Can crash if text is undefined
✅ The Solution:
Use PropTypes (for JavaScript) or TypeScript for type safety.
import PropTypes from "prop-types";
const Button = ({ text }) => <button>{text.toUpperCase()}</button>;
Button.propTypes = {
text: PropTypes.string.isRequired,
};
Or use TypeScript:
type ButtonProps = { text: string };
const Button: React.FC<ButtonProps> = ({ text }) => <button>{text.toUpperCase()}</button>;
Final Thoughts
Avoiding these mistakes will make you a better React developer, improve your app’s performance, and reduce bugs. Mastering state management, performance optimization, and clean coding practices will help you build efficient and scalable React applications.
Top comments (0)