How Fre Implements Suspense for Asynchronous Operations: A Deep Dive into the 2KB Framework

Fre’s Suspense mechanism catches thrown Promises in the reconciler to immediately render fallback UI while keeping primary children off-screen, automatically swapping content when asynchronous work completes.

Fre is a lightweight React alternative that brings concurrent features to a sub-2KB package. Its Suspense implementation handles asynchronous operations by intercepting thrown Promises at the reconciler level, enabling graceful loading states without manual state management. According to the frejs/fre source code, this architecture mirrors React's concurrent-mode Suspense while maintaining a minimal footprint.

How Fre Suspense Works in the Reconciler

Fre’s Suspense architecture centers on the reconciler in src/reconcile.ts, which treats thrown Promises as suspension signals rather than errors. The system follows a four-phase process to manage asynchronous rendering gracefully.

Detecting Thrown Promises in updateHook

When a component throws a Promise during rendering, Fre’s updateHook function catches it between lines 61-66 in src/reconcile.ts. This detection triggers the suspension protocol immediately, distinguishing between actual errors and intentional asynchronous suspensions.

Locating the Nearest Boundary with getBoundary

The reconciler locates the nearest <Suspense> boundary using the getBoundary function (lines 46-53 in src/reconcile.ts). This traversal ensures that fallback rendering occurs at the correct component boundary, even when deeply nested children initiate the suspension.

Dual Fragment Rendering Strategy

Fre implements a unique dual-fragment approach in the suspenseRender function (lines 71-84, src/reconcile.ts):

  • Primary children are wrapped in an off-screen fragment using MODE.OFFSCREEN, preserving component state without DOM visibility.
  • Fallback content receives the special key SUSPENSE_FALLBACK_KEY (defined in src/type.ts at line 5) and renders immediately while the primary content remains hidden.
  • Both fragments reconcile simultaneously through reconcileChildren, ensuring instant fallback display without layout thrashing.

Promise Resolution and Content Swapping

The reconciler tracks pending Promises in a suspendPromiseMap WeakMap. When the Promise resolves (lines 76-82), Fre schedules an update for all registered boundaries, swapping the off-screen primary content into view and removing the fallback fragment atomically.

Using Fre Suspense in Practice

Developers interact with Suspense through the thin wrapper exported from src/h.ts (lines 80-82) and the lazy helper (lines 59-78).

Basic Suspense with Lazy Loading

The most common pattern combines lazy with Suspense for code splitting:

import { render, lazy, Suspense, h } from 'fre'

const LazyMessage = lazy(() => import('./Message'))

function App() {
  return (
    <Suspense fallback={<div>Loading…</div>}>
      <LazyMessage />
    </Suspense>
  )
}

render(<App />, document.body)

When LazyMessage executes the dynamic import, it throws the resulting Promise. Fre’s reconciler catches this in updateHook, renders the fallback via suspenseRender, and automatically swaps to the resolved component once the import completes.

Manual Promise Suspension

You can suspend components manually by throwing Promises directly:

import { Suspense, h } from 'fre'

function AsyncData() {
  throw new Promise(resolve => {
    setTimeout(() => resolve(), 1000)
  })
  // Unreachable until Promise resolves
}

function App() {
  return (
    <Suspense fallback={<span>Fetching data…</span>}>
      <AsyncData />
    </Suspense>
  )
}

The thrown Promise triggers the same boundary detection and fallback rendering logic, suspending the component tree for one second before revealing the content.

Dynamic Imports Without Lazy

While lazy simplifies the pattern, you can implement custom suspension logic:

import { Suspense, h } from 'fre'

const HeavyChart = () => {
  const modulePromise = import('./HeavyChart')
  throw modulePromise.then(m => { HeavyChart.Component = m.default })
}

function Dashboard() {
  return (
    <Suspense fallback={<div>Initializing chart…</div>}>
      <HeavyChart />
    </Suspense>
  )
}

The lazy utility factory in src/h.ts simply automates this pattern, creating a wrapper component that enters a loading state and throws the import Promise for the nearest Suspense boundary to catch.

Core Implementation Files

Understanding Fre’s Suspense requires examining three critical source files:

  • src/reconcile.ts: Contains the core suspension logic including updateHook, getBoundary, and suspenseRender functions that handle Promise detection and dual-fragment rendering.
  • src/h.ts: Exports the Suspense component wrapper and lazy factory function that enables code-splitting workflows with minimal overhead.
  • src/type.ts: Defines the SUSPENSE_FALLBACK_KEY constant and MODE flags used to distinguish fallback fragments from primary content during reconciliation.

Summary

  • Fre Suspense detects asynchronous operations by catching thrown Promises in the reconciler's updateHook function (lines 61-66 of src/reconcile.ts).
  • The system locates the nearest boundary using getBoundary (lines 46-53), ensuring fallback rendering occurs at the correct component scope.
  • Dual-fragment rendering keeps primary children off-screen while displaying fallback content immediately, using MODE.OFFSCREEN and the SUSPENSE_FALLBACK_KEY identifier.
  • Promise resolution triggers automatic UI swapping via the suspendPromiseMap WeakMap, seamlessly replacing fallbacks with resolved content.
  • The lazy helper in src/h.ts simplifies dynamic imports by automatically throwing Promises that Suspense boundaries can catch and track.

Frequently Asked Questions

How does Fre Suspense compare to React Suspense?

Fre Suspense implements the same fundamental pattern as React's concurrent mode—catching thrown Promises to defer rendering—but achieves this in a fraction of the bundle size. Both use boundary-based fallback rendering, though Fre employs a dual-fragment off-screen approach in suspenseRender rather than React's fiber-based interruption model.

Can I use Fre Suspense with data fetching libraries?

Yes. Any function that returns a Promise can trigger Suspense by throwing that Promise during component render. Data fetchers should cache Promises externally and throw them when data is unavailable, allowing Fre's reconciler to catch the Promise in updateHook and render the fallback until resolution.

What happens if a Promise rejects inside a Suspense boundary?

Fre's current Suspense implementation focuses on resolution handling via suspendPromiseMap. Rejected Promises would propagate as standard errors unless caught by an error boundary. The reconciler detects the Promise in lines 61-66 of src/reconcile.ts, but rejection handling typically requires additional error boundary configuration beyond the base Suspense mechanism.

Is the lazy helper required for code splitting with Suspense?

No. While the lazy helper in src/h.ts (lines 59-78) automates the pattern of throwing import Promises, you can manually throw Promises in any component. The helper simply provides a standardized factory for dynamic imports that integrates cleanly with Fre's Suspense boundaries.

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 →