Learn the essentials
Core APIs
Octane lets you build interactive pages from small TypeScript functions called components. This guide introduces the APIs you will use most, starting from zero.
The mental model
An Octane app is made from components. A component receives information, called props, and returns the elements that should appear on screen. When its state changes, Octane runs the component again and updates only the DOM that needs to change.
You do not call a component every time something changes. You describe the output,
attach event handlers such as onClick, and let Octane keep the page in sync.
Components and props
A component is a TypeScript function used with a capitalized JSX tag such as
<WelcomeCard />. Props are its inputs. They let a parent customize a child
without the child changing those values.
interface WelcomeCardProps {
name: string;
unread: number;
}
function WelcomeCard(props: WelcomeCardProps) @{
<section class="welcome-card">
<h2>{'Hello, ' + props.name}</h2>
<p>{'You have ' + props.unread + ' unread messages.'}</p>
</section>
}
export function App() @{
<main>
<WelcomeCard name="Maya" unread={3} />
</main>
}The @{ ... } form means “return the final JSX node.” It is shorthand for a normal
function body with return, so you can use whichever reads more clearly. A component
can also return text, an array, or null when it should render nothing.
Components compose by nesting. The special children prop contains whatever was put
between a component's opening and closing tags:
interface CardProps {
children: unknown;
}
function Card(props: CardProps) @{
<section class="card">{props.children}</section>
}
export function Profile() @{
<Card>
<h2>Ada Lovelace</h2>
<p>Mathematician and programmer</p>
</Card>
}Use a fragment (<>...</>) when you need to return several siblings without adding
another DOM element. APIs such as memo, lazy, and createPortal solve more specific
composition problems and are collected in the API index.
State and events
Props come from a parent. State is a component's own memory. Call useState with an
initial value; it returns the value for this render and a function that schedules the
next value.
import { useState } from 'octane';
export function Counter() @{
const [count, setCount] = useState(0);
<section>
<p>{'Items packed: ' + count}</p>
<button onClick={() => setCount((current) => current + 1)}>
Add one
</button>
</section>
}Here is that component running for real:
Items packed
The number is state. Each click updates it and renders again.
The flow is always the same:
useState(0)gives the first render a count of0.- Clicking the button runs its
onClickfunction. setCountcalculates the next value and asks Octane to render again.- The component sees the new count, and Octane updates the text in the DOM.
The value you read during a render is a snapshot. When the next value depends on the
previous one, the updater form—setCount(current => current + 1)—is the safest choice.
For objects and arrays, create a new value instead of mutating the existing one.
Text fields use the browser's native input event:
import { useState } from 'octane';
export function NameField() @{
const [name, setName] = useState('');
<section>
<label>
Your name
<input value={name} onInput={(event) => setName(event.currentTarget.value)} />
</label>
<p>
{name ? 'Hello, ' + name + '!' : 'Start typing above.'}
</p>
</section>
}When commit-on-blur is intentional, keep native onChange and mark that local host
explicitly. The hint is not rendered to the DOM and does not alter the event:
<input
defaultValue={savedDraft}
onChange={(event) => save(event.currentTarget.value)}
suppressNativeChangeWarning
/>No suppression is needed for select, checkbox/radio inputs, custom elements, or
component API props named onChange; they are outside the text-entry diagnostic.
For state with many related updates, useReducer(reducer, initialState) keeps the update
rules in one function. It supports the same optional third-item latest-state getter as
useState. Most components should begin with useState and move to a reducer only when
that makes the code easier to understand.
Octane deep dive: state getters and conditional hooks
Octane optionally provides a third state tuple item: const [value, setValue, getValue] = useState(initial). getValue() reads the latest scheduled value inside a long-lived callback; it does not subscribe or render. Most components only need the first two items.
Hooks are identified by their compiled call site, so they may appear after an early return or inside a condition. Do not put slot-keyed hooks in a plain JavaScript loop: every iteration would share one call site. The context and Promise readers use() and useContext() are the exceptions. Use a keyed @for block or extract a child component instead.
Lists and conditions
Real pages rarely show the same elements every time. In .tsrx, rendered control flow
uses directives. @if chooses a branch and a keyed @for keeps each list item's DOM and
state attached to the right data.
interface Item {
id: number;
label: string;
packed: boolean;
}
export function PackingList(props: { items: Item[] }) @{
<ul>
@for (const item of props.items; key item.id) {
<li>
{item.label}
@if (item.packed) {
<span aria-label="packed">✓</span>
}
</li>
} @empty {
<li>Your list is empty.</li>
}
</ul>
}Here is the same list with controls that exercise every branch, including the empty state:
Packing list
The keyed list preserves each row while conditions update its status.
1 of 3 packed
The key must be stable for that item—usually an ID from your data. Avoid using the array
position when items can be inserted, removed, or reordered. @switch handles several
exclusive branches, while @try handles loading and errors.
Sharing data with context
Passing props is usually the clearest way to move information. When many distant components need the same value—such as a theme, locale, or signed-in user—context lets a parent provide it to the whole subtree.
import { createContext, use, useState } from 'octane';
const Theme = createContext('light');
function ThemeLabel() @{
const theme = use(Theme);
<p>{'The current theme is ' + theme + '.'}</p>
}
export function Settings() @{
const [theme, setTheme] = useState('light');
<section>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Switch theme
</button>
<Theme.Provider value={theme}>
<ThemeLabel />
</Theme.Provider>
</section>
}createContext('light') creates the context and its fallback value. A provider makes a
value available below it. use(Theme) reads the closest provider; useContext(Theme) is
an equivalent, context-specific spelling.
Context is not automatically global state. Keep changing data close to the components that own it, then lift it to a shared parent or context when several branches truly need the same source of truth.
Refs and effects
Most components only need props and state. Refs and effects are escape hatches for things outside Octane's render flow: focusing an input, connecting to a service, starting a timer, or integrating a DOM library.
Refer to a DOM element with useRef
A ref is a stable object whose current property can hold a DOM element. Changing it
does not render the component again.
import { useRef } from 'octane';
export function SearchBox() @{
const inputRef = useRef<HTMLInputElement | null>(null);
<div>
<input ref={inputRef} aria-label="Search" />
<button onClick={() => inputRef.current?.focus()}>
Focus search
</button>
</div>
}Refs are ordinary props in Octane, so your own component can accept ref directly.
There is no forwardRef wrapper. A host element can also receive several refs at once,
for example ref={[inputRef, measurementRef]}.
Synchronize an external system with useEffect
Page metadata does not need an effect. Render <title> directly and Octane hoists it to
the document head:
export function Article(props: { title: string }) @{
<>
<title>{props.title}</title>
<article>
<h1>{props.title}</h1>
</article>
</>
}Use an effect when a component must subscribe to a system outside Octane. This version adds a global keyboard shortcut to the search field. The effect connects after the DOM commits and disconnects when the component leaves the page:
import { useEffect, useRef } from 'octane';
export function ShortcutSearch() @{
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
const focusSearch = (event: KeyboardEvent) => {
const target = event.target;
const isTyping =
target instanceof HTMLElement &&
(target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName));
if (
event.key !== '/' || event.metaKey || event.ctrlKey || event.altKey || event.isComposing ||
isTyping
) {
return;
}
event.preventDefault();
inputRef.current?.focus();
};
window.addEventListener('keydown', focusSearch);
return () => window.removeEventListener('keydown', focusSearch);
});
<div>
<input ref={inputRef} aria-label="Search" aria-keyshortcuts="/" />
<button onClick={() => inputRef.current?.focus()}>
Focus search
</button>
</div>
}Try both the ref-driven button and the effect-owned shortcut:
Shortcut search
The ref points to the input; the effect owns a global keyboard subscription.
Press /outside a form field, or use the button.
The only component value this effect captures is a stable ref, so it connects once and
disconnects on unmount. If an effect reads changing props or state, those values are
inferred as dependencies. You can still pass an explicit dependency array when you need
exact control; [] means mount and unmount only, while null deliberately means every
render. Locally declared custom hooks in full-compiled .tsrx/.tsx modules receive
the same inference when they transparently forward their callback and final dependency
parameter to a supported hook; plain .ts/.js, imported, or transforming wrappers
require an explicit list.
Subscribe to external state with useSyncExternalStore
Use useSyncExternalStore when the source of truth lives outside Octane and already
offers a subscription. Browser state, a framework-independent store, or a third-party
cache can stay authoritative instead of being copied into component state by an effect.
This status component subscribes to a real browser data source:
import { useSyncExternalStore } from 'octane';
function subscribe(onStoreChange: () => void) {
window.addEventListener('online', onStoreChange);
window.addEventListener('offline', onStoreChange);
return () => {
window.removeEventListener('online', onStoreChange);
window.removeEventListener('offline', onStoreChange);
};
}
function getSnapshot() {
return navigator.onLine;
}
function getServerSnapshot() {
return true;
}
export function NetworkStatus() @{
const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
<p role="status">
{isOnline ? 'Online' : 'Offline — changes will sync when you reconnect.'}
</p>
}subscribe receives the notification function and returns its cleanup. getSnapshot
reads the current value. Keep both functions stable when possible, and keep the same
object identity for an object snapshot until its contents actually change.
The optional third function supplies the server snapshot. Octane reads it during server
rendering and hydration, then synchronizes to getSnapshot in the browser. This example
uses a deterministic online server value; an application store can serialize its real
server snapshot instead.
useLayoutEffect runs earlier when you must measure or adjust layout before paint.
useInsertionEffect is for styling libraries. useEffectEvent creates a non-reactive
callback that an effect can call without reconnecting when its captures change. Its
wrapper identity is fresh on each render, but a wrapper registered by a committed effect
always calls the latest committed body; do not add Effect Events to dependency arrays.
Loading data and code
The use API reads a Promise during rendering. While it is pending, the closest
Suspense boundary shows a fallback. If it rejects, the closest ErrorBoundary shows
an error fallback.
import { ErrorBoundary, Suspense, use } from 'octane';
interface ProfileData {
name: string;
bio: string;
}
function Profile(props: { data: Promise<ProfileData> }) @{
const profile = use(props.data);
<article>
<h2>{profile.name}</h2>
<p>{profile.bio}</p>
</article>
}
export function ProfilePage(props: { data: Promise<ProfileData> }) @{
<ErrorBoundary fallback={<p>We could not load this profile.</p>}>
<Suspense fallback={<p>Loading profile…</p>}>
<Profile data={props.data} />
</Suspense>
</ErrorBoundary>
}The parent owns the loading and error experience; Profile can describe the successful
case directly. Keep the Promise stable by creating it in your data layer or router rather
than creating a new one on every render.
.tsrx also offers the same idea as template control flow:
export function ProfilePage(props: { data: Promise<ProfileData> }) @{
@try {
<Profile data={props.data} />
} @pending {
<p>Loading profile…</p>
} @catch (error) {
<p>{'Could not load profile: ' + (error instanceof Error ? error.message : String(error))}</p>
}
}This live version creates its Promise in an event handler, then shows the pending and success branches owned by the boundary:
Profile loader
The button creates one stable request; the boundary owns the pending UI.
Load the profile to see the boundary change.
lazy(() => import('./Settings.tsrx')) uses the same Suspense mechanism to load a
component's code only when it is needed.
Octane deep dive: parallel data loading
When independent use() calls can start together, the compiler does so automatically and suspends once for the group. Data that truly depends on an earlier result remains sequential. This avoids accidental loading waterfalls without changing the way you write the component.
Deferred hydration
Hydrate keeps useful server-rendered HTML visible while delaying the component work,
effects, and events that make that subtree interactive. By default, the compiler also
moves the direct children into a separate JavaScript chunk, so the browser does not
fetch that code until the boundary is prefetched or activated.
import { Hydrate } from 'octane';
import { visible } from 'octane/hydration';
export function ProductPage() @{
<main>
<ProductHero />
<Hydrate when={visible({ rootMargin: '400px' })}>
<Reviews />
</Hydrate>
</main>
}The server still renders Reviews. During initial hydration its HTML stays visible but
dormant. When the boundary approaches the viewport, Octane loads the generated child
chunk, adopts the same DOM in place, then enables refs, effects, and events.
Hydrate is experimental and applies only to matching server HTML in the initial
document. A boundary first mounted after the app is already running renders normally on
the client.
The three performance decisions
| Prop | Default | Decision |
|---|---|---|
when | required | When the preserved server HTML becomes interactive. |
split | true | Whether the compiler creates a deferred child chunk. |
prefetch | none | Whether code or data preparation starts before when fires. |
Choose the trigger with when
Import hydration strategies from octane/hydration. Use the trigger that matches how
soon the content needs to respond:
| Strategy | Example use |
|---|---|
load() | Hydrate with the initial app hydration. |
idle({ timeout? }) | Hydrate when the browser is idle. |
visible({ rootMargin?, threshold? }) | Hydrate near or inside the viewport. |
media(query) | Hydrate when a media query matches. |
interaction({ events? }) | Hydrate on intent and replay the triggering event. |
condition(booleanOrGetter) | Hydrate after an application condition becomes truthy. |
never() | Keep the initial server HTML permanently static. |
import { Hydrate } from 'octane';
import { condition, idle, interaction, load, media, never, visible } from 'octane/hydration';
export function StrategyExamples(props: { hasConsent: boolean }) @{
<>
<Hydrate when={load()}>
<SiteHeader />
</Hydrate>
<Hydrate when={idle()}>
<RelatedArticles />
</Hydrate>
<Hydrate when={visible()}>
<Reviews />
</Hydrate>
<Hydrate when={media('(min-width: 64rem)')}>
<DesktopChart />
</Hydrate>
<Hydrate when={interaction({ events: ['focusin', 'click'] })}>
<Editor />
</Hydrate>
<Hydrate when={condition(props.hasConsent)}>
<PersonalizedOffers />
</Hydrate>
<Hydrate when={never()}>
<LegalArchive />
</Hydrate>
</>
}The function form chooses a strategy from browser-only information. Octane never calls it on the server, and it must return a strategy synchronously:
import { interaction, visible } from 'octane/hydration';
<Hydrate when={() => window.matchMedia('(pointer: coarse)').matches ? interaction() : visible()}>
<Recommendations />
</Hydrate>Capture interactions before hydrateRoot()
An immediate hydrateRoot() call needs no extra setup: mounting the first Hydrate
boundary installs interaction capture as a synchronous fallback. If your client entry
awaits route discovery, data, or dynamic imports before it calls hydrateRoot(),
install the lightweight capture queue before that work so an interaction during the
gap can be replayed:
import { hydrateRoot } from 'octane';
import { initializeHydrationEventCapture } from 'octane/hydration';
initializeHydrationEventCapture();
await prepareClient();
hydrateRoot(document.getElementById('app')!, App);Calling initializeHydrationEventCapture() more than once is safe; Octane installs
the listeners only once per document.
Control the child chunk with split
Splitting is on by default; this boundary defers both component execution and the
HeavyReviewsWidget JavaScript:
<Hydrate when={visible()}>
<HeavyReviewsWidget />
</Hydrate>Use the literal split={false} when that code is already needed elsewhere or a separate
chunk would cost more than it saves. Hydration still waits for when:
<Hydrate when={idle()} split={false}>
<SmallBadge />
</Hydrate>The compiler can split direct JSX children and capture ordinary lexical values. Move hooks and other setup into a child component, or opt out of splitting.
Prepare before activation with prefetch
A strategy-form prefetch downloads the generated child chunk early without making the server HTML interactive. Here the browser may fetch the editor while idle, but the editor still activates only after interaction:
import { idle, interaction } from 'octane/hydration';
<Hydrate when={interaction()} prefetch={idle()}>
<RecommendationEditor />
</Hydrate>Strategy prefetching accepts load(), idle(), visible(), media(), or
interaction(). Conditions, never(), and the function form are activation-only
strategies.
A procedural prefetch can warm data as well as code. Awaited work blocks activation if
when fires first; the signal is aborted if the boundary goes away:
<Hydrate
when={visible()}
prefetch={async ({ preload, signal }) => {
await preload();
await warmReviews({ signal });
}}
>
<Reviews />
</Hydrate>Procedural prefetch also works with split={false}; in that mode preload() resolves
immediately. Its waitFor(strategy) helper accepts the same five prefetch strategies.
Strategy-form prefetch requires splitting because its job is to fetch the generated
child chunk.
Fallbacks, completion, and the complete prop surface
fallback is client-only loading UI for a boundary mounted later on the client whose
child suspends. It does not replace the preserved server HTML while an initial boundary
waits. onHydrated runs once after the child successfully commits on the client,
whether the boundary adopts preserved server DOM or mounts client-only:
<Hydrate
when={visible()}
fallback={<ReviewsSkeleton />}
onHydrated={() => markReviewsInteractive()}
>
<Reviews />
</Hydrate>| Prop | Type | Purpose |
|---|---|---|
when | HydrationStrategy | (() => HydrationStrategy) | Required activation trigger. |
split | boolean | Generate a deferred child chunk; defaults to true. |
prefetch | strategy or ({ preload, waitFor, signal, element }) | Prepare the split chunk or application resources early. |
fallback | renderable | Loading UI for a later client-only mount or suspension. |
onHydrated | () => void | Called once after the child successfully commits. |
children | renderable | The subtree rendered on the server and deferred. |
For compiler constraints, nesting behavior, event details, and the full procedural context, read the deferred hydration guide.
Responsive updates and actions
Some updates should feel immediate, such as typing. Others may reveal UI that needs to wait for data. A transition marks the second kind so Octane can keep the previous UI visible instead of replacing it with a loading fallback too soon.
Keep the current screen with useTransition
useTransition marks a screen change that can wait. Use it when the next view may need
time, but the current view is still useful—opening a dashboard tab, moving between
pages, or applying a large filter.
export function ProjectTabs() @{
const [tab, setTab] = useState('overview');
const [isPending, startTransition] = useTransition();
<>
<button onClick={() => startTransition(() => setTab('activity'))}>
{isPending ? 'Opening…' : 'Open activity'}
</button>
<TabPanel tab={tab} />
</>
}The live example keeps the current report on screen while another one loads. The
isPending flag gives you a place to explain what is happening instead of flashing a
full-page loading state.
Project dashboard
Open another report. The current one stays useful while its data loads.
Overview is ready.
Overview
Visitors across your active projects
24.8k
A transition does not make the work faster, and state that controls typing should stay
urgent. Read the React useTransition reference
for the full API; Octane uses the same component-level pattern.
Let a slow view lag with useDeferredValue
useDeferredValue gives one part of the page permission to be a step behind. A search
box can receive every keystroke immediately while a slower result list continues to show
the last useful result.
export function ProductSearch() @{
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
<>
<input value={query} onInput={(event) => setQuery(event.currentTarget.value)} />
<div aria-busy={isStale}>
<ProductResults query={deferredQuery} />
</div>
</>
}Type “camera” or “desk” below. The input owns the current value; the catalogue receives the deferred copy and keeps its old cards visible while the new search finishes.
Product search
The search box stays immediate while the slower result view catches up.
- Pocket cameraCategory: Photography
- Camera shoulder bagCategory: Photography
- Oak standing deskCategory: Workspace
- Desk task lampCategory: Workspace
- Travel headphonesCategory: Audio
- Studio microphoneCategory: Audio
This is not a debounce: there is no fixed delay, and it does not reduce network requests.
It only changes which render may wait. See the
React useDeferredValue reference
for initial values, Suspense, and performance details.
Animate visual changes with ViewTransition
ViewTransition handles a different part of the experience: how the old and new screens
visually connect. Wrap the element whose appearance or position changes, then make that
change inside a transition.
export function SavedNotice() @{
const [visible, setVisible] = useState(true);
<>
<button onClick={() => startTransition(() => setVisible((value) => !value))}>
Toggle notice
</button>
@if (visible) {
<ViewTransition enter="notice-in" exit="notice-out">
<p>Project saved.</p>
</ViewTransition>
}
</>
}The lab below turns the three everyday patterns into controls you can try: an item entering or leaving, one item moving between layouts, and tabs sliding in different directions. Browsers without the native API still make the same state changes; they just skip the animation.
Enter and exit
Shared element
Transition types
Read the React ViewTransition reference
for the component model, or the
full Octane View Transitions guide
for class maps, shared elements, and Octane's complete API.
Forms and actions
useActionState connects an async action to a form and gives you its result and pending
state:
import { useActionState } from 'octane';
async function saveName(previousMessage: string, formData: FormData) {
const name = String(formData.get('name') ?? '').trim();
if (!name) return 'Enter a name before saving.';
// Replace this delay with your API or database call.
await new Promise((resolve) => setTimeout(resolve, 500));
return 'Saved ' + name;
}
export function ProfileForm() @{
const [message, submit, isPending] = useActionState(saveName, '');
<form action={submit}>
<label>
Name
<input name="name" />
</label>
<button type="submit" disabled={isPending}>
{isPending ? 'Saving…' : 'Save'}
</button>
<p aria-live="polite">{message}</p>
</form>
}The field starts with a real value. Choose Save name to watch the pending and success states, or clear the field and submit it to see validation. The local delay stands in for a request, so the whole example works without a backend:
Async form action
Save the prefilled name to see pending and success states, or clear it to see validation.
useFormStatus lets a child of the form read its status. useOptimistic temporarily
shows the expected result while an action runs. requestFormReset(form) asks Octane to
reset uncontrolled fields after the surrounding action or transition settles.
Roots and rendering
A root connects an Octane component tree to an element in your HTML. Most apps create one root in their client entry file.
<!-- index.html -->
<div id="app"></div>// main.ts
import { createRoot } from 'octane';
import { App } from './App.tsrx';
const container = document.getElementById('app');
if (!container) {
throw new Error('Missing #app element');
}
const root = createRoot(container);
root.render(App);Call root.unmount() when another system removes the whole app. If the server already
sent this component's HTML, use hydrateRoot(container, App) instead; hydration adopts
that DOM and attaches Octane's events rather than building the page again.
Render an overlay with createPortal
A portal places UI in another DOM container without moving it out of the component tree. This is useful for modals, menus, and notification layers that need to escape clipping or stacking contexts.
<div id="app"></div>
<div id="toast-root"></div>import { createPortal, useState } from 'octane';
interface SavedToastProps {
target: HTMLElement;
onDismiss: () => void;
}
function ToastBody(props: { onDismiss: () => void }) @{
<aside class="toast" aria-label="Save notification">
<p role="status">Draft saved.</p>
<button type="button" onClick={props.onDismiss}>
Dismiss
</button>
</aside>
}
function SavedToast(props: SavedToastProps) {
return createPortal(ToastBody, props.target, {
onDismiss: props.onDismiss,
});
}
export function Editor(props: { toastRoot: HTMLElement }) @{
const [toastOpen, setToastOpen] = useState(false);
const [clicksSeen, setClicksSeen] = useState(0);
<>
<button type="button" onClick={() => setToastOpen(true)}>
Save draft
</button>
<section onClick={() => setClicksSeen((count) => count + 1)}>
<p>{'Toast clicks handled by the editor: ' + clicksSeen}</p>
@if (toastOpen) {
<SavedToast target={props.toastRoot} onDismiss={() => setToastOpen(false)} />
}
</section>
</>
}The toast is physically inside #toast-root, but it remains a logical child of the
editor's <section>. Clicking Dismiss runs the button handler and then bubbles to
the section handler, so its count increases. Context crosses the portal too, unmounting
the owner removes the portalled DOM, and stopPropagation() stops the logical ascent.
Native listeners installed separately with addEventListener still follow the physical
DOM path.
Try the same structure live. The toast appears in the right-hand DOM target; its dismiss event is observed by the logical parent on the left:
Clicks observed by the logical parent: 0
For production notifications, @octanejs/sonner
provides timers, stacking, actions, focus handling, and swipe dismissal:
import { Toaster, toast } from '@octanejs/sonner';
export function App() @{
<main>
<button onClick={() => toast.success('Project saved')}>Save</button>
<Toaster position="top-right" richColors />
</main>
}Sonner positions the <Toaster> host where you mount it rather than calling
createPortal; the direct example above is what demonstrates portal event bubbling.
Other rendering tools are intentionally less common:
flushSync(callback)forces pending work to finish before the next line. Reserve it for browser or third-party integrations that require the DOM immediately.act(callback)flushes renders and effects in tests. Application code should not call it.
Server and static rendering
Server rendering sends useful HTML before client JavaScript starts. Streaming and
single-pass APIs come from octane/server. prerender from octane/static waits for all
Suspense data and is useful whenever you want fully resolved HTML, often at build time.
| Goal | API | What it returns |
|---|---|---|
| Hydratable HTML in one synchronous pass | renderToString | { html, css } |
| HTML that will not hydrate | renderToStaticMarkup | { html, css } |
| Progressive Node.js response | renderToPipeableStream | A pipeable stream controller |
| Progressive Web response | renderToReadableStream | Promise<ReadableStream & { allReady: Promise<void> }> |
| Fully resolved HTML after all data | prerender from octane/static | Promise<{ html, css }> |
import { renderToString } from 'octane/server';
import { App } from './App.tsrx';
const { html, css } = renderToString(App);Put html in the page body and the deduplicated scoped css in <head>. The streaming
APIs can send the shell first and reveal Suspense content as it becomes ready.
API index by job
You do not need to memorize this list. Start with components, useState, and event
handlers; return here when you have a specific problem to solve.
Remember and share data
useStateRemember a value owned by one component.useReducerCentralize several related state updates.createContextCreate information a subtree can provide.use/useContextRead context;usealso reads Promises.useSyncExternalStoreSubscribe to a store managed outside Octane.
Connect to the browser
useRefKeep a DOM node or mutable value without rendering.useEffectSynchronize with an external system after paint.useLayoutEffectMeasure or adjust layout before paint.useInsertionEffectInsert styles before layout effects; mainly for libraries.useEffectEventCall the latest logic from an effect without reacting to it.useIdCreate an accessible ID stable across server and client.useImperativeHandleCustomize the value exposed through a ref.
Load and reveal UI
HydrateKeep server HTML dormant and load its behavior on demand.SuspenseShow a fallback while descendants wait.ErrorBoundaryReplace a failed subtree with an error fallback.lazyLoad a component's code on demand.startTransitionMark an update as non-urgent.useTransitionStart a transition and read its pending state.useDeferredValueLet a slow child temporarily use an older value.ActivityHide or prerender a subtree while suppressing its effects.
Handle actions and forms
useActionStateTrack an action's result and pending state.useFormStatusRead the nearest parent form's active action.useOptimisticShow an expected result before an action finishes.requestFormResetRequest an uncontrolled-form reset from an action or transition.
Compose and optimize
FragmentGroup siblings without an extra DOM element.memoSkip a component render when its props are unchanged.useMemoReuse an expensive calculated value.useCallbackReuse a function identity when a consumer needs it.createPortalRender into another DOM container.ViewTransition/addTransitionTypeCoordinate browser View Transitions.ViewTransitionPseudoElementWork with transition pseudo-elements in advanced integrations.
Roots, resources, and library tools
createRoot/hydrateRootStart a client tree or adopt server HTML.preload/preinitTell the browser about important resources early.preconnect/prefetchDNSWarm a connection to another origin.createElement/cloneElementCreate or adapt element descriptors; mainly for libraries.isValidElement/isChildrenBlock/ChildrenInspect component children in library code.flushSync/actIntegrate synchronous DOM work or test updates.useDebugValueCompatibility hook; it currently has no visible runtime effect.setSsrSuspenseTimeout/getSsrSuspenseTimeoutConfigure the server's global deadline for unresolved async data.versionRead the installed Octane package version.
Practice
Try these small changes in the counter example:
- Add a Reset button that sets the count back to
0. - Accept a
stepprop and add that amount on each click. - Show “Bag is empty” when the count is zero and “Ready to go” otherwise.
Show a hint
Event handlers can call setCount(0) directly. Read props.step inside the updater, and use an @if block for the two messages.
Next steps
- Quick start — install Octane and run your first app.
- TSRX vs TSX/JSX — choose an authoring format and learn the
.tsrxcontrol-flow syntax. - Bindings — use first-party Octane ports of popular libraries.