ReactYou Might Not Need an Effect

You Might Not Need an Effect

useEffect is powerful, but it is also one of the most overused hooks. Many patterns that developers reach for useEffect to solve are actually better solved with simpler React primitives. Over-using effects makes your code harder to understand, introduces timing bugs, and wastes render cycles. This page covers the six most common effect anti-patterns and their clean alternatives.

1. Deriving State from Props or State

A common pattern: store derived data in state and sync it with an effect. For example, filtering a list whenever the search query changes.

JSX
// ❌ Anti-pattern: storing derived state and syncing it
function FilteredList({ items }) {
  const [query, setQuery] = useState('')
  const [filtered, setFiltered] = useState(items) // derived state!

  useEffect(() => {
    setFiltered(items.filter(item => item.name.includes(query)))
  }, [items, query]) // extra render cycle: render → effect → setState → render again

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ul>{filtered.map(item => <li key={item.id}>{item.name}</li>)}</ul>
    </div>
  )
}

// ✅ Correct: compute derived data directly during render
function FilteredList({ items }) {
  const [query, setQuery] = useState('')

  // Computed during render — no state, no effect, no extra render cycle
  const filtered = items.filter(item => item.name.includes(query))

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ul>{filtered.map(item => <li key={item.id}>{item.name}</li>)}</ul>
    </div>
  )
}

The effect version causes two renders per keystroke: one when query changes, and another when the effect updates filtered. The direct computation version causes one render and is easier to read. If the filtering is expensive, see pattern 2.

2. Caching an Expensive Computation

What if the derivation is genuinely expensive (sorting thousands of records, building a tree structure)? Do not use an effect — use useMemo.

JSX
// ❌ Anti-pattern: effect to cache an expensive computation
function SortedTable({ rows }) {
  const [sorted, setSorted] = useState([])

  useEffect(() => {
    setSorted([...rows].sort((a, b) => a.value - b.value)) // extra render
  }, [rows])

  return <Table rows={sorted} />
}

// ✅ Correct: useMemo caches the result between renders
function SortedTable({ rows }) {
  const sorted = useMemo(
    () => [...rows].sort((a, b) => a.value - b.value),
    [rows] // only re-sorts when 'rows' changes
  )

  return <Table rows={sorted} />
}
Tip
`useMemo` runs during rendering (synchronously), while `useEffect` runs after render. For pure data transformations with no side effects, `useMemo` is always the right choice.
3. Resetting State When a Prop Changes

Imagine a CommentInput component that should clear its text when the user switches to a different post. A common approach: watch the postId prop in an effect and reset state.

JSX
// ❌ Anti-pattern: effect to reset state
function CommentInput({ postId }) {
  const [text, setText] = useState('')

  // Runs after render — the user sees the old text briefly before it resets
  useEffect(() => {
    setText('')
  }, [postId])

  return <textarea value={text} onChange={e => setText(e.target.value)} />
}

// ✅ Correct: use the 'key' prop to reset the entire component
// When 'key' changes, React unmounts and remounts a fresh component with
// fresh state — no effect needed, no stale flash.
function PostPage({ postId }) {
  return <CommentInput key={postId} postId={postId} />
}

function CommentInput({ postId }) {
  const [text, setText] = useState('') // always starts empty — key handles the reset
  return <textarea value={text} onChange={e => setText(e.target.value)} />
}
Note
Passing `key={postId}` to a component is the idiomatic React way to say "treat each postId as a completely separate component instance." It is simple, correct, and avoids the one-render lag that the effect pattern suffers from.
4. Adjusting State Based on a Prop Change

Sometimes you only need to reset part of the state, not the whole component. React supports a special pattern for this: calling setState during rendering (not in an effect) when a specific condition is met.

JSX
// ❌ Anti-pattern: watching a prop to adjust state
function List({ items }) {
  const [selectedIndex, setSelectedIndex] = useState(0)
  const [prevItems, setPrevItems] = useState(items)

  useEffect(() => {
    if (items !== prevItems) {
      setSelectedIndex(0) // extra render: render → effect → setState → render
      setPrevItems(items)
    }
  }, [items, prevItems])

  return <ul>{items.map((item, i) => (
    <li key={item.id} onClick={() => setSelectedIndex(i)}
        style={{ fontWeight: i === selectedIndex ? 'bold' : 'normal' }}>
      {item.name}
    </li>
  ))}</ul>
}

