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

> Discover how Fre's update() function schedules reconciliation efficiently. Learn to defer UI updates for optimal responsiveness with this key Fre.js feature.

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

---

**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](https://github.com/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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/src/reconcile.ts). The scheduler, implemented in [`src/schedule.ts`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/src/reconcile.ts). This transitions the component from fallback UI to resolved content without blocking user interactions.

## Practical Code Examples

### Initial Render

```typescript
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()

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

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