⚛️ React Anti-Patterns and Best Practices — The Don’ts and Dos - Part 1
React is an amazing tool that helps us build interactive UIs seamlessly. But as with any powerful tool, it comes with its pitfalls. Writing React apps can be tricky if you fall into common anti-patterns that hurt your app’s performance, maintainability, and developer sanity.
In this article, you’ll discover practical examples of what to avoid ❌ and what to do ✅ instead — to keep your React code clean, efficient, and easy to maintain.
1. ❌ Direct State Mutation — The Silent Bug Trap#
Imagine you’re updating the number of items in your cart by directly modifying the state object. It might seem to work, but React won’t know it needs to re-render.
⚠️ Bad Example#
// Direct state mutation - Don't do this!
this.state.cartCount = 5;Why is this bad? Direct mutation bypasses React’s built-in state change tracking and will not cause a re-render, leading to your UI being out of sync with the data.
✅ Good Example#
// Correct usage: Always use setState or hooks
this.setState({ cartCount: 5 });
// Or in function components
const [count, setCount] = useState(0);
setCount(5);🚀 Takeaway: Always treat state as immutable. Let React know when the state changes so it can re-render accordingly.
2. ❌ Using Array Index as Keys — The Dynamic List Danger#
A common pattern is to use the array index as a key prop when rendering lists. While easy, it’s a trap that leads to weird UI bugs.
⚠️ Bad Example#
items.map((item, index) => <li key={index}>{item.name}</li>);Why is this bad? If the list order changes, React may mistakenly reuse DOM nodes, causing wrong items to display or losing user input.
✅ Good Example#
items.map(item => <li key={item.id}>{item.name}</li>);🚀 Takeaway: Always use unique, stable identifiers (like IDs) as keys to help React track items properly.
3. ❌ Inline Functions in JSX — The Performance Pitfall#
Inline functions in JSX may look convenient, but they can cause unnecessary re-renders.
⚠️ Bad Example#
<button onClick={() => alert("Clicked!")}>Click me</button>Why is this bad? Every render creates a new function, causing child components to re-render unnecessarily.
✅ Good Example#
const handleClick = () => alert("Clicked!");
<button onClick={handleClick}>Click me</button>;Or even better, use useCallback:
const handleClick = useCallback(() => alert("Clicked!"), []);🚀 Takeaway: Define event handlers outside render or memoize them to avoid needless re-renders.
4. ❌ Defining Components Inside Other Components — The Hidden Trap#
Defining components inside other components might look organized but can degrade performance.
⚠️ Bad Example#
function Parent() {
const Child = () => <div>Child</div>;
return <Child />;
}Why is this bad?
Each parent re-render recreates Child, making React treat it as a new component and resetting its state.
✅ Good Example#
function Child() {
return <div>Child</div>;
}
function Parent() {
return <Child />;
}🚀 Takeaway: Declare components at the top level to keep them stable across renders.
5. ❌ Skipping useEffect Dependencies — The Subtle Bug Source#
Hooks are great, but missing dependencies in useEffect can lead to stale values and unpredictable behavior.
⚠️ Bad Example#
useEffect(() => {
fetchData();
}, []); // fetchData is used, but not declared as dependencyWhy is this bad?
If fetchData changes, React won’t rerun the effect since dependencies are missing.
✅ Good Example#
useEffect(() => {
fetchData();
}, [fetchData]);🚀 Takeaway: Always declare all dependencies in useEffect to keep logic in sync.
6. ❌ Overusing Context — The Complexity Culprit#
Context is great for global state but don’t use it everywhere.
⚠️ Bad Example#
// Putting every piece of data in Context unnecessarily
const ThemeContext = React.createContext();Why is this bad? Overuse of context can cause unnecessary re-renders and make your app harder to maintain.
✅ Good Example#
Use local state or prop drilling for component-specific data and reserve context for global concerns like theming or authentication.
7. ❌ Large Components with Many Responsibilities — The Maintenance Nightmare#
A React component should have one job and do it well.
⚠️ Bad Example#
function Dashboard() {
// Fetches data, manages state, renders multiple sections, handles routing...
}Why is this bad? Large components are hard to debug, test, and maintain.
✅ Good Example#
function Dashboard() {
return (
<>
<UserStats />
<ActivityFeed />
<Notifications />
</>
);
}🚀 Takeaway: Follow the Single Responsibility Principle. Split components by concerns.
8. ❌ Forgetting to Clean Up Side Effects — The Memory Leak Hazard#
Effects like timers or subscriptions need cleanup to avoid resource leaks.
⚠️ Bad Example#
useEffect(() => {
const id = setInterval(() => console.log("ping"), 1000);
// No cleanup
}, []);Why is this bad? Intervals or listeners accumulate every time the component mounts, causing memory leaks.
✅ Good Example#
useEffect(() => {
const id = setInterval(() => console.log("ping"), 1000);
return () => clearInterval(id); // Cleanup on unmount
}, []);🚀 Takeaway: Always clean up side effects in useEffect.
9. ❌ Deeply Nested State Objects — The Complexity Trap#
Deeply nested state structures make updates verbose and error-prone.
⚠️ Bad Example#
const [state, setState] = useState({
user: { profile: { name: "", age: 0 } },
});Why is this bad? Updating nested properties requires complex immutable updates.
✅ Good Example#
const [userName, setUserName] = useState("");
const [userAge, setUserAge] = useState(0);🚀 Takeaway: Keep your state flat and simple.
10. ❌ Ignoring Prop Types or TypeScript — The Hidden Bug Magnet#
Without type-checking, bugs can silently slip through.
⚠️ Bad Example#
function Button(props) {
return <button>{props.label}</button>;
}No type guarantees on what label should be.
✅ Good Example#
import PropTypes from "prop-types";
function Button({ label }) {
return <button>{label}</button>;
}
Button.propTypes = {
label: PropTypes.string.isRequired,
};Or even better, use TypeScript:
type ButtonProps = {
label: string;
};
function Button({ label }: ButtonProps) {
return <button>{label}</button>;
}🚀 Takeaway: Always use PropTypes or TypeScript to catch errors early.
🧠 Summary: React Dos and Don’ts
| Aspect | ⚠️ Don’t (Anti-Pattern) | ✅ Do (Best Practice) |
|---|---|---|
| State Mutation | Directly mutate state | Use setState or hooks |
| List Keys | Use array index for keys | Use unique IDs for keys |
| Inline Functions | Define inline in JSX | Memoize or define outside render |
| Component Declaration | Declare inside parent | Declare at top-level |
| useEffect Dependencies | Skip dependencies | Include all dependencies |
| Context Usage | Overuse for local data | Use only for global state |
| Component Size | One huge component | Split by responsibilities |
| Side Effects | Skip cleanup | Always clean up |
| State Structure | Deeply nested state | Keep it flat |
| Type Checking | Skip types | Use PropTypes or TypeScript |
🚀 Final Thoughts
Understanding why certain practices are anti-patterns helps you avoid performance issues and maintain cleaner, more scalable React code. From managing state immutably to memoizing callbacks — each small fix leads to a smoother and more reliable React app.
Refactor one step at a time, and you’ll see the difference in clarity, performance, and developer happiness.