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 insrc/type.tsat 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 includingupdateHook,getBoundary, andsuspenseRenderfunctions that handle Promise detection and dual-fragment rendering.src/h.ts: Exports theSuspensecomponent wrapper andlazyfactory function that enables code-splitting workflows with minimal overhead.src/type.ts: Defines theSUSPENSE_FALLBACK_KEYconstant andMODEflags used to distinguish fallback fragments from primary content during reconciliation.
Summary
- Fre Suspense detects asynchronous operations by catching thrown Promises in the reconciler's
updateHookfunction (lines 61-66 ofsrc/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.OFFSCREENand theSUSPENSE_FALLBACK_KEYidentifier. - Promise resolution triggers automatic UI swapping via the
suspendPromiseMapWeakMap, seamlessly replacing fallbacks with resolved content. - The
lazyhelper insrc/h.tssimplifies 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →