Files
orca/.agents/skills/react-useeffect/anti-patterns.md
2026-03-21 15:15:38 -07:00

5.9 KiB

useEffect Anti-Patterns

1. Redundant State for Derived Values

// BAD: Extra state + Effect for derived value
function Form() {
  const [firstName, setFirstName] = useState('Taylor')
  const [lastName, setLastName] = useState('Swift')
  const [fullName, setFullName] = useState('')

  useEffect(() => {
    setFullName(firstName + ' ' + lastName)
  }, [firstName, lastName])
}

// GOOD: Calculate during rendering
function Form() {
  const [firstName, setFirstName] = useState('Taylor')
  const [lastName, setLastName] = useState('Swift')
  const fullName = firstName + ' ' + lastName // Just compute it
}

Why it's bad: Causes extra render pass with stale value, then re-renders with updated value.


2. Filtering/Transforming Data in Effect

// BAD: Effect to filter list
function TodoList({ todos, filter }) {
  const [visibleTodos, setVisibleTodos] = useState([])

  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter))
  }, [todos, filter])
}

// GOOD: Filter during render (memoize if expensive)
function TodoList({ todos, filter }) {
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter])
}

3. Resetting State on Prop Change

// BAD: Effect to reset state
function ProfilePage({ userId }) {
  const [comment, setComment] = useState('')

  useEffect(() => {
    setComment('')
  }, [userId])
}

// GOOD: Use key prop
function ProfilePage({ userId }) {
  return <Profile userId={userId} key={userId} />
}

function Profile({ userId }) {
  const [comment, setComment] = useState('') // Resets automatically
}

Why key works: React treats components with different keys as different components, recreating state.


4. Event-Specific Logic in Effect

// BAD: Effect for button click result
function ProductPage({ product, addToCart }) {
  useEffect(() => {
    if (product.isInCart) {
      showNotification(`Added ${product.name}!`)
    }
  }, [product])

  function handleBuyClick() {
    addToCart(product)
  }
}

// GOOD: Handle in event handler
function ProductPage({ product, addToCart }) {
  function handleBuyClick() {
    addToCart(product)
    showNotification(`Added ${product.name}!`)
  }
}

Why it's bad: Effect fires on page refresh (isInCart is true), showing notification unexpectedly.


5. Chains of Effects

// BAD: Effects triggering each other
function Game() {
  const [card, setCard] = useState(null)
  const [goldCardCount, setGoldCardCount] = useState(0)
  const [round, setRound] = useState(1)
  const [isGameOver, setIsGameOver] = useState(false)

  useEffect(() => {
    if (card?.gold) setGoldCardCount((c) => c + 1)
  }, [card])

  useEffect(() => {
    if (goldCardCount > 3) {
      setRound((r) => r + 1)
      setGoldCardCount(0)
    }
  }, [goldCardCount])

  useEffect(() => {
    if (round > 5) setIsGameOver(true)
  }, [round])
}

// GOOD: Calculate in event handler
function Game() {
  const [card, setCard] = useState(null)
  const [goldCardCount, setGoldCardCount] = useState(0)
  const [round, setRound] = useState(1)
  const isGameOver = round > 5 // Derived!

  function handlePlaceCard(nextCard) {
    if (isGameOver) throw Error('Game ended')

    setCard(nextCard)
    if (nextCard.gold) {
      if (goldCardCount < 3) {
        setGoldCardCount(goldCardCount + 1)
      } else {
        setGoldCardCount(0)
        setRound(round + 1)
        if (round === 5) alert('Good game!')
      }
    }
  }
}

Why it's bad: Multiple re-renders (setCard -> setGoldCardCount -> setRound -> setIsGameOver). Also fragile for features like history replay.


6. Notifying Parent via Effect

// BAD: Effect to notify parent
function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false)

  useEffect(() => {
    onChange(isOn)
  }, [isOn, onChange])

  function handleClick() {
    setIsOn(!isOn)
  }
}

// GOOD: Notify in same event
function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false)

  function updateToggle(nextIsOn) {
    setIsOn(nextIsOn)
    onChange(nextIsOn) // Same event, batched render
  }

  function handleClick() {
    updateToggle(!isOn)
  }
}

// BEST: Fully controlled component
function Toggle({ isOn, onChange }) {
  function handleClick() {
    onChange(!isOn)
  }
}

7. Passing Data Up to Parent

// BAD: Child fetches, passes up via Effect
function Parent() {
  const [data, setData] = useState(null)
  return <Child onFetched={setData} />
}

function Child({ onFetched }) {
  const data = useSomeAPI()

  useEffect(() => {
    if (data) onFetched(data)
  }, [onFetched, data])
}

// GOOD: Parent fetches, passes down
function Parent() {
  const data = useSomeAPI()
  return <Child data={data} />
}

Why: Data should flow down. Upward flow via Effects makes debugging hard.


8. Fetching Without Cleanup (Race Condition)

// BAD: No cleanup - race condition
function SearchResults({ query }) {
  const [results, setResults] = useState([])

  useEffect(() => {
    fetchResults(query).then((json) => {
      setResults(json) // "hello" response may arrive after "hell"
    })
  }, [query])
}

// GOOD: Cleanup ignores stale responses
function SearchResults({ query }) {
  const [results, setResults] = useState([])

  useEffect(() => {
    let ignore = false

    fetchResults(query).then((json) => {
      if (!ignore) setResults(json)
    })

    return () => {
      ignore = true
    }
  }, [query])
}

9. App Initialization in Effect

// BAD: Runs twice in dev, may break auth
function App() {
  useEffect(() => {
    loadDataFromLocalStorage()
    checkAuthToken() // May invalidate token on second call!
  }, [])
}

// GOOD: Module-level guard
let didInit = false

function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true
      loadDataFromLocalStorage()
      checkAuthToken()
    }
  }, [])
}

// ALSO GOOD: Module-level execution
if (typeof window !== 'undefined') {
  checkAuthToken()
  loadDataFromLocalStorage()
}