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.
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:
- Move the function inside the effect if only the effect uses it.
- Move the function outside the component if it uses no props or state.
- Wrap it in
useCallbackwhen 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, manualuseCallbackanduseMemoare 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
handleRemovePlacepassed toDeleteConfirmationasonConfirm.
Code compare
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.
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 usingeffect()sparingly and preferringcomputed()orlinkedSignal()for derived state. useCallbackonly helps if the consumer compares by reference (an effect's dependency array or amemo()child). Wrapping every handler adds noise without benefit.
Quick reference
| React | Angular |
|---|---|
useCallback(fn, deps) | Class method (always stable) |
| Function recreated every render | Method created once |
setX inside an effect that depends on x → loop | Writing a signal the effect reads → loop |
| React Compiler auto-memoizes | Not applicable |