Effects & Lifecycle
React’s reconciler records effect work on fibers, then executes that work during commit. Layout effects are handled as part of the synchronous commit path, while passive effects are queued for a later flush.
This separation exists so cleanup and setup happen in a controlled order, while passive work does not unnecessarily extend the main commit sequence. The work loop tracks pending passive effects and schedules them independently when a commit contains passive work.
Sources: packages/react-reconciler/src/ReactFiberCommitEffects.js:96-111, packages/react-reconciler/src/ReactFiberCommitEffects.js:304-315, packages/react-reconciler/src/ReactFiberWorkLoop.js:3725-3922
Core concepts
Effect tags
An effect tag is a bitmask that lets React select which effects to process. Layout is 0b0100, while Passive is 0b1000.
React stores function-component effects in the fiber’s update queue as a circular list. commitHookEffectListMount starts from lastEffect.next, visits each effect once, and runs only effects whose tag matches the requested flags.
Sources: packages/react-reconciler/src/ReactHookEffectTags.js:19, packages/react-reconciler/src/ReactHookEffectTags.js:20, packages/react-reconciler/src/ReactFiberCommitEffects.js:140-246
Layout effects
A layout effect is an effect selected with the layout hook flags and mounted through commitHookLayoutEffects. Its cleanup is destroyed during the mutation phase, before layout effect creation begins.
This timing allows all layout cleanups to run before any layout setup in the same commit, preventing one component’s cleanup from interfering with another component’s newly created effect or ref.
Sources: packages/react-reconciler/src/ReactFiberCommitEffects.js:96-111, packages/react-reconciler/src/ReactFiberCommitEffects.js:113-138
Passive effects
A passive effect is an effect selected with passive hook flags and mounted through commitHookPassiveMountEffects. Passive cleanup uses the corresponding commitHookPassiveUnmountEffects path.
Passive effects are not run directly by the initial commitRoot call when a separate callback can be scheduled. If the finished tree contains passive work, React schedules a normal-priority callback whose body calls flushPassiveEffects.
Sources: packages/react-reconciler/src/ReactFiberCommitEffects.js:304-315, packages/react-reconciler/src/ReactFiberCommitEffects.js:317-337, packages/react-reconciler/src/ReactFiberWorkLoop.js:3725-3922
How effects are selected and executed
Mounting an effect
When React mounts a selected effect, commitHookEffectListMount reads the effect’s create function, invokes it, and stores the returned value as the effect instance’s destroy function. In development, React also checks that the return value is either undefined or a function; returning another value produces a warning.
The same list walker serves both layout and passive effects. The wrapper determines the timing category: commitHookLayoutEffects invokes the list walker for layout work, and commitHookPassiveMountEffects invokes it for passive work. Profiling can wrap either operation with an effect timer and duration recording.
Sources: packages/react-reconciler/src/ReactFiberCommitEffects.js:140-246, packages/react-reconciler/src/ReactFiberCommitEffects.js:96-111, packages/react-reconciler/src/ReactFiberCommitEffects.js:304-315
Cleaning up an effect
Cleanup walks the same circular effect list and reads effect.inst.destroy. When a destroy function exists, React first clears the stored reference, then calls safelyCallDestroy. Clearing the reference prevents the same cleanup from being invoked twice through that effect instance.
Errors thrown by effect creation or cleanup are routed to captureCommitPhaseError, which searches upward for a host root or an applicable class error boundary and schedules an error update when one is found.
Sources: packages/react-reconciler/src/ReactFiberCommitEffects.js:248-302, packages/react-reconciler/src/ReactFiberCommitEffects.js:140-246, packages/react-reconciler/src/ReactFiberWorkLoop.js:4913-4968
How a commit orders lifecycle work
The commit path first identifies remaining lanes and then checks whether the finished tree contains passive work. When passive work exists, commitRoot schedules its callback as early as possible so it is queued before other work that may be scheduled during commit.
The broader pending-effects flush runs mutation effects, then layout effects, then spawned work, and finally passive effects. This establishes the practical timing difference: layout work belongs to the immediate commit sequence, while passive work is processed by flushPassiveEffects after that sequence has been arranged.
Evidence
- commit-rootpackages/react-reconciler/src/ReactFiberWorkLoop.js:3725
- callback-queuepackages/react-reconciler/src/ReactFiberWorkLoop.js:3725
- callback-queuepackages/react-reconciler/src/ReactFiberWorkLoop.js:5584
- mutation-phasepackages/react-reconciler/src/ReactFiberWorkLoop.js:4662
- layout-phasepackages/react-reconciler/src/ReactFiberWorkLoop.js:4662
- passive-phasepackages/react-reconciler/src/ReactFiberWorkLoop.js:3725
- passive-phasepackages/react-reconciler/src/ReactFiberWorkLoop.js:4662
The sequence shows that the commit path schedules passive work, while the pending-effects path orders mutation work before layout work and passive work.
Sources: packages/react-reconciler/src/ReactFiberWorkLoop.js:3725-3922, packages/react-reconciler/src/ReactFiberWorkLoop.js:4662-4689, packages/react-reconciler/src/ReactFiberWorkLoop.js:5584-5599
Re-render cleanup and setup
For layout effects, the ordering is explicit: React destroys layout effects during mutation, then mounts layout effects afterward. Thus, on a re-render where an effect is selected for both cleanup and setup, the old layout cleanup runs before the new layout effect body.
For passive effects, React exposes separate unmount and mount wrappers around the same list walker. The unmount path extracts and clears the old destroy function before calling it; the mount path invokes the new create function and records its returned cleanup.
Effect execution can schedule more updates. scheduleUpdateOnFiber records that an update occurred while passive effects were flushing, and the work loop separately tracks nested passive update counts. React reports a maximum update-depth warning when repeated useEffect updates exceed that limit.
Sources: packages/react-reconciler/src/ReactFiberCommitEffects.js:113-138, packages/react-reconciler/src/ReactFiberCommitEffects.js:96-111, packages/react-reconciler/src/ReactFiberCommitEffects.js:317-337, packages/react-reconciler/src/ReactFiberCommitEffects.js:304-315, packages/react-reconciler/src/ReactFiberCommitEffects.js:248-302, packages/react-reconciler/src/ReactFiberCommitEffects.js:140-246, packages/react-reconciler/src/ReactFiberWorkLoop.js:987-1113, packages/react-reconciler/src/ReactFiberWorkLoop.js:5222-5301
How it connects
The render work loop builds and completes fibers before commit begins. performUnitOfWork calls beginWork, and when no child work remains, completeUnitOfWork calls completeWork and continues through siblings or returns to the parent.
The commit machinery is part of the broader Commit Phase. Hook dispatch and effect registration connect to Hooks Implementation. The public behavior of the two hooks is described in useEffect & Cleanup.
Effect errors hand back to the work loop through captureCommitPhaseError, which can schedule an error-boundary update through ensureRootIsScheduled.
Sources: packages/react-reconciler/src/ReactFiberWorkLoop.js:3081-3123, packages/react-reconciler/src/ReactFiberWorkLoop.js:3368-3431, packages/react-reconciler/src/ReactFiberWorkLoop.js:4913-4968
Key takeaways
LayoutandPassiveare separate effect-selection bitmasks.- Layout cleanup runs during mutation before layout setup.
- Passive cleanup and setup use separate commit wrappers and are flushed later.
- Effect cleanup references are cleared before destruction to avoid double invocation.
- Errors and updates from effects re-enter the reconciler’s scheduling and error paths.