useEffect & Cleanup
useEffect records a callback and its dependency array during function-component rendering, then the commit phase mounts or unmounts the recorded effect.
This separation lets React compare dependencies before deciding whether an effect needs execution, retain cleanup state in the effect instance, and route effect errors through commit-phase handling.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:4053-4061, packages/react-reconciler/src/ReactFiberHooks.js:4386-4393, packages/react-reconciler/src/ReactFiberCommitEffects.js:140-246, packages/react-reconciler/src/ReactFiberCommitEffects.js:248-302, packages/react-reconciler/src/ReactFiberHooks.js:2657-2695
Core concepts
Effect record
An effect record stores the callback, dependency array, mutable effect instance, and link to the next effect in a circular list.
The effect instance begins with destroy unset, and pushEffectImpl inserts the record into the function component update queue.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:2588-2603, packages/react-reconciler/src/ReactFiberHooks.js:2605-2622, packages/react-reconciler/src/ReactFiberHooks.js:2624-2626
Dependency array
A dependency array is the optional deps value used to decide whether an updated effect should run its callback again.
areHookInputsEqual compares corresponding entries with is; it returns false when there is no previous array or an entry differs, and returns true when every compared entry matches.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:2640-2655, packages/react-reconciler/src/ReactFiberHooks.js:2657-2695, packages/react-reconciler/src/ReactFiberHooks.js:456-503
Cleanup
A cleanup is the function returned by an effect callback and stored as inst.destroy after the callback runs.
During unmount, React clears inst.destroy before calling it through safelyCallDestroy.
Sources: packages/react-reconciler/src/ReactFiberCommitEffects.js:140-246, packages/react-reconciler/src/ReactFiberCommitEffects.js:261-280
Closure snapshot
A closure snapshot is the particular create function captured by one render and stored in that render’s effect record.
If dependencies compare equal, React stores the new callback with the existing effect instance but without HookHasEffect, so the commit traversal does not select that record when it requires the effect tag.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:2588-2603, packages/react-reconciler/src/ReactFiberHooks.js:2657-2695, packages/react-reconciler/src/ReactFiberHooks.js:2671-2683, packages/react-reconciler/src/ReactFiberCommitEffects.js:140-152
How React records an effect
On mount, the useEffect dispatcher checks the dependency argument in development and calls mountEffect.
mountEffect chooses passive-effect flags and delegates to mountEffectImpl; the implementation obtains a hook node, converts an omitted dependency argument to null, marks the fiber, creates an effect instance, and pushes the effect record.
On update, the dispatcher calls updateEffect, which delegates to updateEffectImpl with the passive hook flag.
The update implementation obtains the current hook, reads the previous effect and dependencies, and compares the new dependencies when they are not null. Equal dependencies produce an effect without HookHasEffect; changed or absent dependencies produce an effect with HookHasEffect.
The effect list is circular: the queue stores lastEffect, whose next points to the first effect, and each new record is linked into that cycle.
The render-to-recording call order is:
Evidence
- use-effectpackages/react-reconciler/src/ReactFiberHooks.js:4386
- update-effectpackages/react-reconciler/src/ReactFiberHooks.js:2721
- effect-implementationpackages/react-reconciler/src/ReactFiberHooks.js:2657
- dependency-comparisonpackages/react-reconciler/src/ReactFiberHooks.js:456
- effect-listpackages/react-reconciler/src/ReactFiberHooks.js:2588
The important detail is that pushSimpleEffect delegates to pushEffectImpl, which creates or updates the circular effect list on the currently rendering fiber.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:4053-4061, packages/react-reconciler/src/ReactFiberHooks.js:2697-2719, packages/react-reconciler/src/ReactFiberHooks.js:2640-2655, packages/react-reconciler/src/ReactFiberHooks.js:4386-4393, packages/react-reconciler/src/ReactFiberHooks.js:2721-2726, packages/react-reconciler/src/ReactFiberHooks.js:2657-2695, packages/react-reconciler/src/ReactFiberHooks.js:2605-2622, packages/react-reconciler/src/ReactFiberHooks.js:2588-2603
How dependencies decide whether to rerun
React compares corresponding dependency entries rather than comparing the dependency-array object as a whole in the shown implementation. For each position, areHookInputsEqual calls is; a mismatch returns false, while completing the loop returns true.
When all compared entries match, updateEffectImpl pushes the new create function with the existing inst and without HookHasEffect. The commit traversal only mounts records whose tags satisfy (effect.tag & flags) === flags, so this record is not selected for that requested effect tag.
When an entry differs, or no previous dependency array exists, updateEffectImpl marks the fiber and pushes the effect with HookHasEffect | hookFlags; updateEffect supplies HookPassive as that hook flag.
In development, React warns if the dependency argument changes between absent and present, or if the dependency array changes length; its diagnostic says the array’s order and size must remain constant.
A stale closure can appear because areHookInputsEqual only compares the values present in the dependency arrays. A changing value omitted from both arrays therefore escapes this comparison. If the remaining entries compare equal, the newly captured callback is stored without HookHasEffect, so the commit traversal does not select it for mounting. The exhaustive-deps rule specifically warns that referenced values omitted from dependency arrays can lead to stale UI or callbacks.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:495-503, packages/react-reconciler/src/ReactFiberHooks.js:2671-2683, packages/react-reconciler/src/ReactFiberCommitEffects.js:140-152, packages/react-reconciler/src/ReactFiberHooks.js:2685-2694, packages/react-reconciler/src/ReactFiberHooks.js:2721-2726, packages/react-reconciler/src/ReactFiberHooks.js:467-493
How cleanup behaves during commit and unmount
commitHookEffectListMount walks the circular effect list, selects records whose tags match the requested flags, calls effect.create, and stores the returned value in effect.inst.destroy. In development, it checks that the return value is a function or undefined; returning a Promise or another value produces a diagnostic because the return value is reserved for cleanup.
For passive effects, commitHookPassiveMountEffects delegates to the mount traversal, while commitHookPassiveUnmountEffects delegates to the unmount traversal. When a function component is unmounted, ReactFiberCommitWork.js calls commitHookPassiveUnmountEffects for its passive hooks.
The unmount traversal reads inst.destroy, clears it immediately, and calls safelyCallDestroy with the finished fiber and nearest mounted ancestor. A cleanup error is caught by the traversal and passed to captureCommitPhaseError.
If an effect callback is still executing, the shown mount code does not interrupt that callback: inst.destroy is assigned only after create() returns. Thus, the shown code establishes cleanup only after the callback has produced its return value.
Sources: packages/react-reconciler/src/ReactFiberCommitEffects.js:140-176, packages/react-reconciler/src/ReactFiberCommitEffects.js:186-235, packages/react-reconciler/src/ReactFiberCommitEffects.js:304-315, packages/react-reconciler/src/ReactFiberCommitEffects.js:317-337, packages/react-reconciler/src/ReactFiberCommitWork.js:5387-5397, packages/react-reconciler/src/ReactFiberCommitEffects.js:261-280, packages/react-reconciler/src/ReactFiberCommitEffects.js:299-301, packages/react-reconciler/src/ReactFiberCommitEffects.js:161-176, packages/react-reconciler/src/ReactFiberCommitEffects.js:140-246
How it connects
renderWithHooks installs a mount or update dispatcher based on whether the current fiber has hook state, and it finishes by calling finishRenderingHooks; the dispatcher exposes the useEffect entry point. See Hooks Implementation for dispatcher selection and hook-list mechanics.
Effect mounting and unmounting are commit-phase hand-offs through commitHookPassiveMountEffects and commitHookPassiveUnmountEffects. See Commit Phase for the broader commit flow.
The passive-versus-layout distinction and effect ordering are covered by Effects & Lifecycle. Layout cleanup is explicitly performed before layout creates during the commit sequence.
Static dependency guidance is covered by ESLint Plugin: react-hooks, whose exhaustive-deps rule reports referenced values that are absent from dependency arrays.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:505-567, packages/react-reconciler/src/ReactFiberHooks.js:4053-4061, packages/react-reconciler/src/ReactFiberHooks.js:4386-4393, packages/react-reconciler/src/ReactFiberCommitEffects.js:304-315, packages/react-reconciler/src/ReactFiberCommitEffects.js:317-337, packages/react-reconciler/src/ReactFiberCommitEffects.js:113-138
Key takeaways
useEffectrecords a callback, dependencies, and cleanup storage in a circular effect list.- Dependency entries are compared pairwise with
is; equal entries suppressHookHasEffect. - A stale closure can result when a changing value is omitted from the dependency array.
- Unmount reads and clears the stored cleanup before calling it through
safelyCallDestroy. - A cleanup is stored only after the effect callback returns.
Sources: packages/react-reconciler/src/ReactFiberHooks.js:2588-2603, packages/react-reconciler/src/ReactFiberHooks.js:2605-2622, packages/react-reconciler/src/ReactFiberHooks.js:456-503, packages/react-reconciler/src/ReactFiberHooks.js:2657-2695, packages/react-reconciler/src/ReactFiberHooks.js:495-503, packages/react-reconciler/src/ReactFiberHooks.js:2671-2683, packages/react-reconciler/src/ReactFiberCommitEffects.js:248-302, packages/react-reconciler/src/ReactFiberCommitEffects.js:140-246