What Is the Fre bubble() Function? Processing Hook Effects in the Reconciler
The bubble() function in Fre executes component side-effects after reconciliation, running layout effects synchronously while scheduling normal effects to run after the DOM commit phase.
The bubble() function serves as the critical bridge between Fre's render phase and commit phase, ensuring hook effects run with the correct timing semantics. Located in the core reconciler at src/reconcile.ts, this function processes effect hooks collected during component rendering in the frejs/fre repository.
How bubble() Works in Fre's Reconciler
The Fre bubble() function resides in src/reconcile.ts and executes after a fiber's children have been fully reconciled. The reconciler invokes bubble() via the sibling() function, passing the current fiber to process any pending side-effects.
// src/reconcile.ts:32-39
const bubble = (fiber: Fiber) => {
if (fiber.isComp) {
if (fiber.hooks) {
// ① run layout-effects immediately
side(fiber.hooks.layout)
// ② schedule normal effects to run after the commit phase
schedule(() => side(fiber.hooks.effect) as undefined)
}
}
}
When bubble() receives a component fiber, it checks for the presence of hooks. If hooks exist, it immediately executes layout effects through the side() helper, then queues normal effects using the schedule() utility to defer execution until after the DOM update completes.
The Side Effect Runner: Understanding the side() Helper
Both effect types delegate to the side() function defined in the same file. This utility manages the cleanup and registration cycle for hook effects:
// src/reconcile.ts:34-38
const side = (effects?: HookEffect[]) => {
effects.forEach((e) => e[2] && e[2]()) // run cleanup from previous render
effects.forEach((e) => (e[2] = e[0]())) // store new cleanup function
effects.length = 0
}
The side() function iterates over an array of HookEffect tuples. For each effect, it first invokes any existing cleanup function stored at index 2 of the tuple, then executes the effect callback at index 0 and stores the returned cleanup function back into index 2. Finally, it clears the effects array to prevent duplicate executions.
Effect Timing: Immediate vs. Scheduled Execution
The Fre bubble() function distinguishes between two effect categories with different execution timing:
- Layout effects (
useLayoutEffect-style): Stored infiber.hooks.layout, these execute synchronously insidebubble()during the render phase. This ensures measurements or DOM mutations occur before the browser paints. - Normal effects (
useEffect-style): Stored infiber.hooks.effect, these are asynchronously scheduled to run after the commit phase completes. This prevents blocking the visual update with non-urgent side-effects.
This implementation mirrors React's commit-after-render model, where layout effects fire immediately after calculating the DOM tree but before painting, while standard effects fire after the visual update is complete.
Practical Code Examples
Using useEffect in a Fre Component
import { useEffect } from "fre"
function Counter() {
useEffect(() => {
console.log("Mounted")
return () => console.log("Unmounted")
}, [])
return <div>🧮</div>
}
When Fre renders this component:
updateHook()builds the fiber and records the effect infiber.hooks.effect.- After children process,
sibling()callsbubble(fiber). bubble()schedules the effect viaschedule(() => side(fiber.hooks.effect)).- Following the DOM commit, the callback executes and logs "Mounted".
- If the component unmounts, the cleanup function stored at
e[2]runs before the next effect execution.
Using useLayoutEffect for Synchronous Measurements
import { useLayoutEffect, useRef } from "fre"
function Measure() {
const ref = useRef(null)
useLayoutEffect(() => {
const rect = ref.current?.getBoundingClientRect()
console.log("size:", rect?.width, rect?.height)
}, [])
return <div ref={ref}>Measure me</div>
}
Here, useLayoutEffect stores its effect in fiber.hooks.layout. During the same render pass, bubble() calls side(fiber.hooks.layout) immediately, allowing the component to measure DOM nodes before the browser paints. This synchronous execution matches the semantics of React's useLayoutEffect hook.
Key Source Files
The effect processing system spans several core files in the frejs/fre repository:
src/reconcile.ts: Contains thebubble()function andside()helper that orchestrate effect execution during the reconciliation process.src/hook.ts: Defines howuseEffectanduseLayoutEffectpopulatefiber.hookswithHookEffectentries during component rendering.src/type.ts: Declares the TypeScript definitions forHookEffecttuples and theHookListstructure used by the reconciler.src/schedule.ts: Provides theschedule()function thatbubble()uses to defer normal effect execution until after the DOM commit.
Summary
- The Fre bubble() function lives in
src/reconcile.tsand processes hook effects after child reconciliation completes. - It executes layout effects synchronously via
side(fiber.hooks.layout)during the render phase. - It schedules normal effects asynchronously using
schedule(() => side(fiber.hooks.effect))to run after the DOM commit. - The
side()helper manages the cleanup cycle by calling previous cleanup functions ate[2]before registering new ones. - This architecture ensures compatibility with React's effect timing semantics while maintaining a lightweight implementation.
Frequently Asked Questions
Where is the bubble() function defined in Fre?
The bubble() function is defined in src/reconcile.ts at lines 32-39. The reconciler invokes it through the sibling() function after processing a fiber's children, ensuring effects run only once the component subtree has been fully reconciled.
What is the difference between layout effects and normal effects in bubble()?
Layout effects (triggered by useLayoutEffect) execute immediately inside bubble() via side(fiber.hooks.layout), allowing synchronous DOM measurements before painting. Normal effects (triggered by useEffect) are deferred via schedule() to run after the commit phase, preventing blocking of the visual update.
How does bubble() handle effect cleanup?
Through the side() helper function, bubble() first iterates over effects to invoke any existing cleanup functions stored at index 2 of the HookEffect tuple. It then executes the new effect callback at index 0, stores the returned cleanup function back into index 2, and clears the effects array to prevent duplicate processing.
When is bubble() called during the render cycle?
bubble() is called after a fiber's children have been reconciled, specifically within the sibling() function in src/reconcile.ts. This placement ensures that parent component effects execute only after all descendant components have completed their rendering and reconciliation phases.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →