What are some common React anti-patterns?
The short answer
Anti-patterns are coding habits that seem fine at first but lead to bugs, poor performance, or unmaintainable code as your app grows. In React, the most common ones include mutating state directly, using array indexes as keys, defining components inside other components, overusing useEffect, and building god components that try to do everything. Recognizing these patterns is a sign that you truly understand how React works under the hood.
Mutating state directly
React relies on immutability to know when to re-render. If you mutate state directly, React doesn't detect the change and your UI stays stale.
// Bad — mutating the existing array
function TodoList() {
const [todos, setTodos] = useState(['Buy milk']);
const addTodo = () => {
todos.push('Walk the dog');
setTodos(todos); // Same reference — React bails out
};
return <button onClick={addTodo}>Add</button>;
}// Good — creating a new array
const addTodo = () => {
setTodos([...todos, 'Walk the dog']);
};React uses Object.is to compare the old and new state. If you pass the same reference, React assumes nothing changed. Always create a new object or array.
Using array index as key
When rendering lists, React needs a key to track which items changed, were added, or removed. Using the array index seems convenient but causes subtle bugs when items are reordered, inserted, or deleted.
// Bad — index as key
{
items.map((item, index) => (
<ListItem key={index} item={item} />
));
}If you insert an item at the beginning, every item's index shifts. React thinks every component changed because the keys all shifted. This can cause lost input state, broken animations, and unnecessary re-renders.
// Good — stable, unique identifier
{
items.map((item) => (
<ListItem key={item.id} item={item} />
));
}Use a unique identifier from your data. If you truly have a static list that never reorders, index is fine — but that's rarely the case in real applications.
Defining components inside other components
This one is sneaky because the code looks perfectly normal:
// Bad — new component identity on every render
function ParentComponent() {
function ChildComponent() {
return <input placeholder="Type here" />;
}
return <ChildComponent />;
}Every time ParentComponent renders, it creates a brand new ChildComponent function. React sees a different component type each render, so it unmounts the old one and mounts a new one. That means the <input> loses its value, focus, and any internal state on every single render.
// Good — define components outside
function ChildComponent() {
return <input placeholder="Type here" />;
}
function ParentComponent() {
return <ChildComponent />;
}Always define components at the module level. If the child needs data from the parent, pass it through props.
Overusing useEffect
useEffect is for synchronizing with external systems — fetching data, setting up subscriptions, interacting with the DOM. It's not a general-purpose "run code when something changes" tool.
// Bad — derived state in useEffect
function Cart({ items }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(
items.reduce((sum, item) => sum + item.price, 0)
);
}, [items]);
return <p>Total: ${total}</p>;
}This triggers an unnecessary extra render. The total is derived directly from props — just compute it during render:
// Good — compute during render
function Cart({ items }) {
const total = items.reduce(
(sum, item) => sum + item.price,
0
);
return <p>Total: ${total}</p>;
}If the computation is expensive, use useMemo. But reach for useEffect only when you're genuinely syncing with something outside of React.
Prop drilling when composition would work
Passing props through five layers of components that don't use them is called prop drilling. Many developers jump straight to context or a state manager, but often the real fix is composition:
// Bad — drilling `user` through components that don't need it
<App user={user}>
<Layout user={user}>
<Sidebar user={user}>
<UserMenu user={user} />
</Sidebar>
</Layout>
</App>// Good — compose by passing the component directly
function App({ user }) {
return (
<Layout
sidebar={<Sidebar menu={<UserMenu user={user} />} />}
/>
);
}By passing already-rendered components, you skip the middlemen entirely. The intermediate components don't need to know about user at all.
Premature optimization
Wrapping everything in React.memo, useMemo, and useCallback "just in case" adds complexity without measurable benefit in most cases.
// Unnecessary — React.memo on a simple component
const Label = React.memo(({ text }) => {
return <span>{text}</span>;
});Memoization has a cost — React has to store the previous props and compare them on every render. For lightweight components, the comparison itself can be more expensive than just re-rendering. Optimize when you've measured a performance problem, not before.
God components
A god component is one that does too many things — fetching data, managing multiple pieces of state, handling form logic, rendering a complex UI, and orchestrating side effects all in one place.
These components become impossible to test, hard to read, and brittle to change. If your component file is over 300 lines, it's probably doing too much.
The fix is decomposition: extract custom hooks for stateful logic, break the UI into smaller presentational components, and keep each piece focused on one responsibility.
Ignoring the exhaustive-deps lint rule
The react-hooks/exhaustive-deps ESLint rule exists to catch bugs. Suppressing it is almost always a mistake:
// Bad — missing dependency, stale closure
useEffect(() => {
fetchData(userId);
}, []); // ESLint warns that userId is missingIf userId changes, this effect still uses the old one. The lint rule catches exactly this class of bug. If you find yourself wanting to suppress it, it usually means the effect needs restructuring — not that the rule is wrong.
Why interviewers ask this
Anti-patterns reveal how deeply you understand React's rendering model and design philosophy. Anyone can write React code that works. Interviewers want to know if you can write React code that's correct, performant, and maintainable — and whether you can spot problems in existing code during a code review. Being able to name these patterns and explain why they're problematic shows real-world experience.