What Is Fre's update() Function? Scheduling Reconciliation in the Fre Framework

Fre's update() function marks fibers as dirty and queues them for the cooperative scheduler, deferring reconciliation to a yield-aware flush loop that preserves UI responsiveness.

Fre's update() function serves as the central entry point for all reactive changes in the frejs/fre repository. When components need to re-render due to state mutations, prop changes, or suspense resolutions, this function initiates the scheduling process without immediately executing DOM operations. Understanding how update() coordinates with the internal scheduler is essential for grasping Fre's concurrent rendering architecture.

How Fre's update() Function Schedules Reconciliation

Marking Fibers as Dirty

In src/reconcile.ts, the update() function receives a fiber node representing the component requiring re-rendering. If the fiber is not already flagged, the function sets fiber.dirty = true at lines 32-35, indicating that the virtual DOM tree needs reconciliation. This dirty flag prevents redundant scheduling and ensures that multiple rapid state changes coalesce into a single reconciliation pass.

Delegating to the Cooperative Scheduler

Rather than performing work immediately, update() passes the dirty fiber to schedule() at lines 35-36 in src/reconcile.ts. The scheduler, implemented in src/schedule.ts, maintains a global task queue and manages execution timing. At lines 12-15, schedule() pushes a callback object onto the queue and invokes startTransition(flush), initiating a transition that will process the fiber tree when the browser is idle.

The Flush Loop and Yielding Mechanism

The flush() function in src/schedule.ts (lines 32-46) drives the actual reconciliation work. It repeatedly retrieves the next job from the queue using peek(queue) and executes the associated callback—which triggers the internal reconcile loop—while shouldYield() returns false.

Yielding logic at lines 49-51 compares the current time against a deadline calculated as deadline = getTime() + threshold. When this deadline is reached, flush() stops execution, leaving remaining work in the queue to resume later via requestIdleCallback-like mechanisms such as MessageChannel, queueMicrotask, or setTimeout. This cooperative multitasking prevents blocking the main thread during large component tree updates.

Committing Changes After Reconciliation

As the reconciliation walk progresses through the fiber tree, completed work triggers commit() (referenced at src/reconcile.ts:18-27), which applies DOM mutations. Only after this phase does the fiber become clean, completing the lifecycle initiated by update().

Where update() Is Invoked in the Fre Codebase

Initial Mounts and Renders

During the initial render, the render() function creates a root fiber and immediately calls update(rootFiber) to schedule the first reconciliation pass. This establishes the fiber tree and commits the initial DOM structure.

State Changes via Hooks

Hooks such as useState and useReducer in src/hook.ts invoke update(currentFiber) after detecting state mutations. For example, when setCount executes in a component, it triggers update() on the fiber owning that state, scheduling a re-render that respects the cooperative scheduler's timing constraints.

Suspense and Error Boundaries

When promises resolve within Suspense boundaries or errors are caught, the framework re-queues boundary fibers via update(b) at lines 80-81 in src/reconcile.ts. This transitions the component from fallback UI to resolved content without blocking user interactions.

Practical Code Examples

Initial Render

import { render } from "./reconcile";

const App = () => <h1>Hello Fre</h1>;
const root = document.getElementById("root")!;

// update() is called internally, scheduling reconciliation
render(<App />, root);

State Update Triggering update()

import { useState } from "./hook";

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      {count}
    </button>
  );
}

// Clicking the button invokes setCount, which calls update(currentFiber)
// The scheduler batches this update and runs reconciliation when idle

Suspense Resolution

import { Suspense } from "./h";

function DataComponent() {
  const data = fetchData(); // Throws promise
  return <div>{data}</div>;
}

// When the promise resolves, update(boundary) is called at reconcile.ts:80-81
// This schedules the transition from fallback to actual content

Summary

  • Fre's update() function acts as the gateway for all reconciliation work, setting fiber.dirty = true and queuing tasks rather than executing immediately.
  • Cooperative scheduling via src/schedule.ts batches multiple update() calls and processes them during idle periods using flush() and shouldYield().
  • Integration points include initial renders, hook state changes in src/hook.ts, and suspense resolutions at src/reconcile.ts:80-81.
  • Non-blocking execution is achieved through time-slicing that yields control back to the browser when deadlines are exceeded.

Frequently Asked Questions

Does Fre's update() function perform DOM mutations directly?

No. The update() function only marks fibers as dirty and schedules work. Actual DOM mutations occur later in the commit() phase (located in src/commit.ts), which executes after the reconciliation walk completes successfully.

How does update() handle multiple rapid state changes?

Fre coalesces multiple update() calls into a single reconciliation pass. By setting the dirty flag and enqueueing to the scheduler's global queue, subsequent updates to the same fiber before the flush loop runs do not create redundant reconciliation work.

What determines when Fre yields during reconciliation?

The shouldYield() function in src/schedule.ts compares the current time against a deadline calculated from getTime() + threshold. When the deadline is reached, the flush() loop stops and remaining work stays queued for the next idle period.

Can update() be called manually outside of hooks?

While update() is primarily invoked internally by render() and hooks, advanced use cases such as suspense boundaries manually call update(b) on boundary fibers at src/reconcile.ts:80-81 to trigger re-renders when asynchronous operations complete.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →