A component runs once when it mounts. Keep state in its closure. Props arrive as a getter; call it inside a reactive expression to read the latest value.
Elements receive connect and disconnect events. Tie subscriptions to the component lifecycle with an abort signal.
Lifecycle
import { createAbortSignal } from'radi';functionPanel(this:HTMLElement) {constsignal=createAbortSignal(this); window.addEventListener('resize', () => {// Respond to the new viewport. }, { signal });return <div>Ready when you are.</div>;}
Use radi/router for SPA navigation without replacing the document. Keep shared layout outside a route outlet. Routes can load code lazily, read server data through loaders, and mutate it through actions followed by revalidation.
router.ts
import { createElement as h, createRoot } from'radi';import { createRouter, fetchRouteData } from'radi/router';constroot=createRoot(document.getElementById('outlet')!);constrouter=createRouter({ routes: [{ path: '/pages/:id',loader: ({ params, signal }) =>fetchRouteData('/api/pages/'+encodeURIComponent(params.id), { signal }, ), }],});router.subscribe((state) => {if (state.status !=='idle') return;if (state.error) root.render(h('p', null, 'Could not load this page.'));else root.render(h('pre', null, JSON.stringify(state.data, null, 2)));});router.attach();await router.ready;// Dispose when the application is removed: router.dispose();
Serve the application shell for direct visits to these URLs. The example expects a JSON endpoint implemented by your server. Authorization and persistence remain on the server; fresh data can still require a request.
The optional initialData setting reuses data serialized by your server. Rendering an outlet replaces its existing DOM; it does not hydrate it. Routes are flat, with static paths, :parameters, and a final * wildcard. This is not Remix API compatibility.
Caching is disabled by default. Enable staleTime and a bounded cacheSize to reuse prefetched data. Invalidate on account changes. New navigation aborts obsolete loads, but aborting an action cannot undo a server mutation.
Use the separate server entry to render HTML without a browser. Choose synchronous rendering, resolved async HTML, or ordered streaming. For JSX on the server, set jsxImportSource to radi/server.
server.ts
import { createElement as h, renderToString } from'radi/server';consthtml=renderToString(h('h1', null, 'Hello from the server.'));
Keep application components in TypeScript/TSX while Go owns HTTP, authorization, and Flop access. The Go adapter embeds V8 in the server process and streams ordered HTML. It uses neither a Node/Deno serving process nor a persisted bytecode cache.
Register the page operation in Go and return its Flop result. data() resolves that result as a Promise. Bundle the TSX with radi-build, embed the JavaScript source with go:embed, and pass it to radi.New from github.com/Marcisbee/radi/go. The checked-in example includes the complete Go host and database setup.
Run these commands from a Radi checkout with Go 1.26.10 and Clang 21. Linux amd64 is the qualified target. The Go-based bundler needs no JavaScript runtime at build time. Each request gets fresh JavaScript state; the default single worker reuses its V8 instance and compiled source.
The adapter bounds queued work, data calls, response bytes, and execution time. Slow readers apply backpressure; disconnects cancel operations. Use trusted application code, external memory limits and a restart policy: the V8 heap setting is not a hard memory sandbox. Go operations must respect cancellation. Pair the adapter with radi/router for client navigation and route data handling through Go HTTP endpoints.
This guide is built with Radi’s static site generator. Routes become complete HTML files. Your build owns the filesystem writer, asset bundling, and deployment; no Radi server runs in production.
Add explicit resumable boundaries to static HTML. Their handlers update existing DOM without rendering the whole page again. This example includes its handler in the initial script, so the first click needs no additional download.
importtype { ResumeContext } from'radi/resume';exportfunctionincrement({ state, boundary }:ResumeContext) {constcounter= state as { count:number }; boundary.querySelector('output')!.textContent =String(++counter.count);}
Bundle the client entry as a classic script and pass its public URL as the static page’s bootstrap. Dynamic imports are optional: use them for larger, rarely used interactions when the download delay is acceptable.
This is explicit resumption: arbitrary closures are not serialized. Use native links and forms for navigation. The current island API supports registered clicks.
Why are islands explicit?
Function children identify dynamic output, but do not identify a browser module or preserve a closure’s captured state. Radi currently has no compiler to extract those functions. A wrapper could generate registry entries for explicit module references, but could not automatically resume arbitrary components.
Creates one browser router per window. Routes specify path, loader, action, optional lazy module import, and optional view. Loader context contains url, decoded params, and signal. Action context also contains formData and method. initialData skips the first loader; staleTime enables data caching and cacheSize bounds entries (default 20).
State exposes url, match, module, data, actionData, error, and status (idle, loading, or submitting). subscribe immediately calls its listener. Existing data stays visible while busy. Render the outlet or an error when idle; the router never calls a module’s optional view itself.
Loads a same-origin route, then updates history. Back/forward resolves the browser URL. New navigation aborts obsolete work. Hash-only navigation skips the loader. Unmatched programmatic navigation produces a 404 RouteError.
Intercepts ordinary clicks on matching internal links. Modified clicks, downloads, external links, explicit targets, and data-radi-reload stay native. Forms are not automatically intercepted.
Runs the destination action, invalidates cached data, and reloads its loader. Supports POST (default), PUT, PATCH, and DELETE. Action results appear in actionData; a missing action produces 405.
Independent data state without navigation, scrolling, route notifications or automatic loader revalidation. load runs a fresh loader; submit stores action data in fetcher.state.data. Mutations invalidate all cache entries unless an explicit URL list is supplied ([] opts out). Errors and RouteRedirect stay in fetcher state. New work aborts older work on this fetcher; dispose it when its component unmounts. Cancellation cannot undo server writes.
revalidate clears cached data and reloads the current browser URL. invalidate() clears all cached data; invalidate([url, ...]) clears selected pathname/search keys without navigating. Use full invalidation after identity changes.
Loads route code and data without navigating. Data reuse requires a positive staleTime. Cache keys include pathname and search; hits do not extend expiry. Catch speculative failures. Prefetch does not follow redirects.
constdata=awaitfetchRouteData<Page>('/api/page', { signal });
Fetches JSON with Accept: application/json. Accepts JSON or 204; non-success responses throw RouteError with status and response data. A successful X-Radi-Location response redirects. The type parameter describes your contract, not runtime validation.
Throws a navigation redirect from a loader or action. Only same-origin destinations are supported and chains are limited to ten hops. Ordinary HTTP redirects followed by fetch are not route navigation.
Defines the application entry consumed by the Go adapter. Export it as app and bundle with radi-build, which selects server JSX and the Radi global. render runs once per request in a fresh V8 context. Components may return promises; content streams in document order. The returned start/pull/settle methods are the host protocol, not browser routing APIs.
Invokes an operation registered in the Go handler for this request. Input and output cross the bridge as JSON. The type parameter does not validate runtime data. Unknown operations, quota violations, invalid results, operation errors and panics reject the promise. Authorize and validate inside the Go operation; its context carries request cancellation and middleware values. This API is unavailable in the browser.
Request metadata supplied to defineApp. url is the request URI (path and query), not an absolute URL. Header names use Go HTTP canonical casing. The adapter accepts GET; mount separate Go handlers for mutations and other HTTP methods. No request body is exposed.
constroot=createRoot(target: HTMLElement);root.render(element);root.root; // current node, or nullroot.unmount();
Creates a DOM render root. The first render clears existing children; this is not hydration. Rendering the same component type and key reuses its host and updates props. Unmount disconnects the rendered tree and runs cleanup.
import { createElement as h, createRoot, Fragment } from'radi';consttarget= document.body.appendChild(document.createElement('div'));constview=h('main', { className: 'greeting' },h(Fragment, null,h('h1', null, 'Hello, Radi'),h('p', null, 'These siblings share no extra wrapper.'), ),);createRoot(target).render(view);
Creates DOM elements or component hosts. Components receive a props getter and run once on mount. Function children and attributes are reactive; Fragment groups children without an element wrapper. Use JSX for normal authoring.
Synchronously reevaluates reactive expressions on the target and its descendants. Returns the dispatch result. Update the ancestor containing the expressions that read your changed state.
Returns a reactive function. The first evaluation computes its value; later updates retain it when shouldMemo returns true. Return false when dependencies change.
Returns a keyed-list descriptor for use inside a reactive child function. Use stable, unique keys. Updates reuse keyed content and reconcile its order; removed items are disconnected.
Displays its fallback while descendants are suspended. Overlapping suspensions are counted. Async components participate automatically; manual work must balance suspend and unsuspend.
Dispatch bubbling, cancelable suspend/unsuspend events. Return the dispatch result. Call unsuspend when each manual operation finishes, including failure paths.
functionComponent(this:ComponentNode, props: () =>Props) {// this is the component host}
Type for the DOM component host. Lifecycle events are connect, disconnect, and update. Props are read by calling their getter, including inside reactive expressions.
Creates lazy server nodes, text, comments, and fragments. These are separate from client DOM and legacy adapter nodes. Components and function children execute during rendering. Text and attributes are escaped; no raw-HTML API is exposed.
Synchronous rendering; promises are rejected. renderToStringRoot is an alias. ssr calls the supplied entry and renders its result. Component errors produce an ERROR:ComponentName fallback; invalid children and reactive errors fail the render.
Streams ordered UTF-8 HTML with backpressure. Available HTML flushes before awaiting a promise; later siblings wait. Cancellation stops future traversal, but cannot cancel application promises already running. No out-of-order replacement scripts are emitted.
typeServerComponent<P> = (props: () =>P) =>ServerChild;// ServerNode: lazy server element representation// ServerChild: nodes, primitives, arrays, functions,// promises, and subscribable values
ServerChild, ServerComponent, and ServerNode are exported types. Subscribables supply their first synchronous value and are immediately unsubscribed. DOM lifecycle events do not run. Event, key, ref, and function attributes are omitted. Use explicit CSS units.
Creates an explicit island boundary and binds a click to a client registry key. See Resumption API for state, event, and delivery constraints. Legacy createRenderer, createDomAdapter, createServerStringAdapter, and SERVER_RENDERER are also re-exported; use them with adapter nodes only.
Install once per root before interactive HTML is parsed. Avoid overlapping roots. Returns an idempotent disposer that removes capture, aborts pending delivery, and releases cached loaders. It does not mount or rerender the page.
constloaders= {increment: () =>import('./handler').then(m=> m.increment),};// Or bundle the handler up front:// increment: () => Promise.resolve(increment)
A record of trusted handler keys to asynchronous loader functions. Loaded handlers are cached; a failed load can retry on a later click. Lazy imports are optional and can delay the first interaction.
typeResumeHandler= (context:ResumeContext) =>void|Promise<void>;// context: boundary, element, event, state, signal
boundary is the island element; element carries the click binding; event is the original MouseEvent (currentTarget is no longer valid). Read or replace state and mutate existing DOM. Honor signal for asynchronous work. Handlers run serially within a boundary. Removed targets are skipped.
Defaults are 64 queued/running clicks per root and a 30,000 ms wait for handler loading and streamed HTML readiness. Overflow is dropped and reported. The timeout does not forcibly stop application handler code.
Exported by radi/server and radi/resume. State is public HTML: never include secrets. Only finite numbers and JSON values in arrays/plain objects are accepted. Cycles, functions, undefined, BigInt, and class instances are rejected.
Handler keys must be nonempty and at most 256 characters. Supported boundary tags: div, section, article, main, aside, nav, span, li, td. Registered clicks suppress native defaults and propagation, then deliver once to the handler. Use type="button". Navigation, forms, input/change, gestures, and privileged user-activation actions are not replayed.
Validates all routes first, then renders and awaits each write sequentially. Returns generated relative paths. / maps to index.html; /guide/ maps to guide/index.html. Duplicate routes, dot segments, encoded paths, queries, and fragments are rejected. Unicode slugs are supported.
path, title, and render are required. render returns ServerChild, including promises. lang defaults to en; head and bootstrap are optional. bootstrap is a public URL for a bundled classic script, placed in the head before interactive content. Without it, no script is generated.
Owns filesystem writes, asset bundling/copying, cleanup, and deployment. Build into a fresh output directory: failures do not roll back earlier writes, and removed routes are not deleted. Deploy directory indexes; use the correct asset URL prefix for subdirectories.
Creates a callable signal. Writes notify connected subscriber nodes through update. A mapper call creates a reactive child function; it is not a functional setter. Subscribers attach through Radi connect/disconnect events. The current overload types do not fully express the mapper’s returned function.
Creates a DOM-scoped channel with a default value and unique key. provide attaches a value to an element; use resolves the nearest live provider after connection. Before resolution it reads the default, and writes are ignored.
Accessors read the current value. set changes it and updates the provider subtree; update forces a refresh after in-place mutation. provider is an Element or null, and resolved indicates provider discovery. Channel accessor types are inferred, not separately exported.
Legacy universal renderer for custom targets. render appends children to a UniversalNode root. renderToString exists only if the adapter supplies serialize. This is a one-shot abstraction, not full client reactivity or the lazy server streaming API.
Adapter factories target DOM or virtual string nodes. DOM_RENDERER and SERVER_RENDERER are prebuilt renderer instances. Keep creation and rendering within the same API; client DOM nodes, lazy server nodes, and UniversalNode wrappers are not interchangeable.
Implement node creation, mutation, navigation, and text detection. isCommentNode and serialize are optional. Exported types include RendererAdapter, Renderer, UniversalNode, and ComponentType. UniversalNode wraps a platform node with optional children, text, comment, tag, props, and key metadata.
Compiler-facing JSX factories. Set jsxImportSource to radi for DOM or radi/server for server nodes. Development entries re-export the runtime; jsxDEV accepts source metadata. The example shows calls normally generated by the JSX compiler: children live in props; jsx takes one child and jsxs takes multiple. The optional third argument is the key. jsxDEV also takes the static-children flag and optional source/self metadata. Server JSX exposes its own JSX namespace, including Element, ElementType, LibraryManagedAttributes, ElementChildrenAttribute, and IntrinsicElements.
isSubscribable(value): value is Subscribable<unknown>isReactiveGenerator(value): value is ReactiveGeneratortoChildArray(child): Child[]
radi/types exports Child, ReactiveGenerator, and Subscribable<T>. Subscribable.subscribe(listener) may return void, a cleanup function, or an unsubscribe object. The guards test for a subscribe method or a function respectively. toChildArray wraps a single child; it does not flatten nested arrays.
radi/symbols exposes runtime bookkeeping symbols for low-level integrations. Ordinary application code should use the public rendering and state APIs rather than mutate these fields.