# What Is the Fre bubble() Function? Processing Hook Effects in the Reconciler

> Discover how Fre's bubble() function processes component side-effects. Learn about layout and normal effects running synchronously and after DOM commit for efficient reconciliation.

- Repository: [frejs/fre](https://github.com/frejs/fre)
- Tags: internals
- Published: 2026-03-02

---

**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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/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.

```typescript
// 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:

```typescript
// 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 in `fiber.hooks.layout`, these execute **synchronously** inside `bubble()` during the render phase. This ensures measurements or DOM mutations occur before the browser paints.
- **Normal effects** (`useEffect`-style): Stored in `fiber.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

```tsx
import { useEffect } from "fre"

function Counter() {
  useEffect(() => {
    console.log("Mounted")
    return () => console.log("Unmounted")
  }, [])

  return <div>🧮</div>
}

```

When Fre renders this component:

1. `updateHook()` builds the fiber and records the effect in `fiber.hooks.effect`.
2. After children process, `sibling()` calls `bubble(fiber)`.
3. `bubble()` schedules the effect via `schedule(() => side(fiber.hooks.effect))`.
4. Following the DOM commit, the callback executes and logs **"Mounted"**.
5. If the component unmounts, the cleanup function stored at `e[2]` runs before the next effect execution.

### Using useLayoutEffect for Synchronous Measurements

```tsx
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`](https://github.com/frejs/fre/blob/main/src/reconcile.ts)**: Contains the `bubble()` function and `side()` helper that orchestrate effect execution during the reconciliation process.
- **[`src/hook.ts`](https://github.com/frejs/fre/blob/main/src/hook.ts)**: Defines how `useEffect` and `useLayoutEffect` populate `fiber.hooks` with `HookEffect` entries during component rendering.
- **[`src/type.ts`](https://github.com/frejs/fre/blob/main/src/type.ts)**: Declares the TypeScript definitions for `HookEffect` tuples and the `HookList` structure used by the reconciler.
- **[`src/schedule.ts`](https://github.com/frejs/fre/blob/main/src/schedule.ts)**: Provides the `schedule()` function that `bubble()` uses to defer normal effect execution until after the DOM commit.

## Summary

- The **Fre bubble() function** lives in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) and 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 at `e[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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/src/reconcile.ts). This placement ensures that parent component effects execute only after all descendant components have completed their rendering and reconciliation phases.