Request Lifecycle¶
This document traces the complete lifecycle of a component request, from the HTML data attribute to a rendered React component in the browser. Two paths are covered: API mode (development) and static mode (production).
API mode (development)¶
In development, components are bundled on-the-fly by esbuild when first requested.
sequenceDiagram
participant HTML as HTML Page
participant Loader as loader.js
participant API as Python Bridge
participant Cache as BundleCache
participant ESBuild as esbuild
HTML->>Loader: DOMContentLoaded
Loader->>Loader: Find [data-wilco-component] elements
Loader->>API: GET /api/bundles/counter.js?v=abc123
API->>Cache: Check cache (name + mtime)
alt Cache hit
Cache-->>API: BundleResult
else Cache miss
API->>ESBuild: Bundle index.tsx
ESBuild-->>API: ESM code + source map
API->>Cache: Store (name, result, mtime)
end
API-->>Loader: JavaScript (Cache-Control: immutable)
Loader->>Loader: transformEsmToRuntime(code)
Loader->>Loader: new Function(transformed)()
Loader->>HTML: createRoot(container).render(<Component />)
Step-by-step breakdown¶
1. DOM discovery
When loader.js is loaded (with defer), it waits for DOMContentLoaded
and then scans the page for elements with the data-wilco-component attribute:
<div data-wilco-component="counter"
data-wilco-props='{"initialValue": 10}'
data-wilco-api="/api"
data-wilco-hash="abc123">
Loading...
</div>
2. Bundle fetch
For each discovered element, the loader calls loadComponent(name, apiBase, hash).
The fetch URL is constructed as {apiBase}/bundles/{name}.js?v={hash}.
If the same component is requested by multiple containers, the loader caches the Promise itself (not just the result), so only one network request is made.
3. Python bridge processing
The bridge’s get_bundle(name) method:
Checks the manifest first (if
build_dirwas provided)Looks up the component in the registry
Reads the source file’s mtime (modification time)
Checks the BundleCache with that mtime
On cache miss: calls
bundle_component()which runs esbuildStores the result in cache with the current mtime
4. ESM transformation
The browser receives ESM code that cannot run directly (browsers don’t support
bare module specifiers like import { useState } from "react"). The loader’s
transformEsmToRuntime function rewrites it:
// Input (from esbuild)
import { useState, useEffect } from "react";
import { useComponent } from "@wilcojs/react";
var Counter = function({ initialValue }) { /* ... */ };
export { Counter as default };
// Output (after transformation)
const { useState, useEffect } = window.__MODULES__["react"];
const { useComponent } = window.__MODULES__["@wilcojs/react"];
var Counter = function({ initialValue }) { /* ... */ };
return Counter;
The transformation handles four import patterns:
ESM syntax |
Transformed to |
|---|---|
|
|
|
|
|
|
|
|
Source map comments (//# sourceMappingURL=...) are preserved through the
transformation and reattached at the end.
5. Component execution and rendering
The transformed code is executed via new Function(code)(), which returns the
default-exported React component. The loader then:
Creates a React root with
createRoot(container)Wraps the component in a
SuspenseWrapper(to supportuseComponentcalls inside the component)Wraps in an
ErrorBoundary(to catch render errors)Renders with the parsed props from
data-wilco-props
Static mode (production)¶
In production, pre-built bundles are served as static files. The API is not involved in bundle delivery.
sequenceDiagram
participant HTML as HTML Page
participant Loader as loader.js
participant Manifest as window.staticManifest
participant Static as Static Server
HTML->>Loader: DOMContentLoaded
Loader->>Loader: Find [data-wilco-component] elements
Loader->>Manifest: Look up "counter"
Manifest-->>Loader: {file: "bundles/counter.a1b2c3.js", hash: "a1b2c3"}
Loader->>Static: GET /static/wilco/bundles/counter.a1b2c3.js
Static-->>Loader: JavaScript (Cache-Control: immutable)
Loader->>Loader: transformEsmToRuntime(code)
Loader->>Loader: new Function(transformed)()
Loader->>HTML: createRoot(container).render(<Component />)
How static mode activates¶
The server-rendered HTML includes manifest data that the loader detects:
<script>
window.staticManifest = {"counter": {"file": "bundles/counter.a1b2.js", "hash": "a1b2"}};
window.staticManifestBaseUrl = "/static/wilco/";
</script>
<script src="/static/wilco/loader.js" defer></script>
In Django, the {% wilco_loader_script %} template tag generates this
automatically when WILCO_BUILD_DIR is configured.
When window.staticManifest exists, the loader:
Looks up the component name in the manifest
Fetches from
{staticManifestBaseUrl}{file}instead of the APIFalls back to API mode if the component is not in the manifest
This fallback allows incremental adoption: pre-build some components while others are still bundled at runtime.
Manifest persistence¶
The manifest variables are stored on the window object rather than as
module-level variables inside the IIFE. This ensures they survive when
loader.js is included multiple times on the same page (e.g., once from a
template, once from an admin widget). Each IIFE execution uses a guard pattern:
window.staticManifest = window.staticManifest || null;
window.staticManifestBaseUrl = window.staticManifestBaseUrl || "";
Error handling¶
Errors can occur at every step of the lifecycle. Here’s what happens at each failure point:
Failure point |
Error type |
User-visible result |
|---|---|---|
Invalid |
JSON parse error |
Error logged to console, error message rendered in container |
Component not in registry |
HTTP 404 |
“Failed to load component: {name}” rendered in container (red text) |
esbuild not found |
|
HTTP 500, error logged server-side |
esbuild compilation fails |
|
HTTP 500, esbuild stderr logged server-side |
Network error (fetch fails) |
|
“Failed to load component: {name}” rendered in container |
ESM transform fails |
Compilation error |
|
Component throws during render |
React render error |
Caught by |
|
Suspense + HTTP 404 |
Error propagates to nearest |
Multi-component pages¶
When a page contains multiple wilco components:
sequenceDiagram
participant Page as HTML Page
participant Loader as loader.js
participant API as API / Static
Page->>Loader: DOMContentLoaded
Note over Loader: Found 3 containers:<br/>counter, product, counter
par Parallel fetches
Loader->>API: GET counter.js
Loader->>API: GET product.js
end
Note over Loader: counter fetched once<br/>(promise deduplication)
API-->>Loader: counter bundle
API-->>Loader: product bundle
Loader->>Page: Render counter (container 1)
Loader->>Page: Render product (container 2)
Loader->>Page: Render counter (container 3, reuses cached bundle)
All components share:
A single React instance (from
window.__MODULES__["react"])The same module registry
The same bundle cache (loaded components are never re-fetched)
Each component gets its own createRoot() call and independent React tree.
See also¶
System Architecture — high-level system overview
Bundling and esbuild — esbuild configuration and source map details
JavaScript Architecture — JavaScript API reference
HTTP Caching — caching specification