@pyreon/core — API Reference
Generated from
core'ssrc/manifest.ts— the same source that powersllms.txtand MCPget_api. Do not edit this page by hand; edit the manifest. For the conceptual guide, see core.
Component model and lifecycle for Pyreon. Provides the VNode type system, h() hyperscript function, JSX automatic runtime (@pyreon/core/jsx-runtime), lifecycle hooks (onMount, onUnmount), two-tier context system (createContext for static values, createReactiveContext for signal-backed values), control-flow components (Show, Switch/Match, For, Suspense, ErrorBoundary), code-splitting via lazy(), dynamic rendering via Dynamic, and props utilities (splitProps, mergeProps, cx, createUniqueId). Components are plain functions (ComponentFn<P> = (props: P) => VNodeChild) that run ONCE — reactivity comes from reading signals inside reactive scopes, not from re-running the component.
Multiplatform
Tier: Shared — the same source runs on web, iOS and Android
the JSX authoring surface PMTC compiles — For/Show/Suspense/ErrorBoundary lower; Switch/Match/Dynamic/Portal/Index warn with concrete alternatives
See Multiplatform for the capability matrix and Multiplatform libraries for every package's tier.
Features
Native compiler tooling: corePlugin from @pyreon/core/native-plugin owns hook recognition and Swift/Kotlin lowering; the native CLI discovers it through pyreon.native.plugin, and direct compiler callers install it explicitly
h() — hyperscript function producing VNodes, JSX compiles to h() or _tpl()
Fragment — group children without a wrapper DOM element
onMount / onUnmount — lifecycle hooks, onMount supports cleanup return
createContext / createReactiveContext — two-tier context system
provide / useContext — push and read context values
Show / Switch+Match — reactive conditional rendering
For — keyed reactive list rendering with by prop
Suspense / ErrorBoundary — async and error boundaries
lazy() / Dynamic — code splitting and dynamic component rendering
splitProps / mergeProps / removeUndefinedProps — reactivity-preserving props utilities
cx() — class value combiner (strings, objects, arrays, nested)
createUniqueId — SSR-safe unique ID generation
Async — pending/error/empty/data over any structural AsyncLike source (query results, http resources, hand-rolled)
use() — compose element directives (plain (el) => cleanup | void functions) into a single ref callback
elementRef() — one value that is both a ref and the () => T | null accessor element-consuming hooks take
Complete example
A full, end-to-end usage of the package:
import { h, Fragment, onMount, onUnmount, provide, createContext, createReactiveContext, useContext, Show, Switch, Match, For, Suspense, ErrorBoundary, lazy, Dynamic, cx, splitProps, mergeProps, createUniqueId } from "@pyreon/core"
import { signal, computed } from "@pyreon/reactivity"
// Context — static (destructure-safe) vs reactive (must call to read)
const ThemeCtx = createContext<"light" | "dark">("light")
const ModeCtx = createReactiveContext<"light" | "dark">("light")
const App = (props: { children: any }) => {
const mode = signal<"light" | "dark">("dark")
provide(ThemeCtx, "dark") // static — safe to destructure
provide(ModeCtx, () => mode()) // reactive — consumer must call
return <>{props.children}</>
}
// Lifecycle
const Timer = () => {
const count = signal(0)
onMount(() => {
const id = setInterval(() => count.update(n => n + 1), 1000)
return () => clearInterval(id) // cleanup runs on unmount
})
return <div>{count()}</div>
}
// Control flow — reactive conditional rendering
const Page = (props: { items: { id: number; name: string }[]; loggedIn: () => boolean }) => (
<div>
<Show when={props.loggedIn()} fallback={<p>Please log in</p>}>
<For each={props.items} by={item => item.id}>
{item => <li>{item.name}</li>}
</For>
</Show>
</div>
)
// Props utilities — preserve reactivity
const Button = (props: { class?: string; size?: string; onClick: () => void; children: any }) => {
const [local, rest] = splitProps(props, ["class", "size"])
const merged = mergeProps({ size: "md" }, local)
const id = createUniqueId()
return <button id={id} {...rest} class={cx(["btn", `btn-${merged.size}`, local.class])} />
}
// Code splitting
const HeavyPage = lazy(() => import("./HeavyPage"))
const LazyApp = () => (
<Suspense fallback={<div>Loading...</div>}>
<HeavyPage />
</Suspense>
)
// Async data boundary — pending / error / empty / data from any AsyncLike source
import type { AsyncLike, Directive } from "@pyreon/core"
declare const todos: AsyncLike<{ id: number; title: string }[]>
const TodoList = () => (
<Async of={todos} empty="No todos yet." error={(e) => <p>{String(e)}</p>}>
{(rows) => <ul>{rows.map(r => <li>{r.title}</li>)}</ul>}
</Async>
)
// Directive composer — compose element behaviours into one ref
const clickOutside = (cb: () => void): Directive => (el) => {
const h2 = (e: Event) => { if (!el.contains(e.target as Node)) cb() }
document.addEventListener("mousedown", h2)
return () => document.removeEventListener("mousedown", h2)
}
const Popover = (props: { close: () => void }) => (
<div ref={use(clickOutside(props.close))} />
)
// elementRef — one value that is both the ref AND the () => T | null accessor
const Card = () => {
const cardEl = elementRef<HTMLDivElement>()
onMount(() => cardEl()?.focus())
return <div ref={cardEl} tabIndex={0} />
}Exports
| Symbol | Kind | Summary |
|---|---|---|
h | function | Create a VNode from a component function, HTML tag string, or symbol (Fragment, Portal). |
Fragment | constant | Symbol used as the type for fragment VNodes that group children without producing a wrapper DOM element. |
onMount | function | Register a callback that runs after the component mounts into the DOM. |
onUnmount | function | Register a callback that runs when the component is removed from the DOM. |
onUpdate | function | Register a callback that runs after the component updates (reactive dependencies change and DOM patches complete). |
onErrorCaptured | function | Register an error handler that captures errors thrown by descendant components. |
createContext | function | Create a static context. |
createReactiveContext | function | Create a reactive context. |
provide | function | Push a context value for all descendant components. |
useContext | function | Read the nearest provided value for a context. |
Show | component | Reactive conditional rendering. |
Async | component | Renders one of pending / error / empty / data from any async-shaped source, replacing the hand-written guard chain at a |
use | function | Composes element behaviours into a single ref. |
Switch | component | Multi-branch conditional rendering. |
Match | component | A branch inside a <Switch>. |
For | component | Keyed reactive list rendering. |
Suspense | component | Async boundary that shows fallback while any lazy() component or async component inside is loading — at ANY depth: a |
ErrorBoundary | component | Catches render errors thrown by descendant components. |
lazy | function | Wrap a dynamic import for code splitting. |
Dynamic | component | Renders a component by reference or string tag name. |
cx | function | Combine a class value into a single string. |
useControllableState | function | The controlled/uncontrolled state pattern, as one primitive. |
useColorMode | function | The framework-wide light/dark mode, as an accessor — the ONE source every package reads, so <PyreonUI>, charts, flow a |
splitProps | function | Split a props object into two parts: the picked keys and the rest. |
mergeProps | function | Merge multiple props objects with last-source-wins semantics. |
removeUndefinedProps | function | Copy a props object, dropping keys whose DATA value is exactly undefined while preserving every getter-shaped (reactiv |
createUniqueId | function | Generate a unique string ID ("pyreon-1", "pyreon-2", ...) that is consistent between server and client when called in th |
Portal | component | Render children into a DOM element outside the component tree (typically document.body). |
mapArray | function | Low-level reactive array mapping used internally by <For>. |
createRef | function | Create a mutable ref object ({ current: T | null }) for holding DOM element references. |
elementRef | function | A single value that is BOTH a ref and the () => T | null accessor element-consuming hooks take (useElementSize, `use |
nativeCompat | function | Mark a Pyreon framework component as "self-managing" so compat layers (@pyreon/{react,preact,vue,solid}-compat) skip t |
isNativeCompat | function | Compat-layer-side: read whether a function has been marked as a Pyreon native framework component via nativeCompat(). |
NATIVE_COMPAT_MARKER | constant | The well-known registry symbol (Symbol.for("pyreon:native-compat")) used to mark a component as a Pyreon native framew |
Defer | component | Client-side lazy-load a chunk on ONE of three triggers (exactly one is provided): when={accessor} loads once the acces |
registerErrorHandler / reportError | function | registerErrorHandler is the telemetry hook for Sentry/Datadog/custom error reporting — called whenever a component thr |
isClient / isServer | constant | Re-exported from @pyreon/reactivity for convenience — isServer = typeof document === 'undefined', isClient its inv |
defineComponent | function | An identity wrapper — returns fn unchanged. |
ExtractProps | type | Extracts the props type from a ComponentFn. |
HigherOrderComponent | type | Typed HOC pattern where HOP is the props the HOC adds and P is the wrapped component's own props. |
API
h function
h<P extends Props>(type: ComponentFn<P> | string | symbol, props: P | null, ...children: VNodeChild[]): VNodeCreate a VNode from a component function, HTML tag string, or symbol (Fragment, Portal). Low-level API — prefer JSX which compiles to h() calls (or _tpl() + _bind() for template-optimized paths). Children are stored in vnode.children; components must merge them via props.children = vnode.children.length === 1 ? vnode.children[0] : vnode.children.
Example
const vnode = h("div", { class: "container" },
h("h1", null, "Hello"),
h(Counter, { initial: 0 })
)Common mistakes
h("div", "text")— second arg is always props (or null). Text children go in the third+ positions:h("div", null, "text")h(MyComponent, { children: <span /> })— children go as rest args, not a prop:h(MyComponent, null, <span />)h("input", { className: "x" })— useclassnotclassName(Pyreon uses standard HTML attributes)h("input", { onChange: handler })— useonInputfor keypress-by-keypress updates (native DOM events)
See also: Fragment · Dynamic · lazy
Fragment constant
Fragment: symbolSymbol used as the type for fragment VNodes that group children without producing a wrapper DOM element. In JSX, <>...</> compiles to h(Fragment, null, ...). Useful when a component needs to return multiple sibling elements.
Example
// JSX:
<>
<h1>Title</h1>
<p>Content</p>
</>
// h() API:
h(Fragment, null, h("h1", null, "Title"), h("p", null, "Content"))See also: h
onMount function
onMount(fn: () => CleanupFn | void): voidRegister a callback that runs after the component mounts into the DOM. The callback can optionally return a cleanup function that runs on unmount — this is the idiomatic pattern for event listeners, timers, and subscriptions. Must be called during component setup (the synchronous function body), not inside effects or async callbacks.
Example
const Timer = () => {
const count = signal(0)
onMount(() => {
const id = setInterval(() => count.update(n => n + 1), 1000)
return () => clearInterval(id) // cleanup on unmount
})
return <div>{count()}</div>
}Common mistakes
Forgetting cleanup:
onMount(() => { const id = setInterval(...) })leaks the interval. Return cleanup:return () => clearInterval(id)Using
onMount+ separateonUnmountfor paired setup/teardown — prefer returning cleanup fromonMountinsteadCalling
onMountinside aneffect()or async callback — it only works during synchronous component setupAccessing DOM refs before mount — the callback runs AFTER mount, which is the right place for DOM measurements
See also: onUnmount · onUpdate
onUnmount function
onUnmount(fn: () => void): voidRegister a callback that runs when the component is removed from the DOM. For paired setup/teardown, prefer returning a cleanup function from onMount instead — it co-locates the cleanup with the setup. onUnmount is useful when cleanup needs to reference state computed separately from the mount callback.
Example
onUnmount(() => {
console.log("Component removed from DOM")
})See also: onMount
onUpdate function
onUpdate(fn: () => void): voidRegister a callback that runs after the component updates (reactive dependencies change and DOM patches complete). Rarely needed — most update logic belongs in effect() or computed(). Useful for imperative DOM measurements that need to run after all reactive updates have flushed.
Example
onUpdate(() => {
console.log("Component updated, DOM is current")
})See also: onMount · onUnmount
onErrorCaptured function
onErrorCaptured(fn: (error: unknown) => boolean | void): voidRegister an error handler that captures errors thrown by descendant components. Return false to prevent the error from propagating further up the tree. Works alongside ErrorBoundary for programmatic error handling.
Example
onErrorCaptured((error) => {
console.error("Caught:", error)
return false // stop propagation
})See also: ErrorBoundary
createContext function
createContext<T>(defaultValue: T): Context<T>Create a static context. useContext() returns the value directly (T), so it is safe to destructure. Use this for values that do not change after being provided (theme name, locale string, config object). For values that change reactively (mode signal, locale signal), use createReactiveContext instead — otherwise consumers capture a stale snapshot at setup time.
Example
const ThemeCtx = createContext<"light" | "dark">("light")
// Provide:
const App = () => {
provide(ThemeCtx, "dark")
return <Child />
}
// Consume:
const Child = () => {
const theme = useContext(ThemeCtx) // "dark" — safe to destructure
return <div class={theme}>...</div>
}Common mistakes
provide(ThemeCtx, () => modeSignal())with a static context — the consumer receives the function itself, not the signal value. UsecreateReactiveContextfor dynamic valuesDestructuring a reactive context value:
const { mode } = useContext(reactiveCtx)captures once. Keep the object reference and access lazilyCalling
useContextoutside a component body — it reads from the component context stack, which only exists during setup
See also: createReactiveContext · provide · useContext
createReactiveContext function
createReactiveContext<T>(defaultValue: T): ReactiveContext<T>Create a reactive context. useContext() returns () => T — an accessor that must be called to read the current value. Use this for values that change over time (mode, locale, user). The accessor subscribes to updates when read inside reactive scopes (effect(), JSX thunks, computed()).
Example
const ModeCtx = createReactiveContext<"light" | "dark">("light")
// Provide:
const App = () => {
const mode = signal<"light" | "dark">("dark")
provide(ModeCtx, () => mode())
return <Child />
}
// Consume:
const Child = () => {
const getMode = useContext(ModeCtx) // () => "dark"
return <div class={getMode()}>...</div>
}See also: createContext · provide · useContext
provide function
provide<T>(ctx: Context<T> | ReactiveContext<T>, value: T): voidPush a context value for all descendant components. Auto-cleans up on unmount. Must be called during component setup (synchronous function body). Preferred over manual pushContext/popContext. For reactive values, provide a getter function to a ReactiveContext: provide(ModeCtx, () => modeSignal()).
Example
const ThemeCtx = createContext<"light" | "dark">("light")
function App() {
provide(ThemeCtx, "dark")
return <Child />
}Common mistakes
provide(ctx, "static")for a value that changes — usecreateReactiveContext+provide(ctx, () => signal())Calling
provideinsideonMountoreffect— it must run during synchronous component setupProviding the same context twice in one component — the second
provideshadows the first for that subtree
See also: createContext · createReactiveContext · useContext
useContext function
useContext<T>(ctx: Context<T>): TRead the nearest provided value for a context. For static Context<T>, returns T directly. For ReactiveContext<T>, returns () => T — must call the accessor to read. Falls back to the default value if no ancestor provides the context.
Example
const theme = useContext(ThemeContext) // static: returns T
const getMode = useContext(ModeCtx) // reactive: returns () => TSee also: provide · createContext · createReactiveContext
Show component
<Show when={condition} fallback={alternative}>{children}</Show>Reactive conditional rendering. Mounts children when when is truthy, unmounts and shows fallback when falsy. More efficient than ternary for signal-driven conditions because it avoids re-evaluating the entire branch expression on every signal change — Show only transitions between mounted/unmounted when the boolean flips. when accepts BOTH a value (when={true}, when={signal()}) and an accessor (when={() => signal()}) — the framework normalizes via typeof === "function". The accessor form is required for true reactivity (the framework re-evaluates it on signal change); a bare when={signal} reference works because the compiler's signal auto-call rewrites it to when={signal()}.
Example
<Show when={isLoggedIn()} fallback={<LoginForm />}>
<Dashboard />
</Show>Common mistakes
{cond() ? <A /> : <B />}— works but less efficient than<Show>for signal-driven conditions<Show when={items().length}>— works (truthy check), but be explicit:<Show when={items().length > 0}><Show when={signal}>(bare reference) — relies on the compiler's signal auto-call to rewrite towhen={signal()}. Works defensively but usewhen={() => signal()}for explicit accessor semantics across the entire reactive lifecycle.
See also: Switch · Match · For
Async component
<Async of={source} pending={…} error={(e) => …} empty={…}>{(data) => …}</Async>Renders one of pending / error / empty / data from any async-shaped source, replacing the hand-written guard chain at a data boundary. The of prop is STRUCTURAL, not a dependency: anything exposing isPending / isError / error / data accessors satisfies AsyncLike<T>, so @pyreon/query results, @pyreon/http resources and hand-rolled sources all work without @pyreon/core importing any of them. Returns a reactive accessor (the same shape Show uses), so every branch re-evaluates on source change. empty covers BOTH null/undefined data and an empty array — but only when you pass it: an empty array with no empty prop is handed to children instead, so a list that renders its own empty state keeps working rather than silently vanishing.
Example
import type { AsyncLike } from '@pyreon/core'
// any source with these four accessors — e.g. a useQuery() result
declare const todos: AsyncLike<{ id: number; title: string }[]>
<Async of={todos} empty="No todos yet." error={(e) => <p>{String(e)}</p>}>
{(rows) => <ul>{rows.map((r) => <li>{r.title}</li>)}</ul>}
</Async>Common mistakes
Expecting
<ErrorBoundary>to catch the source error — it CANNOT. Only a throw during the initial mount is caught; a reactive re-run throws outside its reach, which is exactly the common case (a request that fails after mount). That is why<Async>has no rethrowing default: passerror={(e) => …}or nothing renders.Omitting
errorand assuming the failure is visible — it renders nothing and warns ONCE in development. In production it is silent.Passing
emptyand expecting it for a non-empty falsy value —0and""are DATA and reachchildren. Only null/undefined and an empty array are empty.Reaching for
<Async>to catch a THROWN error — it reads an error off the source, it does not trap exceptions.<ErrorBoundary>is still the tool for a component that throws at mount.Assuming
pendingdefaults to a spinner or text — it defaults to rendering nothing, deliberately, so the framework never injects English into your UI.
See also: Show · Switch · ErrorBoundary · Suspense
use function
use<T extends Element>(...directives: DirectiveEntry<T>[]): RefCallback<T>Composes element behaviours into a single ref. A Directive is a plain (el) => cleanup | void function, so attaching N behaviours costs one attribute instead of a ref declaration, N hook calls and a ref attach. Nothing here is special-cased by the compiler or the renderer — use() returns an ordinary RefCallback, which the runtime already invokes with the element on mount and null on unmount. Cleanups run in REVERSE attach order (LIFO), so a directive whose setup depends on an earlier one tears down first. Falsy entries are skipped, so a directive can be applied conditionally inline. Behaviour bundles are plain arrays: use(...dialogBehaviours, autoFocus).
Example
const clickOutside = (cb: () => void): Directive => (el) => {
const h = (e: Event) => { if (!el.contains(e.target as Node)) cb() }
document.addEventListener('mousedown', h)
return () => document.removeEventListener('mousedown', h)
}
<div ref={use(autoFocus, clickOutside(close), hotkey({ Escape: close }))} />Common mistakes
Returning a cleanup from a directive and expecting it on EVERY render — it runs on unmount (or before a re-attach), not per reactive update. A directive attaches once per element.
Passing
use(...)to something other thanref— it is a ref callback, not a prop object; it does not spread.Assuming a second attach stacks behaviours — a re-attach without an intervening detach tears the previous registration down FIRST, so listeners cannot pile up (leak class D).
Writing a directive that captures a value at attach time and expecting it to track — a directive receives the element, not a reactive scope. Read a signal inside the event handler, or pass an accessor.
See also: createRef · Ref
Switch component
<Switch fallback={default}>{Match children}</Switch>Multi-branch conditional rendering. Renders the first <Match> child whose when prop is truthy. If no match, renders the fallback. More readable than nested <Show> for multi-way conditions.
Example
<Switch fallback={<p>Unknown status</p>}>
<Match when={status() === "loading"}>
<Spinner />
</Match>
<Match when={status() === "error"}>
<ErrorDisplay />
</Match>
<Match when={status() === "success"}>
<Results />
</Match>
</Switch>See also: Match · Show
Match component
<Match when={condition}>{children}</Match>A branch inside a <Switch>. Renders its children when when is truthy and it is the first truthy <Match> in the parent <Switch>. Must be a direct child of <Switch>. when accepts both a value and an accessor (same normalization as <Show>).
Example
<Switch>
<Match when={tab() === "home"}><Home /></Match>
<Match when={tab() === "settings"}><Settings /></Match>
</Switch>See also: Switch · Show
For component
<For each={items} by={keyFn}>{renderFn}</For>Keyed reactive list rendering. Uses the by prop (not key) for the key function because JSX extracts key as a special VNode reconciliation prop. The render function receives each item and its index. Internally uses an LIS-based reconciler for minimal DOM mutations when the list changes.
Example
const items = signal([
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
])
<For each={items()} by={item => item.id}>
{(item, index) => <li>{item.name}</li>}
</For>Common mistakes
<For each={items}>— must call the signal:<For each={items()}><For each={items()} key={...}>— usebynotkey(JSX reserveskeyfor VNode reconciliation){items().map(...)}— use<For>for reactive list rendering;.map()re-creates all DOM nodes on every change<For each={items()} by={index}>— using array index as key defeats the reconciler; use a stable identity likeitem.id
See also: Show · mapArray
Suspense component
<Suspense fallback={loadingUI}>{children}</Suspense>Async boundary that shows fallback while any lazy() component or async component inside is loading — at ANY depth: a still-loading descendant registers with the NEAREST boundary through context. While it waits, the content stays mounted off-screen (not torn down), so the descendant's ancestors keep their DOM and state and set up once; a descendant that starts loading after the boundary resolved brings the fallback back the same way. SSR mode streams the fallback immediately and swaps in the resolved content when ready (30s timeout); hydration never shows the fallback over server content. Nested Suspense boundaries are independent — an inner boundary resolving does not affect the outer.
Example
const LazyPage = lazy(() => import("./HeavyPage"))
<Suspense fallback={<div>Loading...</div>}>
<LazyPage />
</Suspense>See also: lazy · ErrorBoundary
ErrorBoundary component
<ErrorBoundary fallback={(err, reset) => VNodeChild}>{children}</ErrorBoundary>Catches render errors thrown by descendant components. The fallback receives the caught error (typed unknown) and a reset() function — calling reset() clears the error and re-renders children. Without an ErrorBoundary, uncaught errors propagate to the nearest registerErrorHandler or crash the app. There is no onCatch prop — for logging/telemetry, log inside fallback or use registerErrorHandler.
Example
<ErrorBoundary
fallback={(err, reset) => (
<div>
<p>Error: {String(err)}</p>
<button onClick={reset}>Retry</button>
</div>
)}
>
<App />
</ErrorBoundary>Common mistakes
Passing an
onCatchprop — it does not exist.fallbackis the only prop (besides children); log the error inside it or viaregisterErrorHandlerReading
err.messagedirectly —erris typedunknown; narrow it (err instanceof Error ? err.message : String(err)) orString(err)
See also: Suspense · registerErrorHandler
lazy function
lazy(loader: () => Promise<{ default: ComponentFn }>, options?: LazyOptions): LazyComponentWrap a dynamic import for code splitting. Returns a component that integrates with Suspense — the parent Suspense boundary shows its fallback until the import resolves. The loaded component is cached after first resolution. On the SERVER a still-loading lazy is WAITED for like an async component: renderToStream resolves it inside the Suspense boundary after flushing the fallback, and renderToString renders the loaded content. If the client chunk has not landed when hydration reaches it, the server nodes are KEPT in place and hydrated once it lands (node identity, focus and typed input survive); preloading only makes the page interactive sooner. A lazy mounted while loading renders its component once the chunk lands, with or without a Suspense.
Example
const Settings = lazy(() => import("./pages/Settings"))
// Use in JSX (wrap with Suspense):
<Suspense fallback={<Spinner />}>
<Settings />
</Suspense>See also: Suspense · Dynamic
Dynamic component
<Dynamic component={comp} {...props} />Renders a component by reference or string tag name. Useful when the component to render is determined at runtime (tab panels, plugin systems, polymorphic containers). When component changes, the previous component unmounts and the new one mounts.
Example
const components = { home: HomePage, about: AboutPage }
const current = signal("home")
<Dynamic component={components[current()]} />See also: lazy · h
cx function
cx(value: ClassValue): stringCombine a class value into a single string. Takes ONE ClassValue — a string, boolean (falsy ignored), object ({ active: true }), or (possibly nested) array. To combine multiple values, pass them as an ARRAY (cx(["btn", active && "on"])), not as separate arguments — cx is single-arg. The class prop on JSX elements already accepts ClassValue directly, so explicit cx() is only needed when building class strings outside JSX.
Example
cx(["foo", "bar"]) // "foo bar"
cx(["base", isActive && "active"]) // conditional
cx({ base: true, active: isActive() }) // object syntax
cx(["a", ["b", { c: true }]]) // nested arrays
// class prop accepts ClassValue directly:
<div class={["base", cond && "active"]} />Common mistakes
Calling
cx("a", "b")with multiple arguments —cxtakes ONEClassValue. Wrap in an array:cx(["a", "b"])
See also: splitProps · mergeProps
useControllableState function
useControllableState<T>(options: { value: () => T | undefined; defaultValue: T; onChange?: (value: T) => void }): [() => T, (next: T | ((prev: T) => T)) => void]The controlled/uncontrolled state pattern, as one primitive. Returns a [getter, setter] pair that reads an external value prop when one is supplied and falls back to internal signal state when it is not, calling onChange on every write either way. value MUST be a getter (() => own.checked) so the controlled prop is read lazily inside reactive scopes — an eager read captures it once and the component stops tracking the owner. Every component with a checked/value/open-style prop needs this; it lives beside splitProps because it is a props primitive, not a hook (it owns no lifecycle). Re-exported from @pyreon/hooks for back-compat.
Example
const Switch = (props: { checked?: boolean; onChange?: (v: boolean) => void }) => {
const [own, rest] = splitProps(props, ["checked", "onChange"])
const [checked, setChecked] = useControllableState({
value: () => own.checked,
defaultValue: false,
onChange: own.onChange,
})
return <button {...rest} aria-checked={() => (checked() ? "true" : "false")} onClick={() => setChecked(!checked())} />
}Common mistakes
Passing a VALUE instead of a getter —
value: own.checkedreads once at setup and freezes; it must bevalue: () => own.checkedHand-rolling
const isControlled = props.value !== undefined+ a parallel signal — that is exactly this primitive, and the hand-rolled version usually forgets to callonChangein the uncontrolled branchWriting to the setter and expecting a CONTROLLED component to update itself — it will not; the owner decides. The setter only reports via
onChangeAssuming
defaultValuestill applies oncevalueis supplied — controlled wins for the whole lifetime of the component
See also: splitProps · mergeProps
useColorMode function
useColorMode(): () => 'light' | 'dark' // + provideColorMode(mode), <ColorModeProvider mode>, systemColorMode()The framework-wide light/dark mode, as an accessor — the ONE source every package reads, so <PyreonUI>, charts, flow and the code editor agree. Resolution, nearest first: a <ColorModeProvider mode> / provideColorMode(mode) above the component (<PyreonUI mode> calls it), else the page's scheme (the CSS color-scheme <html> declares, when it names exactly one — a site's own theme toggle), else prefers-color-scheme; light on the server. mode is 'light', 'dark' or 'system', or an accessor over one. systemColorMode() is the page/OS half alone, a document-lifetime singleton. On native a literal mode is a compile-time scope; a reactive one follows the platform scheme.
Example
import { ColorModeProvider, useColorMode } from '@pyreon/core'
const Badge = () => {
const mode = useColorMode()
return <span class={() => (mode() === 'dark' ? 'badge-dark' : 'badge-light')}>new</span>
}
<ColorModeProvider mode="dark">
<Badge />
</ColorModeProvider>Common mistakes
Calling
useColorMode()()once at setup — it returns an ACCESSOR; read it inside JSX, an effect or a computed so a flip re-rendersAdding a separate mode prop to each library component —
<PyreonUI mode>or<ColorModeProvider mode>sets it for everything below; a component that takes its own mode prop should treat it as an override ofuseColorMode(), not the sourceReading
matchMedia('(prefers-color-scheme: dark)')directly — it ignores both an app's pinned mode and the page's declaredcolor-schemeCalling it outside component setup — it reads context, so outside a component it returns the system mode, not a provider's
See also: createReactiveContext · provide
splitProps function
splitProps<T, K extends keyof T>(props: T, keys: K[]): [Pick<T, K>, Omit<T, K>]Split a props object into two parts: the picked keys and the rest. Both halves preserve signal reactivity — reads through either half still track the original reactive prop getters. This is the Pyreon replacement for const { x, ...rest } = props destructuring, which captures values once and loses reactivity.
Example
const Button = (props: { class?: string; onClick: () => void; children: VNodeChild }) => {
const [local, rest] = splitProps(props, ["class"])
return <button {...rest} class={cx(["btn", local.class])} />
}Common mistakes
const { class: cls, ...rest } = props— destructuring captures once, loses reactivity. UsesplitProps(props, ["class"])Passing a non-props object —
splitPropsrelies on reactive getter descriptors that the compiler creates on props objectsForgetting that symbol-keyed props are preserved —
splitPropsusesReflect.ownKeysso symbols (likeREACTIVE_PROP) survive
See also: mergeProps · cx
mergeProps function
mergeProps<T extends object[]>(...sources: T): MergedProps<T>Merge multiple props objects with last-source-wins semantics. Reads are lazy — the merged object delegates to the source objects via getters, so signal reactivity is preserved. Commonly used to inject default props: mergeProps({ size: "md" }, props). Forces configurable: true on copied descriptors to prevent "Cannot redefine property" errors.
Example
const Button = (props: { size?: string; variant?: string }) => {
const merged = mergeProps({ size: "md", variant: "primary" }, props)
return <button class={`btn-${merged.size} btn-${merged.variant}`} />
}Common mistakes
Object.assign({}, defaults, props)— loses reactivity. UsemergeProps(defaults, props)insteadmergeProps(props, defaults)— wrong order. Defaults go FIRST, actual props last (last source wins)
See also: splitProps · cx
removeUndefinedProps function
removeUndefinedProps<T>(props: T): { [K in keyof T as T[K] extends undefined ? never : K]: T[K] }Copy a props object, dropping keys whose DATA value is exactly undefined while preserving every getter-shaped (reactive) prop verbatim. The descriptor-aware filter a prop-forwarding HOC runs before mergeProps: an undefined consumer prop must not shadow a default, but a compiler-emitted reactive prop must survive with its subscription intact. Copies property descriptors (never values) — a value-copy would fire each getter at setup time and collapse the live signal to a static snapshot. null / 0 / "" / false are kept; only undefined data props are dropped, and getter descriptors are always kept (cannot peek without firing).
Example
const filtered = removeUndefinedProps(props) // undefined keys gone, getters live
const merged = mergeProps(defaults, filtered)Common mistakes
result[key] = props[key]to filter — fires getter-shaped reactive props, collapsing the subscription. Use this helper (it copies descriptors)Expecting
null/0/falseto be dropped — onlyundefineddata values are removedCalling on
undefined—Object.getOwnPropertyDescriptors(undefined)throws; guard the input
See also: mergeProps · splitProps · makeReactiveProps
createUniqueId function
createUniqueId(): stringGenerate a unique string ID ("pyreon-1", "pyreon-2", ...) that is consistent between server and client when called in the same order. SSR-safe — the counter resets per request context. Use for id/for/aria-* attribute pairing in components.
Example
const LabeledInput = (props: { label: string }) => {
const id = createUniqueId()
return (
<>
<label for={id}>{props.label}</label>
<input id={id} />
</>
)
}See also: splitProps
Portal component
<Portal target={element}>{children}</Portal>Render children into a DOM element outside the component tree (typically document.body). Useful for modals, tooltips, and overlays that need to escape parent overflow/z-index stacking contexts. Context values from the Portal source tree are preserved.
Example
<Portal target={document.body}>
<div class="modal-overlay">
<div class="modal">Content</div>
</div>
</Portal>See also: Dynamic
mapArray function
mapArray<T, U>(source: () => T[], getKey: (item: T) => string | number, map: (item: T) => U): () => U[]Low-level reactive array mapping used internally by <For>. Maps a reactive array through a transform, caching results per KEY so unchanged items reuse their mapped value. Takes THREE args — the source accessor, a getKey identity function, and the map transform. Prefer <For> in JSX — use mapArray only when you need a reactive derived array outside of rendering.
Example
const items = signal([{ id: 1, n: 2 }, { id: 2, n: 3 }])
const doubled = mapArray(() => items(), (item) => item.id, (item) => item.n * 2)
// doubled() → [4, 6] — updates reactively, keyed by idCommon mistakes
Omitting the
getKeyargument —mapArrayrequires 3 args (source, getKey, map); without a key function it cannot cache per-item across updates
See also: For
createRef function
createRef<T>(): Ref<T>Create a mutable ref object ({ current: T | null }) for holding DOM element references. Pass as the ref prop on JSX elements — the runtime sets .current after mount and clears it on unmount. Callback refs ((el: T | null) => void) are also supported via RefProp<T>.
Example
const inputRef = createRef<HTMLInputElement>()
onMount(() => inputRef.current?.focus())
return <input ref={inputRef} />See also: onMount
elementRef function
elementRef<T = HTMLElement>(): ElementRef<T>A single value that is BOTH a ref and the () => T | null accessor element-consuming hooks take (useElementSize, useClickOutside, useDraggable, and ten others across @pyreon/hooks/@pyreon/dnd). The runtime already invokes a function ref as ref(el) on mount and ref(null) on unmount, so elementRef reads that call shape directly: called WITH an argument means SET (a ref attach/detach), called with NO argument means READ (the accessor hooks want). Without it, wiring N hooks to one element costs three touchpoints (a local variable, a hand-written () => el thunk per hook, a callback ref wiring the local back) — elementRef collapses that to two, and N hooks on the same element add none of them. .current is kept so it drops into code written against createRef.
Example
import { elementRef } from '@pyreon/core'
import { useElementSize } from '@pyreon/hooks'
function Card() {
const el = elementRef<HTMLDivElement>()
const size = useElementSize(el) // el IS the () => T | null accessor
return <div ref={el}>{size().width}px</div>
}Common mistakes
Using
elementRefwhere TWO different elements are involved — it is one value FOR one element; a second element needs its ownelementRef(), not a second read of the same one.Treating
nullas a "no value yet" read —nullis a legal SET (exactly what unmount passes). The discriminator isundefined(zero-arg call), not truthiness —el()reads,el(null)still writes.Passing
elementRef()to a hook that expects a PLAINT | nullvalue instead of an accessor — it is callable, not a value; hooks that take() => T | null(the convention every element-consuming hook in@pyreon/hooks/@pyreon/dndfollows) accept it directly, a hook expecting a raw element needsel().Re-declaring a local
let+ manual thunk alongsideelementRef"to be safe" — that reintroduces the exact three-touchpoint duplicationelementRefexists to remove.
See also: createRef · use
nativeCompat function
<T>(fn: T) => TMark a Pyreon framework component as "self-managing" so compat layers (@pyreon/{react,preact,vue,solid}-compat) skip their wrapping and route the component through Pyreon's mount path. Use on every @pyreon/* JSX component whose setup body uses provide() / lifecycle hooks / signal subscriptions — wrapping breaks those by running the body inside the compat layer's render context instead of Pyreon's. Idempotent; non-function inputs pass through unchanged. The marker is a registry symbol (Symbol.for("pyreon:native-compat")), so framework and compat sides share it without an import dependency between them.
Example
// In a framework package:
export const RouterView = nativeCompat(function RouterView(props) {
provide(RouterContext, ...)
return <div>{children}</div>
})Common mistakes
Forgetting to mark a new framework JSX export — under compat mode, the component's
provide()/onMount()calls fail with "called outside component setup" warnings and the rendered DOM silently breaks.Marking user-app components — only
@pyreon/*framework components that already manage their own reactivity should be marked. User components in compat mode are SUPPOSED to be wrapped (that's how they get re-render-on-state-change semantics).
See also: isNativeCompat · NATIVE_COMPAT_MARKER
isNativeCompat function
(fn: unknown) => booleanCompat-layer-side: read whether a function has been marked as a Pyreon native framework component via nativeCompat(). Compat jsx() calls this to decide whether to skip the React/Vue/Solid/Preact-style wrapping. Always returns false for non-function inputs.
Example
// In a compat layer's jsx-runtime:
if (isNativeCompat(type)) return h(type, props)
return wrapCompatComponent(type)(props)See also: nativeCompat · NATIVE_COMPAT_MARKER
NATIVE_COMPAT_MARKER constant
symbolThe well-known registry symbol (Symbol.for("pyreon:native-compat")) used to mark a component as a Pyreon native framework component. Most callers should use nativeCompat() / isNativeCompat() instead of touching the symbol directly; exported for advanced cases (e.g., a compat layer that wants to inspect the property without going through the helper).
Example
import { NATIVE_COMPAT_MARKER } from '@pyreon/core'
// Equivalent to nativeCompat(MyComponent):
;(MyComponent as Record<symbol, boolean>)[NATIVE_COMPAT_MARKER] = trueSee also: nativeCompat · isNativeCompat
Defer component
<Defer chunk={() => import('./X')} when={() => boolean} | on='visible'|'idle' fallback? rootMargin?>{(Component) => VNodeChild}</Defer>Client-side lazy-load a chunk on ONE of three triggers (exactly one is provided): when={accessor} loads once the accessor becomes truthy (the modal-open pattern); on="visible" loads when the wrapper scrolls into the viewport (IntersectionObserver, rootMargin default '200px' so it's typically ready before the user scrolls to it); on="idle" loads during browser idle time (requestIdleCallback, falling back to setTimeout(1)). The chunk fetch fires EXACTLY ONCE per Defer instance — a when accessor oscillating true/false/true does not re-fetch. Two authoring forms: the EXPLICIT render-prop form shown above (works everywhere), and an INLINE JSX form (<Defer when={x}><Modal/></Defer>) that @pyreon/compiler's transformDeferInline rewrites into the explicit form at BUILD time — the inline form requires @pyreon/vite-plugin; without it, Defer throws a clear dev-mode error naming the fix. fallback renders while the chunk loads (default null); a chunk-load failure is caught, dev-warned, and re-thrown into the nearest ErrorBoundary on render.
Example
// Signal-driven (modal) — explicit form:
<Defer chunk={() => import('./ConfirmDeleteModal')} when={open} fallback={<Spinner/>}>
{Modal => <Modal onClose={() => setOpen(false)} />}
</Defer>
// Viewport-driven (below-fold), with @pyreon/vite-plugin — inline form:
<Defer on="visible">
<Comments postId={id} />
</Defer>Common mistakes
Using the inline JSX form WITHOUT
@pyreon/vite-pluginenabled — the compiler pass that rewrites<Defer on="visible"><Comments/></Defer>into the chunk-prop form never runs, soDeferthrows a clear "<Defer>has nochunkprop" error the moment the trigger fires; use the explicit render-prop form instead in a non-Vite buildExpecting the chunk to re-fetch when a
whenaccessor flips false then true again — it loads exactly once per mountedDefer; unmount and remount the<Defer>itself if you need a fresh fetchForgetting the module needs a
defaultexport —Deferaccepts either{ default: Component }or a bareComponentFn, matchinglazy()'s contract; a module with neither dev-warns and renders nothing
See also: lazy · Suspense · ErrorBoundary
registerErrorHandler / reportError function
registerErrorHandler(handler: (ctx: ErrorContext) => void) => () => void · reportError(ctx: ErrorContext) => voidregisterErrorHandler is the telemetry hook for Sentry/Datadog/custom error reporting — called whenever a component throws in ANY lifecycle phase (setup/render/mount/unmount) OR an effect() in @pyreon/reactivity throws (bridged via a globalThis.__pyreon_report_error__ sink, since reactivity can't depend on core). Returns an unregister function; multiple handlers can be registered simultaneously (all fire). The ErrorContext passed to your handler carries component/phase/error/timestamp/props, plus (dev-only, tree-shaken in production) reactiveTrace — the last ~50 signal writes leading up to the error, answering "what reactive state changed in the run-up?" for free. reportError is the lower-level function the RUNTIME calls to dispatch a caught error through the registered handlers — call it directly only from custom framework-adjacent code (a hand-rolled error boundary, a compat-layer bridge) that catches an error the normal mount pipeline won't see.
Example
import { registerErrorHandler } from '@pyreon/core'
import * as Sentry from '@sentry/browser'
registerErrorHandler((ctx) => {
Sentry.captureException(ctx.error, {
extra: { component: ctx.component, phase: ctx.phase, reactiveTrace: ctx.reactiveTrace },
})
})Common mistakes
Registering more than one handler and expecting only the LAST one to run — every registered handler fires for every error; deduplicate reporting inside your own handler if you register from multiple places
Expecting
ctx.reactiveTraceto be populated in production — it is dev-only by design (the recorder tree-shakes out of prod bundles for zero cost); do not branch production telemetry logic on its presenceCalling
reportErrorfrom ordinary application code instead of just throwing — a thrown error inside a component/effect ALREADY reaches every registered handler via the normal mount/effect error paths;reportErroris for framework-adjacent code catching an error OUTSIDE those paths
See also: ErrorBoundary · onErrorCaptured
isClient / isServer constant
isClient: boolean · isServer: booleanRe-exported from @pyreon/reactivity for convenience — isServer = typeof document === 'undefined', isClient its inverse. Plain runtime constants evaluated ONCE at module load (not an export-condition fold), so they work correctly in any bundler regardless of whether it sets a browser condition. Use for small environment guards (module-level singletons, lazy globals, render output that differs server vs client); for DOM access INSIDE a component prefer onMount/effect (which never run during SSR at all) over branching on isClient yourself, and for large server-only code prefer a /server subpath export over a runtime branch.
Example
import { isClient } from '@pyreon/core'
if (isClient) {
// module-level singleton setup safe here — never runs during SSR
}Common mistakes
Hand-rolling
const isBrowser = typeof window !== "undefined"instead of importingisClient/isServer—typeof document(what this uses) is the more reliable discriminator thantypeof window, and a hand-rolled local const is NOT recognized by theno-window-in-ssrlint rule's SSR-guard detection the wayisClient/isServerimported from@pyreon/core/@pyreon/reactivityareUsing
isClientto gate DOM access inside a component body whenonMount/effectwould do — those never run during SSR at all, which is simpler and doesn't need the guard
See also: onMount
defineComponent function
<P>(fn: ComponentFn<P>) => ComponentFn<P>An identity wrapper — returns fn unchanged. Purely a TYPE-LEVEL / tooling annotation: marks a function as a Pyreon component for IDE tooling and potential future compiler optimizations. Has ZERO runtime effect today (no wrapping, no registration, no different behavior from an unwrapped component function).
Example
const Button = defineComponent((props: { label: string }) => <button>{props.label}</button>)
// Identical behavior to: const Button = (props: { label: string }) => <button>{props.label}</button>Common mistakes
Expecting
defineComponentto do anything at runtime (memoization, registration, special reactivity handling) — it is a no-op identity function; components work identically with or without it
ExtractProps type
type ExtractProps<T> = /* matches up to 4 overloads, unions the props */ T extends ComponentFn<infer P> ? P : TExtracts the props type from a ComponentFn. Passes through unchanged if T is not a ComponentFn. Multi-overload aware — matches up to 4 call signatures and produces the UNION of their first-argument types. Critical for multi-overload primitives (Iterator, List, Element) whose loosest overload is last; without overload-aware extraction, HOC wrapping (rocketstyle(), attrs()) silently downgraded their public prop surface. Single-overload functions still work — the union of 4 copies of the same props type dedupes back to the single shape.
Example
function Iterator<T extends SimpleValue>(p: { data: T[]; valueName?: string }): VNodeChild
function Iterator<T extends ObjectValue>(p: { data: T[]; component: ComponentFn<T> }): VNodeChild
type Props = ExtractProps<typeof Iterator>
// → { data: SimpleValue[]; valueName?: string }
// | { data: ObjectValue[]; component: ComponentFn<ObjectValue> }Common mistakes
Assuming
ExtractProps<T>returns only the LAST overload — pre-fix it did, post-fix it returns the UNION of up to 4 overloads. Functions with more than 4 overloads still drop the extras.Using
T extends (props: infer P) => any ? P : neverdirectly in user code — that pattern captures only the LAST overload of a multi-overload function. UseExtractProps<T>to get the full union.
See also: HigherOrderComponent
HigherOrderComponent type
type HigherOrderComponent<HOP, P> = ComponentFn<HOP & P>Typed HOC pattern where HOP is the props the HOC adds and P is the wrapped component's own props. The resulting component accepts both sets of props.
Example
function withLogger<P>(Wrapped: ComponentFn<P>): HigherOrderComponent<{ logLevel?: string }, P> {
return (props) => {
console.log(`[${props.logLevel ?? "info"}] Rendering`)
return <Wrapped {...props} />
}
}See also: ExtractProps
Package-level notes
Components run once: Pyreon components are plain functions that execute a single time. Reactivity comes from reading signals inside reactive scopes (JSX expression thunks,
effect(),computed()), not from re-running the component function.if (!cond()) return nullat the top level runs once and is static — usereturn (() => { if (!cond()) return null; return <div /> })for reactive conditional rendering.
Destructuring props kills reactivity:
const { name } = propscaptures the value at setup time — it becomes static. Useprops.nameinside reactive scopes, orsplitProps(props, ["name"])for rest patterns. The compiler handlesconst x = props.y; return <div>{x}</div>by inliningprops.yback at the use site, but only forconst(notlet/var).
Two context types:
createContext<T>returnsTfromuseContext()— safe to destructure.createReactiveContext<T>returns() => T— must call to read. Using the wrong one is a common source of stale-value bugs (static context for dynamic values) or unnecessary ceremony (reactive context for constants).
For uses by, not key: The
<For>component uses thebyprop for its key function because JSX extractskeyas a special VNode reconciliation prop. Writing<For each={items()} key={fn}>silently passes the key to the VNode system instead of the list reconciler.
JSX uses standard HTML attributes: Use
classnotclassName,fornothtmlFor,onInputnotonChangefor per-keystroke updates. Pyreon maps to native DOM events, not the React synthetic event system.