Beginner Mistakes Every New React Developer Makes—And How to Fix Them

Posted November 17, 2025 by Karol Polakowski

React is approachable, which makes it easy to start — and easy to pick up bad habits. The mistakes below are practical, common, and fixable. For each one I show what goes wrong, why it matters, and a concrete fix you can use immediately.


1) Mutating state instead of treating it as immutable

The problem

Beginners often modify arrays or objects in place (push, splice, direct property assignment). React’s state updates rely on identity changes. Mutating state in place can make UI updates unpredictable.

Bad example

// Bad: mutating state directly
function Todos() {
  const [todos, setTodos] = useState([])

  function add(todo) {
    todos.push(todo) // <-- mutation
    setTodos(todos) // React may not re-render
  }

  return null
}

Fix

  • Always create new objects/arrays when updating state.
  • Use spread, concat, or functional updates when the new state depends on previous state.
// Good: create a new array
function Todos() {
  const [todos, setTodos] = useState([])

  function add(todo) {
    setTodos(prev => ([...prev, todo]))
  }

  return null
}

Why it helps

New references guarantee React detects changes and re-renders predictably.

2) Missing dependencies in useEffect (stale closures)

The problem

useEffect closures capture variables at the time they run. Forgetting to include dependencies causes stale values or duplicated effects.

Bad example

// Bad: missing dependency causes stale count
function Timer() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1) // captures initial count = 0
    }, 1000)

    return () => clearInterval(id)
  }, []) // missing [count]

  return null
}

Fixes

Option A — include dependencies (safe, but may recreate timers)

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1)
  }, 1000)
  return () => clearInterval(id)
}, [])

Option B — use functional updates to avoid depending on stale values (preferred for intervals)

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1)
  }, 1000)
  return () => clearInterval(id)
}, [])

Why it helps

Including correct dependencies or using functional updates prevents subtle bugs and ensures effects run with correct values.

3) Using array index as key in lists

The problem

Using the array index for key (key={index}) is easy but dangerous when list items are inserted, removed, or reordered. It can break component state, animations, and form inputs.

Bad example

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

Fix

Use stable IDs (database id, uuid, or otherwise stable unique identifier).

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

Why it helps

Stable keys let React reconcile items correctly and preserve local component state.

4) Overusing useEffect for things that should be in event handlers or derived from props/state

The problem

useEffect is for side effects — fetching, subscriptions, DOM mutations. It’s often misused for state derivation or logic that should happen inside callbacks or during render.

Bad example

// Bad: deriving filtered list in an effect and storing it
useEffect(() => {
  setFiltered(items.filter(i => i.active))
}, [items])

Fix

Derive values during render or memoize them with useMemo.

const filtered = useMemo(() => items.filter(i => i.active), [items])

Why it helps

Keeps effects reserved for real side effects and simplifies your data flow.


5) Unnecessary re-renders due to anonymous functions and inline objects

The problem

Passing inline functions or objects into child props makes props change every render, which can trigger re-renders even when data hasn’t changed.

Bad example

<Child onClick={() => doSomething(item)} style={{ color: 'red' }} />

Fix

  • Use useCallback for functions you want to preserve between renders
  • Use useMemo for objects
  • Or lift logic into the child or create stable handlers per item
const handleClick = useCallback((id) => doSomething(id), [doSomething])
const style = useMemo(() => ({ color: 'red' }), [])

<Child onClick={() => handleClick(item.id)} style={style} />

Why it helps

Reduces wasted renders and makes memoization (React.memo) effective.

6) Not cleaning up subscriptions / listeners

The problem

Adding listeners or subscriptions in effects without cleaning them up causes memory leaks and duplicate handlers.

Bad example

useEffect(() => {
  window.addEventListener('resize', onResize)
}, []) // no cleanup

Fix

Always return a cleanup function from effects.

useEffect(() => {
  window.addEventListener('resize', onResize)
  return () => window.removeEventListener('resize', onResize)
}, [onResize])

Why it helps

Prevents leaks and avoids multiple handlers accumulating over time.

7) Confusing controlled vs uncontrolled inputs

The problem

Switching between controlled (value/onChange) and uncontrolled (defaultValue/ref) modes can cause warnings or unexpected behavior.

Bad example

// Bad: sometimes value is undefined which switches modes
<input value={maybeUndefined} onChange={e => setVal(e.target.value)} />

Fix

Always provide a defined value for controlled inputs (use empty string fallback), or use refs for uncontrolled components.

<input value={val ?? ''} onChange={e => setVal(e.target.value)} />

Why it helps

Keeps input behavior predictable and prevents React warnings.

8) Not handling async errors (and missing loading state)

The problem

Not handling loading and error states when fetching makes the UI fragile and hard to debug.

Fix

Always model loading, error, and success states. Prefer a data-fetching library (React Query, SWR) for real projects.

const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)

useEffect(() => {
  let mounted = true
  setLoading(true)
  fetch('/api/items')
    .then(r => r.json())
    .then(json => mounted && setData(json))
    .catch(err => mounted && setError(err))
    .finally(() => mounted && setLoading(false))

  return () => { mounted = false }
}, [])

Why it helps

Clear UX during fetches and safer state updates when components unmount.


9) Reinventing data fetching and caching

The problem

Writing a bespoke data-fetching layer misses lots of hard problems: caching, deduping requests, background refetch, retries, pagination.

Fix

Adopt a battle-tested library: React Query, SWR, or Apollo (GraphQL). They dramatically reduce complexity and bugs.

Why it helps

You get caching, stale-while-revalidate, background updates, and simpler components.

10) Not using error boundaries for unexpected runtime errors

The problem

If a child component throws, it can unmount the whole React tree (in practice this results in an error overlay in dev or broken UI in prod).

Fix

Use an Error Boundary (class component) or a library that provides one.

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { error: null }
  }
  static getDerivedStateFromError(error) { return { error } }
  render() {
    if (this.state.error) return <div>Something went wrong</div>
    return this.props.children
  }
}

Why it helps

Error boundaries contain failures and give you a chance to display fallback UI and log errors.

11) Not splitting logic into hooks and components (monolithic components)

The problem

Large components with many responsibilities are hard to test and reason about.

Fix

Extract reusable logic into custom hooks (useTodos, useFetch) and smaller presentational components.

Why it helps

Improves reuse, testability, and clarity.

Quick checklist for cleaner React code

  • Treat state as immutable — never mutate in place.
  • Always declare correct useEffect dependencies; use functional updates to avoid stale closures.
  • Use stable keys for lists.
  • Prefer derived values during render or with useMemo, not stored state.
  • Clean up subscriptions and event listeners in effects.
  • Handle loading and error states for async code; consider React Query or SWR.
  • Use useCallback/useMemo sparingly to prevent unnecessary renders — measure first.
  • Use Error Boundaries in production apps.
  • Split big components and extract custom hooks.
  • Use TypeScript or prop-types to catch type-related mistakes early.

Tools and habits that help

  • React DevTools (Profiler) — find expensive renders.
  • ESLint with eslint-plugin-react-hooks — enforces effect dependencies.
  • TypeScript — catches many runtime mistakes before you run the app.
  • React Query / SWR — simplify data fetching concerns.

Final thoughts

Most React bugs follow simple patterns: accidental mutation, stale closures, and unstable identities (keys/props). Once you learn to think in terms of immutable state, pure render functions, and well-scoped effects, your components will be easier to reason about and much more reliable. Start small: enforce hooks rules in ESLint, avoid index keys, and adopt a data-fetching library — you’ll prevent far more bugs than you fix later.