Difficulty: Advanced
What are common React anti-patterns? How do you identify and fix them?
Anti-patterns in React are the recurring mistakes and code smells that make applications buggy, slow, or painful to maintain. Knowing these patterns is arguably more valuable than knowing the correct patterns, because in the real world, most of your time is spent maintaining and improving existing code, not writing greenfield applications. Let me walk through the most important ones.
The single most common anti-pattern is the unnecessary useEffect. The React team has an entire documentation page titled 'You Might Not Need an Effect' dedicated to this problem. Developers reach for useEffect like a Swiss army knife, using it for everything, when most use cases do not need it.
The first manifestation: using useEffect to compute derived state. You have products and a category filter in state, and you create a separate filteredProducts state updated by useEffect. This is unnecessary. The filtered list is derived data that can be computed directly during render: const filtered = products.filter(p => p.category === category). No extra state, no effect, no unnecessary re-render. If the computation is expensive, wrap it in useMemo.
The second manifestation: using useEffect to respond to events. A user submits a form, you set submitted to true, then useEffect watches submitted and triggers analytics and a toast. This is wrong because those actions are consequences of the submit event, not consequences of state changing. They belong in the event handler. useEffect is for synchronization with external systems, not event handling.
The third manifestation: using useEffect to initialize state from props. You receive a user prop and useEffect to copy user.name into local state. Instead, initialize directly with useState(user.name). If you need state to reset when the user prop changes, use the key prop: <EditProfile key={user.id} user={user} />.
The rule: useEffect should be reserved for synchronizing with external systems -- DOM measurements, browser APIs, third-party libraries, WebSocket connections, timers. If your effect just sets state based on other state or props, you almost certainly do not need it.
Component structure anti-patterns are the next category. The god component does too much -- hundreds of lines, dozens of state variables, mixing concerns. Break it into focused pieces where each has a single responsibility. Prop drilling is passing props through components that do not use them. Two or three levels is fine; beyond that, use composition (restructure to pass components as children) or Context. Defining components inside other components creates new component types on every render, causing full unmount/remount cycles that destroy state.
State management anti-patterns include storing derived data in state (creates sync bugs), putting everything in global state (most state should be local), failing to use URL state for filters and pagination (loses shareability, bookmarkability, and refresh persistence), and mutating state directly (push on an array, direct property assignment -- React will not detect the change because the reference is the same).
Performance anti-patterns include creating new objects and functions inline in render and passing them to memoized children (defeats React.memo), over-memoizing everything (adds complexity without benefit), and not profiling before optimizing (guessing at performance problems instead of measuring).
Premature abstraction deserves a mention. Developers often extract components, hooks, or utilities before seeing enough repetition to know what the right abstraction looks like. The rule of three is a good guide: wait until you see the same pattern three times before extracting. Your abstractions will be better because they are based on real patterns.
Recognizing anti-patterns in code reviews and in your own code is a sign of experience. In interviews, discussing specific anti-patterns you have encountered and fixed -- with concrete examples -- is far more convincing than listing them theoretically.
// ANTI-PATTERN 1: useEffect to sync derived state
function ProductList({ products }) {
const [filtered, setFiltered] = useState([]);
const [category, setCategory] = useState('all');
// BAD: useEffect to derive state from other state
useEffect(() => {
setFiltered(
category === 'all'
? products
: products.filter(p => p.category === category)
);
}, [products, category]);
// GOOD: Compute during render
const filtered = category === 'all'
? products
: products.filter(p => p.category === category);
}
// ANTI-PATTERN 2: useEffect to handle events
function Form() {
const [submitted, setSubmitted] = useState(false);
// BAD: useEffect reacting to state change
useEffect(() => {
if (submitted) {
sendAnalytics('form_submit');
showToast('Success!');
}
}, [submitted]);
// GOOD: Handle in the event handler directly
function handleSubmit() {
submitForm();
sendAnalytics('form_submit');
showToast('Success!');
}
}
// ANTI-PATTERN 3: useEffect to initialize state from props
function EditUser({ user }) {
const [name, setName] = useState('');
// BAD: syncing props to state
useEffect(() => {
setName(user.name);
}, [user]);
// GOOD: Initialize from props OR use key to reset
const [name, setName] = useState(user.name);
// Parent: <EditUser key={user.id} user={user} />
}
Most useEffect calls for state synchronization can be replaced by computing during render, handling in event handlers, or using the key prop for resets. useEffect should be reserved for synchronizing with external systems.
// ANTI-PATTERN: God component (does everything)
function Dashboard() {
// 20+ state variables
// 500+ lines of JSX
// Multiple concerns mixed together
// Impossible to test or maintain
}
// FIX: Break into focused components
function Dashboard() {
return (
<div>
<DashboardHeader />
<MetricsGrid />
<RecentActivity />
<QuickActions />
</div>
);
}
// ANTI-PATTERN: Prop drilling through many levels
function App({ user }) {
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <span>{user.name}</span>; // Finally used!
}
// FIX 1: Composition (often the best solution)
function App({ user }) {
return (
<Layout sidebar={<Sidebar menu={<UserMenu user={user} />} />} />
);
}
// FIX 2: Context for truly global data
const UserContext = createContext(null);
function UserMenu() {
const user = useContext(UserContext);
return <span>{user.name}</span>;
}
// ANTI-PATTERN: Defining components inside components
function Parent() {
// BAD: Child is recreated every render!
function Child() {
return <div>I remount on every parent render</div>;
}
return <Child />;
}
// FIX: Define outside
function Child() {
return <div>I persist across parent renders</div>;
}
function Parent() {
return <Child />;
}
God components are hard to maintain and test. Prop drilling can often be solved with composition before reaching for context. Components defined inside render functions are recreated every render, causing full remounts.
// ANTI-PATTERN: Storing derived data in state
function Cart({ items }) {
const [total, setTotal] = useState(0);
const [itemCount, setItemCount] = useState(0);
// BAD: State that can be computed
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price, 0));
setItemCount(items.length);
}, [items]);
// GOOD: Compute directly
const total = items.reduce((sum, i) => sum + i.price, 0);
const itemCount = items.length;
}
// ANTI-PATTERN: Not using URL for shareable state
function ProductsPage() {
// BAD: Filters in state - lost on refresh, not shareable
const [category, setCategory] = useState('all');
const [sortBy, setSortBy] = useState('price');
const [page, setPage] = useState(1);
// GOOD: Filters in URL params
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category') || 'all';
const sortBy = searchParams.get('sort') || 'price';
const page = parseInt(searchParams.get('page') || '1');
}
// ANTI-PATTERN: Mutating state
function TodoApp() {
const [todos, setTodos] = useState([]);
// BAD: Direct mutation
function addTodo(text) {
todos.push({ id: Date.now(), text }); // Mutating!
setTodos(todos); // Same reference - no re-render!
}
// GOOD: Immutable update
function addTodo(text) {
setTodos(prev => [...prev, { id: Date.now(), text }]);
}
}
Derived state causes unnecessary complexity and sync bugs. URL state makes filters bookmarkable and shareable. State mutations cause silent bugs because React cannot detect changes to the same reference.
Anti-patterns, Best Practices, Code Smells, Refactoring