DEV Community

Learcise
Learcise

Posted on

Common React Anti-Patterns and How to Fix Them

React is a powerful and flexible library. But that flexibility also makes it easy to fall into anti-patterns—habits that seem fine at first but hurt readability, maintainability, and performance over time.

In this post, we’ll walk through common pitfalls, show bad code examples, explain why they’re bad, and then demonstrate better approaches.


1. Overusing useState and Scattering State

Bad: multiple related states separated

function ProfileForm() {
  const [firstName, setFirstName] = React.useState('');
  const [lastName, setLastName] = React.useState('');
  const [age, setAge] = React.useState(0);
  // and it keeps growing...
  return (
    <>
      <input value={firstName} onChange={e => setFirstName(e.target.value)} />
      <input value={lastName}  onChange={e => setLastName(e.target.value)} />
      <input value={age}       onChange={e => setAge(Number(e.target.value))} />
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

Why is this bad?

  • Update logic gets scattered across multiple setters.
  • Hard to keep values consistent (e.g. resetting all fields at once).
  • More states = more dependency management and risk of bugs.

Good: group related state in an object

function ProfileForm() {
  const [profile, setProfile] = React.useState({ firstName: '', lastName: '', age: 0 });
  const update = (key) => (e) => setProfile(p => ({
    ...p,
    [key]: key === 'age' ? Number(e.target.value) : e.target.value
  }));
  return (
    <>
      <input value={profile.firstName} onChange={update('firstName')} />
      <input value={profile.lastName}  onChange={update('lastName')} />
      <input value={profile.age}       onChange={update('age')} />
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

2. Monster Components

Bad: everything stuffed into one component

function TodoApp() {
  const [text, setText] = React.useState('');
  const [todos, setTodos] = React.useState([]);
  const add = () => { if (text.trim()) setTodos(t => [...t, { id: Date.now(), text }]); setText(''); };
  const remove = (id) => setTodos(t => t.filter(x => x.id !== id));
  return (
    <div>
      <h1>Todos</h1>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={add}>add</button>
      <ul>
        {todos.map(t => <li key={t.id}><span>{t.text}</span><button onClick={() => remove(t.id)}>x</button></li>)}
      </ul>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Why is this bad?

  • Mixes input, list rendering, add/remove logic in one file.
  • Hard to test or reuse pieces.
  • Any small change risks breaking unrelated logic.

Good: split responsibilities into smaller components

const TodoInput = ({ text, setText, onAdd }) => (
  <div>
    <input value={text} onChange={e => setText(e.target.value)} />
    <button onClick={onAdd}>add</button>
  </div>
);

const TodoList = ({ todos, onRemove }) => (
  <ul>{todos.map(t => <li key={t.id}>{t.text}<button onClick={() => onRemove(t.id)}>x</button></li>)}</ul>
);

function TodoApp() {
  const [text, setText] = React.useState('');
  const [todos, setTodos] = React.useState([]);
  const add = () => { if (text.trim()) setTodos(t => [...t, { id: crypto.randomUUID(), text }]); setText(''); };
  const remove = id => setTodos(t => t.filter(x => x.id !== id));
  return (
    <div>
      <h1>Todos</h1>
      <TodoInput text={text} setText={setText} onAdd={add} />
      <TodoList todos={todos} onRemove={remove} />
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

3. Components Doing Too Much Logic

Bad: fetching data directly inside a UI component

function Users() {
  const [users, setUsers] = React.useState([]);
  React.useEffect(() => {
    fetch('/api/users').then(r => r.json()).then(setUsers);
  }, []);
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Enter fullscreen mode Exit fullscreen mode

Why is this bad?

  • Couples data fetching with UI rendering.
  • Harder to test in isolation.
  • Makes the component harder to reuse.

Good: move logic into a custom hook

function useUsers() {
  const [users, setUsers] = React.useState([]);
  React.useEffect(() => {
    let aborted = false;
    (async () => {
      const r = await fetch('/api/users');
      const data = await r.json();
      if (!aborted) setUsers(data);
    })();
    return () => { aborted = true; };
  }, []);
  return users;
}

function Users() {
  const users = useUsers();
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Enter fullscreen mode Exit fullscreen mode

4. Reversing Data Flow (Child Updating Parent)

Bad: child updates parent’s state directly

function Child() {
  // Imagine directly importing parent’s setter or using context incorrectly
  // ❌ breaks one-way data flow
  return null;
}

Enter fullscreen mode Exit fullscreen mode

Why is this bad?

  • Breaks React’s one-way data flow.
  • Debugging becomes painful because updates happen in unexpected places.
  • Creates tight coupling.

Good: Lift state up, pass down via props

function Parent() {
  const [value, setValue] = React.useState('');
  return <Child value={value} onChange={setValue} />;
}
const Child = ({ value, onChange }) => (
  <input value={value} onChange={e => onChange(e.target.value)} />
);

Enter fullscreen mode Exit fullscreen mode

5. Storing Derived State

Bad: storing values you could calculate

function Cart({ items }) {
  const [total, setTotal] = React.useState(0);
  React.useEffect(() => {
    setTotal(items.reduce((s, it) => s + it.price, 0));
  }, [items]);
  return <div>Total: {total}</div>;
}

Enter fullscreen mode Exit fullscreen mode

Why is this bad?

  • Two sources of truth → data inconsistency risk.
  • Extra state → extra re-renders.
  • Harder to debug.

Good: compute when needed

function Cart({ items }) {
  const total = React.useMemo(() => items.reduce((s, it) => s + it.price, 0), [items]);
  return <div>Total: {total}</div>;
}

Enter fullscreen mode Exit fullscreen mode

6. Abusing useEffect for Calculations

Bad: using useEffect + setState just for filtering

function Filtered({ list, q }) {
  const [filtered, setFiltered] = React.useState([]);
  React.useEffect(() => {
    setFiltered(list.filter(x => x.includes(q)));
  }, [list, q]);
  return <ul>{filtered.map(x => <li key={x}>{x}</li>)}</ul>;
}

Enter fullscreen mode Exit fullscreen mode

Why is this bad?

  • Triggers extra render cycles.
  • Makes simple logic look like a side effect.
  • Harder to reason about.

Good: useMemo for derived data

function Filtered({ list, q }) {
  const filtered = React.useMemo(() => list.filter(x => x.includes(q)), [list, q]);
  return <ul>{filtered.map(x => <li key={x}>{x}</li>)}</ul>;
}

Enter fullscreen mode Exit fullscreen mode

7. Wrong key Usage

Bad: using index as key

items.map((item, i) => <li key={i}>{item.name}</li>);

Enter fullscreen mode Exit fullscreen mode

Why is this bad?

  • When list order changes, React can’t track items correctly.
  • Leads to bugs like losing focus or input values jumping around.

Good: use stable, unique keys

items.map(item => <li key={item.id}>{item.name}</li>);

Enter fullscreen mode Exit fullscreen mode

8. Forgetting Cleanup in Effects

Bad: leaking event listeners

function WindowSize() {
  const [w, setW] = React.useState(window.innerWidth);
  React.useEffect(() => {
    const onResize = () => setW(window.innerWidth);
    window.addEventListener('resize', onResize);
    // ❌ forgot to remove
  }, []);
  return <div>{w}</div>;
}

Enter fullscreen mode Exit fullscreen mode

Why is this bad?

  • Causes memory leaks.
  • Can trigger updates after unmount → warnings/errors.

Good: return cleanup

function WindowSize() {
  const [w, setW] = React.useState(window.innerWidth);
  React.useEffect(() => {
    const onResize = () => setW(window.innerWidth);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);
  return <div>{w}</div>;
}

Enter fullscreen mode Exit fullscreen mode

9. Defining Components Inside Components

Bad: redefining child every render

function Parent({ items }) {
  const Item = ({ item }) => <li>{item.name}</li>; // ❌ new component each time
  return <ul>{items.map(i => <Item key={i.id} item={i} />)}</ul>;
}

Enter fullscreen mode Exit fullscreen mode

Why is this bad?

  • Creates a new function reference on every render.
  • Memoization breaks, causing unnecessary re-renders.
  • Harder to debug.

Good: define outside, memoize if needed

const Item = React.memo(({ item }) => <li>{item.name}</li>);

function Parent({ items }) {
  return <ul>{items.map(i => <Item key={i.id} item={i} />)}</ul>;
}

Enter fullscreen mode Exit fullscreen mode

10. No Error Boundaries

Bad: one error kills the entire app

function App() {
  return <ProblemChild />; // throw → blank screen
}

Enter fullscreen mode Exit fullscreen mode

Why is this bad?

  • Terrible UX: user sees nothing.
  • Errors don’t get logged or handled gracefully.
  • Can’t recover or retry.

Good: wrap with an Error Boundary

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Retry</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <ProblemChild />
    </ErrorBoundary>
  );
}

Enter fullscreen mode Exit fullscreen mode

11. Ignoring "State is a Snapshot" (Stale Closures)

Bad: using outdated state inside interval

function Counter() {
  const [count, setCount] = React.useState(0);
  React.useEffect(() => {
    const id = setInterval(() => {
      console.log('tick uses stale count:', count); // ❌ stale value
      setCount(c => c + 1); // functional update works
    }, 1000);
    return () => clearInterval(id);
  }, []); // missing dependency
  return <div>{count}</div>;
}

Enter fullscreen mode Exit fullscreen mode

Why is this bad?

  • Closures capture old state.
  • Leads to confusing, inconsistent logs and behaviors.

Good: use refs or functional updates

function Counter() {
  const [count, setCount] = React.useState(0);
  const ref = React.useRef(count);
  React.useEffect(() => { ref.current = count; }, [count]);

  React.useEffect(() => {
    const id = setInterval(() => {
      console.log('fresh count:', ref.current);
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <div>{count}</div>;
}

Enter fullscreen mode Exit fullscreen mode

12. Wrong Dependency Arrays in useEffect

Bad: missing dependencies

function Search({ query, fetcher }) {
  const [result, setResult] = React.useState(null);
  React.useEffect(() => {
    fetcher(query).then(setResult);
  }, []); // ❌ ignores query and fetcher
  return <div>{JSON.stringify(result)}</div>;
}

Enter fullscreen mode Exit fullscreen mode

Why is this bad?

  • Effect doesn’t rerun when query changes → stale data.
  • Bugs that only appear in specific cases, hard to trace.

Good: declare dependencies properly

function Search({ query }) {
  const [result, setResult] = React.useState(null);
  const fetcher = React.useCallback(
    (q) => fetch(`/api?q=${encodeURIComponent(q)}`).then(r => r.json()),
    []
  );
  React.useEffect(() => {
    let ignore = false;
    fetcher(query).then(r => { if (!ignore) setResult(r); });
    return () => { ignore = true; };
  }, [query, fetcher]);
  return <div>{JSON.stringify(result)}</div>;
}

Enter fullscreen mode Exit fullscreen mode

Takeaways

To write cleaner, more maintainable React code:

  • Keep state minimal and consistent.
  • Follow the single responsibility principle.
  • Use useEffect only for syncing with the outside world.
  • Always provide proper keys and error boundaries.
  • Treat state as a snapshot per render, not as a mutable variable.

These small improvements add up to code that’s easier to debug, test, and evolve. 🚀

Top comments (0)