Skip to content

Primitives ​

Litsx has its own public surface. The important thing is the authored API of the framework, not the code shape produced by transforms.

Core Runtime Surface ​

These are the primitives that matter most when you write Litsx directly:

  • ErrorBoundary
  • SuspenseBoundary
  • SuspenseList

The runtime also exposes helpers such as ensureLazyElement(...), but those are supporting pieces. They are relevant for transforms and advanced integration, not for day-one authoring.

@litsx/core/jsx-runtime and @litsx/core/jsx-dev-runtime are part of the tooling story, not the authored runtime surface. They matter for jsxImportSource: "@litsx/core" and editor integration; see Tooling.

For everyday component work, the runtime surface also includes the hooks that model state, DOM access, and effect timing:

  • useState(...)
  • useMemoValue(...)
  • useRef(...)
  • useAsyncState(...)
  • useOptimistic(...)
  • useOnConnect(...)
  • useEvent(...)
  • useEmit(...)
  • useOnCommit(...)
  • useAfterUpdate(...)

Structural hooks are a separate authoring contract for library authors who need reusable host capabilities. They are consumed with normal hook syntax and declared with defineHook({ mixin, use }); see Structural Hooks.

This example shows that split directly in one component:

  • useState(...) keeps the counter alive across renders
  • useRef(...) captures the rendered button
  • useOnCommit(...) does immediate DOM work on the commit path
  • useAfterUpdate(...) runs a little later as passive post-update work

useMemoValue(...), usePrevious(...), and useControlledState(...) extend that authored surface without changing the same core model:

  • useMemoValue(...) keeps expensive derived values render-pure
  • usePrevious(...) lets render logic compare against the previous frame
  • useControlledState(...) supports controlled/uncontrolled library components

useId(...) and useStableId(...) solve different identity problems:

  • useId(...) creates instance-local DOM ids for labels, descriptions, and accessibility relationships
  • useStableId(...) creates a callsite-stable id that is the same between server output and browser hydration

Use useStableId(...) for serializable resource identity such as cache keys, preload records, i18n message slots, and hydration metadata. It is intentionally not unique per component instance; every instance that reaches the same authored callsite receives the same value. The Litsx transform injects stable callsite metadata, so the id does not depend on render order, function names, stack traces, or Math.random().

useAsyncState(...) and useOptimistic(...) cover two related but separate async jobs:

  • useAsyncState(...) owns authoritative async mutations, pending, latest error, and reset
  • useOptimistic(...) layers a temporary optimistic overlay on top of authoritative state

Use useAsyncState(...) when the component needs to drive a real async state transition such as save, refresh, or confirm. Use useOptimistic(...) when the UI should show a temporary expected outcome before authoritative state catches up.

That split matters:

  • useAsyncState(...) owns authoritative async mutation state
  • useOptimistic(...) owns a disposable optimistic overlay
  • SuspenseBoundary owns async reads that pause render

If the question is "what should the UI show while we wait for a mutation?", reach for useAsyncState(...) and optionally useOptimistic(...).

If the question is "what should this subtree show while render-time data is still unresolved?", reach for SuspenseBoundary.

Authoritative Async State ​

This example focuses on the authoritative side of an async mutation:

  • the saved value is the source of truth
  • pending reflects in-flight work
  • error is part of the same state surface
  • reset() restores the authoritative state

Use this pattern when the component is coordinating a real async transition such as save, retry, or confirm.

Optimistic Overlay ​

This example focuses on the temporary overlay:

  • the left column shows the authoritative state
  • the right column shows what the UI renders with optimistic values applied
  • the optimistic layer can be discarded explicitly
  • when authoritative state changes, the overlay re-anchors to it

Use this pattern when the UI should briefly show an expected outcome before the real state catches up.

Authoring Model ​

Litsx uses standard JSX and TSX:

  • event listeners use on:event
  • ordinary prop names are lowered to Lit bindings from the destination contract
  • component metadata uses top-level assignments
  • Lit directives remain first-class in template expressions
  • components compile to Lit-compatible output

That last point matters more than it may seem: when a problem is already a Lit template problem, the right answer is usually still a Lit directive.

So in native Litsx code:

  • use built-in directives such as keyed(...), when(...), repeat(...), cache(...), guard(...), and until(...) when they fit
  • use custom directives too, when your project already has them or needs them
  • do not treat directives as something "outside" Litsx; they are part of the intended authoring stack

Connection-Scoped Work ​

useOnConnect(...), useEvent(...), and useEmit(...) cover three different event jobs:

  • useOnConnect(...) registers and cleans up resources tied to the host being connected
  • useEvent(...) gives those resources a stable callback that still sees fresh state and props
  • useEmit(...) publishes public CustomEvents from the current host without reaching for this.dispatchEvent(...)

Use useOnConnect(...) by itself when re-arming the resource on dependency changes is acceptable.

Pair it with useEvent(...) when you want to register once and avoid stale closures in listeners owned by window, document, observers, or other imperative APIs.

Reach for useEmit(...) when the component needs to publish a DOM event as part of its public API, such as change, select, or open.

The playground above focuses on render-time and post-render hooks. useEvent(...) fits the same runtime surface, but it matters most when the component also owns connection-scoped listeners or subscriptions.

Controlled State ​

useControlledState(...) is mainly for reusable components and design-system APIs. It lets the same component work with:

  • value controlled from outside
  • defaultValue owned locally
  • onChange notifications in both modes

Suspense Primitives ​

SuspenseBoundary and SuspenseList are native Litsx primitives for asynchronous UI.

  • SuspenseBoundary owns fallback/content rendering and reveal phases
  • SuspenseList coordinates reveal order across sibling boundaries
  • both are designed around light DOM so styles can flow through from the containing component

Failure Boundaries ​

ErrorBoundary is the native Litsx primitive for recoverable synchronous render failures.

  • it catches one subtree failing without taking down the whole component
  • it keeps fallback UI latched once the error has been captured
  • to retry, give the boundary a new identity with Lit's keyed(...)

Next ​