=================
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.
.. mermaid::
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()
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:
.. code-block:: html
Loading...
**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:
1. Checks the **manifest** first (if ``build_dir`` was provided)
2. Looks up the component in the **registry**
3. Reads the source file's **mtime** (modification time)
4. Checks the **BundleCache** with that mtime
5. On cache miss: calls ``bundle_component()`` which runs esbuild
6. Stores 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:
.. code-block:: javascript
// 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:
.. list-table::
:widths: 50 50
:header-rows: 1
* - ESM syntax
- Transformed to
* - ``import { a, b } from "mod"``
- ``const { a, b } = window.__MODULES__["mod"]``
* - ``import Default from "mod"``
- ``const Default = window.__MODULES__["mod"].default || window.__MODULES__["mod"]``
* - ``import * as Mod from "mod"``
- ``const Mod = window.__MODULES__["mod"]``
* - ``export { X as default }``
- ``return X`` (at end of function body)
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:
1. Creates a React root with ``createRoot(container)``
2. Wraps the component in a ``SuspenseWrapper`` (to support ``useComponent``
calls inside the component)
3. Wraps in an ``ErrorBoundary`` (to catch render errors)
4. 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.
.. mermaid::
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()
How static mode activates
-------------------------
The server-rendered HTML includes manifest data that the loader detects:
.. code-block:: html
In Django, the ``{% wilco_loader_script %}`` template tag generates this
automatically when ``WILCO_BUILD_DIR`` is configured.
When ``window.staticManifest`` exists, the loader:
1. Looks up the component name in the manifest
2. Fetches from ``{staticManifestBaseUrl}{file}`` instead of the API
3. Falls 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:
.. code-block:: javascript
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:
.. list-table::
:widths: 25 35 40
:header-rows: 1
* - Failure point
- Error type
- User-visible result
* - Invalid ``data-wilco-props`` JSON
- 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
- ``BundlerNotFoundError``
- HTTP 500, error logged server-side
* - esbuild compilation fails
- ``RuntimeError``
- HTTP 500, esbuild stderr logged server-side
* - Network error (fetch fails)
- ``TypeError``
- "Failed to load component: {name}" rendered in container
* - ESM transform fails
- Compilation error
- ``console.error`` with details, error rendered in container
* - Component throws during render
- React render error
- Caught by ``ErrorBoundary``, error message displayed
* - ``useComponent`` target missing
- Suspense + HTTP 404
- Error propagates to nearest ``ErrorBoundary``
Multi-component pages
=====================
When a page contains multiple wilco components:
.. mermaid::
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:
counter, product, counter
par Parallel fetches
Loader->>API: GET counter.js
Loader->>API: GET product.js
end
Note over Loader: counter fetched once
(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
========
- :doc:`architecture` — high-level system overview
- :doc:`bundling` — esbuild configuration and source map details
- :doc:`/reference/javascript` — JavaScript API reference
- :doc:`/specs/http-caching` — caching specification