// ✅ Correct: adjust state during rendering
// React will immediately re-render with the new state — no extra async cycle.
function List({ items }) {
  const [selectedIndex, setSelectedIndex] = useState(0)
  const [prevItems, setPrevItems] = useState(items)

  // Detect the prop change and update state synchronously during render
  if (items !== prevItems) {
    setSelectedIndex(0)
    setPrevItems(items)
  }

  return <ul>{items.map((item, i) => (
    <li key={item.id} onClick={() => setSelectedIndex(i)}
        style={{ fontWeight: i === selectedIndex ? 'bold' : 'normal' }}>
      {item.name}
    </li>
  ))}</ul>
}
Warning
Calling `setState` during rendering must be guarded by a condition (comparing current and previous values) so it does not cause an infinite loop. This is an advanced pattern — reaching for the `key` prop trick (pattern 3) is usually simpler.
5. Sending a POST Request on Form Submit

A frequent mistake is using state as a "trigger" for an effect that makes a network request. The POST belongs in the event handler — it is a direct response to the user submitting the form, not to a render.

JSX
// ❌ Anti-pattern: using an effect as a trigger for a POST
function ContactForm() {
  const [submitted, setSubmitted] = useState(false)
  const [formData, setFormData] = useState(null)

  // Fires any time 'submitted' becomes true, even on remounts
  useEffect(() => {
    if (submitted && formData) {
      fetch('/api/contact', { method: 'POST', body: JSON.stringify(formData) })
    }
  }, [submitted, formData])

  function handleSubmit(e) {
    e.preventDefault()
    setFormData({ email: e.target.email.value })
    setSubmitted(true)
  }

  return <form onSubmit={handleSubmit}>...</form>
}

// ✅ Correct: POST directly in the event handler
function ContactForm() {
  async function handleSubmit(e) {
    e.preventDefault()
    const formData = { email: e.target.email.value }
    await fetch('/api/contact', { method: 'POST', body: JSON.stringify(formData) })
  }

  return <form onSubmit={handleSubmit}>...</form>
}
6. Subscribing to an External Store

When subscribing to a non-React data store (Redux, Zustand, browser APIs like navigator.onLine, custom event emitters), a hand-rolled useEffect subscription is fragile. React provides useSyncExternalStore specifically for this purpose.

JSX
import { useSyncExternalStore } from 'react'

// ❌ Fragile hand-rolled subscription
function OnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine)

  useEffect(() => {
    function handleOnline() { setIsOnline(true) }
    function handleOffline() { setIsOnline(false) }
    window.addEventListener('online', handleOnline)
    window.addEventListener('offline', handleOffline)
    return () => {
      window.removeEventListener('online', handleOnline)
      window.removeEventListener('offline', handleOffline)
    }
  }, [])
  // Problem: initial state is read outside the effect → can be stale on SSR

  return <span>{isOnline ? 'Online' : 'Offline'}</span>
}

// ✅ useSyncExternalStore — handles subscriptions, snapshots, and SSR correctly
function subscribe(callback) {
  window.addEventListener('online', callback)
  window.addEventListener('offline', callback)
  return () => {
    window.removeEventListener('online', callback)
    window.removeEventListener('offline', callback)
  }
}

function OnlineStatus() {
  const isOnline = useSyncExternalStore(
    subscribe,                          // how to subscribe
    () => navigator.onLine,             // current snapshot (client)
    () => true                          // server snapshot (SSR fallback)
  )

  return <span>{isOnline ? 'Online' : 'Offline'}</span>
}

useSyncExternalStore handles tearing (inconsistent renders during concurrent features), deduplicates subscriptions, and correctly handles the server/client snapshot mismatch. Use it for any non-React external store.

Summary: The Right Tool for Each Job
  • Derived data from props/state → compute during render (no hook needed)

  • Expensive derived datauseMemo

  • Reset all state when prop changeskey prop

  • Adjust part of state when prop changessetState during render

  • Respond to user action → event handler

  • Subscribe to an external storeuseSyncExternalStore

  • Synchronize with an external systemuseEffect (this is the right use)

Tip
Before reaching for `useEffect`, ask yourself: "Is this code synchronizing React with an external system?" If the answer is no — if you are just transforming data, responding to a user action, or computing a value — there is a better tool available.