Handling Side Effects & Working with the useEffect() HookEntry 6 of 7

Infinite Loops, Functions as Dependencies, and useCallback

useCallback(fn, deps) returns the same function instance across renders until one of its dependencies changes.

react.devuseCallback

Notes

Loop 1: updating a dependency inside its own effect.

useEffect(() => {  setCount(count + 1); // changes count...}, [count]); // ...which re-runs the effect → infinite loop

The fix is almost always to not use an effect. If count should change in response to something, do it in that event's handler.

Loop 2: functions as dependencies. A function declared in the component body is recreated on every render, so it's a "new" dependency every time. The effect then re-runs after every render. On its own, that's wasteful. It becomes an infinite loop only when the effect (or the function) updates state, which causes another render, which creates another new function.

In the course example, the parent passes onConfirm to DeleteConfirmation. If onConfirm updates the parent's state, the parent re-renders, onConfirm becomes a new function, and the child's timer effect restarts.

Fixes, in order of preference:

  1. Move the function inside the effect if only the effect uses it.
  2. Move the function outside the component if it uses no props or state.
  3. Wrap it in useCallback when it must be passed as a prop or shared.

useCallback dependencies follow the same rules as useEffect: include every prop, state value, or component-scoped variable the function reads. State setters, dispatch, and refs can be left out.

React Compiler note: Your notes said React 19 itself reduces the need for useCallback. The feature that does this is the React Compiler (stable since late 2025), a separate build plugin that memoizes functions and values automatically. With the compiler enabled, manual useCallback and useMemo are rarely needed. Without it, the rules above still apply.

What / Why / When

What

A memoized function reference.

Why

Keeps a function's identity stable so effects and memo() children don't re-run needlessly.

When

A function is an effect dependency, or is passed to a child wrapped in memo().

Use case

A stable handleRemovePlace passed to DeleteConfirmation as onConfirm.

Code compare

React
import { useCallback, useRef, useState } from "react";​export function App() {  const [pickedPlaces, setPickedPlaces] = useState<Place[]>([]);  const [modalIsOpen, setModalIsOpen] = useState(false);  const selectedPlaceId = useRef<string | null>(null);​  // Stable identity: no dependencies, because setters and refs are stable  const handleRemovePlace = useCallback(() => {    setPickedPlaces((prev) => prev.filter((place) => place.id !== selectedPlaceId.current));    setModalIsOpen(false);  }, []);​  return (    <Modal open={modalIsOpen} onClose={() => setModalIsOpen(false)}>      <DeleteConfirmation        onConfirm={handleRemovePlace} // safe as an effect dependency in the child        onCancel={() => setModalIsOpen(false)}      />    </Modal>  );}

Class methods are created once per instance, so their identity never changes, and there is no dependency array to keep in sync. There's no useCallback equivalent because none is needed.

Loops are still possible in Angular: an effect that writes to a signal it also reads will re-trigger itself.

Angular
constructor() {  // ❌ Reads and writes count: re-triggers itself  effect(() => this.count.set(this.count() + 1));}​// ✅ Derive instead of syncingdoubled = computed(() => this.count() * 2);

Differences and gotchas

  • Your notes said Angular has "no infinite loop risk" and called effect() "more predictable." Angular removes the function-identity problem, but it doesn't remove loops. Angular's docs also recommend using effect() sparingly and preferring computed() or linkedSignal() for derived state.
  • useCallback only helps if the consumer compares by reference (an effect's dependency array or a memo() child). Wrapping every handler adds noise without benefit.

Quick reference

ReactAngular
useCallback(fn, deps)Class method (always stable)
Function recreated every renderMethod created once
setX inside an effect that depends on x → loopWriting a signal the effect reads → loop
React Compiler auto-memoizesNot applicable