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, settingfiber.dirty = trueand queuing tasks rather than executing immediately. - Cooperative scheduling via
src/schedule.tsbatches multipleupdate()calls and processes them during idle periods usingflush()andshouldYield(). - Integration points include initial renders, hook state changes in
src/hook.ts, and suspense resolutions atsrc/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →