Components API
Component creation and built-in components.
component
Create a SignalX component.
function component<TCombined, TRef, TSlots>(
setup: (ctx: ComponentSetupContext) => ViewFn | Promise<ViewFn>,
options?: ComponentOptions
): ComponentFactory<TCombined, TRef, TSlots>;
Parameters
| Name | Type | Description |
|---|---|---|
setup | (ctx) => ViewFn | Setup function receiving context |
options.name | string | Component name for DevTools |
Returns
ComponentFactory - A function usable as a JSX element.
Setup Context
| Property | Type | Description |
|---|---|---|
signal | typeof signal | Create reactive state |
props | PropsAccessor<TProps> | Reactive props |
slots | SlotsObject<TSlots> | Slot functions |
emit | EmitFn<TEvents> | Emit custom events |
el | PlatformElement | Root element (after mount) |
parent | any | Parent component |
onMounted | (fn) => void | Register mount callback |
onUnmounted | (fn) => void | Register unmount callback |
onCreated | (fn) => void | Register creation callback |
onUpdated | (fn) => void | Register update callback |
expose | (api) => void | Expose API to parent |
update | () => void | Force re-render |
Examples
import { component, type Define } from 'sigx';
type CardProps =
& Define.Prop<'title', string, true>
& Define.Event<'close'>
& Define.Slot<'default'>
& Define.Slot<'footer'>;
const Card = component<CardProps>(({ props, slots, emit }) => {
return () => (
<div class="card">
<header>
{props.title}
<button onClick={() => emit('close')}>×</button>
</header>
<main>{slots.default?.()}</main>
{slots.footer?.()}
</div>
);
}, { name: 'Card' });
// Usage
<Card
title="Welcome"
onClose={() => setOpen(false)}
slots={{
footer: () => <button>OK</button>
}}
>
<p>Card content</p>
</Card>
Fragment
Render children without a wrapper element.
const Fragment: Symbol;
Usage
import { Fragment } from 'sigx';
// Using Fragment component
<Fragment>
<li>Item 1</li>
<li>Item 2</li>
</Fragment>
// Using short syntax
<>
<li>Item 1</li>
<li>Item 2</li>
</>
lazy
Create a lazy-loaded component.
function lazy<T extends ComponentFactory>(
loader: () => Promise<T | { default: T }>
): LazyComponentFactory<T>;
Parameters
| Name | Type | Description |
|---|---|---|
loader | () => Promise<Component> | Dynamic import function |
Returns
LazyComponentFactory with:
preload(): Promise<T>- Start loading before renderisLoaded(): boolean- Check if loaded
Examples
import { lazy, Defer } from 'sigx';
const HeavyChart = lazy(() => import('./HeavyChart'));
// Basic usage — Defer shows the fallback while the chunk loads
<Defer fallback={<Spinner />}>
<HeavyChart />
</Defer>
// Preload on hover
<button onMouseEnter={() => HeavyChart.preload()}>
Show Chart
</button>
// Check load status
if (HeavyChart.isLoaded()) {
console.log('Ready');
}
Defer
Show fallback content while lazy component chunks load. <Defer>'s fallback covers chunk loading only — a pending useData read renders through its own component's match(), never through a wrapper. On the server, the fallback streams with the shell and a single replacement arrives when everything pending beneath it (chunks and keyed data) resolves.
const Defer: ComponentFactory<DeferProps>;
Props
| Name | Type | Description |
|---|---|---|
fallback | JSXElement | () => JSXElement | Content while chunks below load |
Examples
import { lazy, Defer } from 'sigx';
const Dashboard = lazy(() => import('./Dashboard'));
// Static fallback
<Defer fallback={<div>Loading...</div>}>
<Dashboard />
</Defer>
// Dynamic fallback
<Defer fallback={() => <Spinner size={size} />}>
<Dashboard />
</Defer>
// Multiple lazy components — one fallback until all chunks resolve
<Defer fallback={<Loading />}>
<LazyHeader />
<LazyContent />
</Defer>
Portal
Render children to a different DOM location.
const Portal: ComponentFactory<PortalProps>;
Props
| Name | Type | Default | Description |
|---|---|---|---|
to | string | Element | document.body | Target container |
disabled | boolean | false | Render in-place instead |
Examples
import { Portal } from 'sigx';
// Render to body (default)
<Portal>
<div class="modal">Modal content</div>
</Portal>
// Render to specific element
<Portal to="#modal-root">
<div class="modal">Modal content</div>
</Portal>
// Render to element reference
<Portal to={containerElement}>
<Tooltip />
</Portal>
// Conditional portal
<Portal disabled={inline}>
<Dropdown />
</Portal>
isLazyComponent
Check if a component is lazy-loaded.
function isLazyComponent(component: any): component is LazyComponentFactory;
Examples
import { lazy, isLazyComponent } from 'sigx';
const LazyComp = lazy(() => import('./Comp'));
const RegularComp = component(() => () => <div />);
isLazyComponent(LazyComp); // true
isLazyComponent(RegularComp); // false
getCurrentInstance
Get the current component's setup context.
function getCurrentInstance(): ComponentSetupContext | null;
Returns
The current component context, or null if not in a component.
Examples
import { getCurrentInstance } from 'sigx';
function useCustomHook() {
const ctx = getCurrentInstance();
if (!ctx) {
throw new Error('Must be called in component setup');
}
ctx.onMounted(() => {
console.log('Component mounted');
});
}
Data Loading
useData
The keyed async read. Every read has a key — the reactive trigger, the cache / SSR identity, and the fetcher's input. A static string key is SSR-transferable (runs on the server, serialized under the key into window.__SIGX_ASYNC__, restored on hydration, deduped per key). A reactive getter returning a string or tuple re-runs on change; a falsy result holds the read in idle. There is no unkeyed form.
// Static key: SSR + hydration state transfer.
function useData<T>(
key: string,
fetcher: (arg: string, ctx: { signal: AbortSignal }) => Promise<T>,
opts?: AsyncOptions,
): AsyncState<T>;
// Reactive key (string or tuple); falsy ⇒ 'idle'.
function useData<T, const K extends KeyValue>(
key: () => K | Falsy,
fetcher: (arg: K, ctx: { signal: AbortSignal }) => Promise<T>,
opts?: AsyncOptions,
): AsyncState<T>;
interface AsyncOptions {
server?: boolean; // default true; false ⇒ client-only keyed read
}
Render every state with match(); refresh() re-runs in place (stale-while-revalidate) and never rejects. See the Data Loading guide.
useAction
The async write — the manual counterpart to useData, triggered by .run(input). run never rejects; it resolves a settled RunResult<T>. In-flight writes are never aborted.
function useAction<T, In = void>(
fn: (input: In, ctx: { signal: AbortSignal }) => Promise<T>,
opts?: ActionOptions,
): AsyncAction<T, In>;
all
Combine several AsyncStates into one all-or-nothing state — a single match() for a whole view. A pure derived view; no fetching.
// Object form (named value/errors records).
function all<S extends Record<string, AsyncState<unknown>>>(sources: S): AllState<…>;
// Rest-tuple form (positional).
function all<S extends readonly AsyncState<unknown>[]>(...sources: S): AllState<…>;
AllState<T, E> extends AsyncState<T> with an errors collect-all counterpart to first-error-wins .error.
useStream
Accumulate a streamed AsyncIterable<string> into a reactive string — progressive text (LLM-token-style), SSR-aware via the key.
function useStream(
key: string,
source: () => AsyncIterable<string>
): { readonly value: string };
Helper Functions
supportsMoveBefore
Check if browser supports state-preserving DOM moves.
function supportsMoveBefore(): boolean;
moveNode
Move a DOM node with state preservation when available.
function moveNode(
parent: Element,
node: Node,
anchor?: Node | null
): void;
Types
ComponentFactory
type ComponentFactory<TCombined, TRef, TSlots> =
((props: ComponentProps) => JSXElement) & {
__setup: SetupFn;
__name?: string;
__props: TCombined;
__ref: TRef;
__slots: TSlots;
};
LazyComponentFactory
type LazyComponentFactory<T> = T & {
preload: () => Promise<T>;
isLoaded: () => boolean;
__lazy: true;
};
DeferProps
type DeferProps =
& Define.Prop<'fallback', JSXElement | (() => JSXElement)>
& Define.Slot<'default'>;
PortalProps
type PortalProps =
& Define.Prop<'to', string | Element>
& Define.Prop<'disabled', boolean>;
ViewFn
type ViewFn = () => JSXElement | JSXElement[] | undefined;
ComponentOptions
interface ComponentOptions {
name?: string;
}
AsyncState
interface AsyncState<T> {
readonly state: 'idle' | 'pending' | 'ready' | 'refreshing' | 'errored';
readonly value: T | null; // SWR last-good; kept across same-key refresh, cleared on key change
readonly error: Error | null;
readonly loading: boolean; // state === 'pending' ONLY (refresh reads state === 'refreshing')
match<R>(arms: MatchArms<T, R>): R | undefined;
refresh(): Promise<void>; // re-run in place; NEVER rejects (failures land on .error)
}
MatchArms
interface MatchArms<T, R> {
idle?: () => R; // defaults to `pending`
pending?: () => R; // omitted ⇒ renders nothing
error?: (e: Error, retry: () => void, stale: T | null) => R; // omitted ⇒ bubbles to errorScope / app onError
ready: (v: T) => R; // required — the only route to a non-null T
}
AsyncAction
interface AsyncAction<T, In> {
readonly state: 'idle' | 'pending' | 'ready' | 'errored';
readonly value: T | null; // last successful result
readonly error: Error | null;
readonly loading: boolean; // state === 'pending' — the double-submit guard
match<R>(arms: MatchArms<T, R>): R | undefined;
run(input: In): Promise<RunResult<T>>; // never rejects; in-flight runs never aborted
reset(): void; // back to 'idle'; clears value/error
}
type RunResult<T> = { ok: true; value: T } | { ok: false; error: Error };
A superseded run resolves { ok: false, error: SupersededError } and never writes state.
