Components API#

Component creation and built-in components.

component#

Create a SignalX component.

TSX
function component<TCombined, TRef, TSlots>(
    setup: (ctx: ComponentSetupContext) => ViewFn | Promise<ViewFn>,
    options?: ComponentOptions
): ComponentFactory<TCombined, TRef, TSlots>;

Parameters#

NameTypeDescription
setup(ctx) => ViewFnSetup function receiving context
options.namestringComponent name for DevTools

Returns#

ComponentFactory - A function usable as a JSX element.

Setup Context#

PropertyTypeDescription
signaltypeof signalCreate reactive state
propsPropsAccessor<TProps>Reactive props
slotsSlotsObject<TSlots>Slot functions
emitEmitFn<TEvents>Emit custom events
elPlatformElementRoot element (after mount)
parentanyParent component
onMounted(fn) => voidRegister mount callback
onUnmounted(fn) => voidRegister unmount callback
onCreated(fn) => voidRegister creation callback
onUpdated(fn) => voidRegister update callback
expose(api) => voidExpose API to parent
update() => voidForce re-render

Examples#

TSX
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.

TSX
const Fragment: Symbol;

Usage#

TSX
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.

TSX
function lazy<T extends ComponentFactory>(
    loader: () => Promise<T | { default: T }>
): LazyComponentFactory<T>;

Parameters#

NameTypeDescription
loader() => Promise<Component>Dynamic import function

Returns#

LazyComponentFactory with:

  • preload(): Promise<T> - Start loading before render
  • isLoaded(): boolean - Check if loaded

Examples#

TSX
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.

TSX
const Defer: ComponentFactory<DeferProps>;

Props#

NameTypeDescription
fallbackJSXElement | () => JSXElementContent while chunks below load

Examples#

TSX
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.

TSX
const Portal: ComponentFactory<PortalProps>;

Props#

NameTypeDefaultDescription
tostring | Elementdocument.bodyTarget container
disabledbooleanfalseRender in-place instead

Examples#

TSX
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.

TSX
function isLazyComponent(component: any): component is LazyComponentFactory;

Examples#

TSX
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.

TSX
function getCurrentInstance(): ComponentSetupContext | null;

Returns#

The current component context, or null if not in a component.

Examples#

TSX
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.

TSX
// 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.

TSX
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.

TSX
// 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.

TSX
function useStream(
    key: string,
    source: () => AsyncIterable<string>
): { readonly value: string };

Helper Functions#

supportsMoveBefore#

Check if browser supports state-preserving DOM moves.

TSX
function supportsMoveBefore(): boolean;

moveNode#

Move a DOM node with state preservation when available.

TSX
function moveNode(
    parent: Element,
    node: Node,
    anchor?: Node | null
): void;

Types#

ComponentFactory#

TSX
type ComponentFactory<TCombined, TRef, TSlots> = 
    ((props: ComponentProps) => JSXElement) & {
        __setup: SetupFn;
        __name?: string;
        __props: TCombined;
        __ref: TRef;
        __slots: TSlots;
    };

LazyComponentFactory#

TSX
type LazyComponentFactory<T> = T & {
    preload: () => Promise<T>;
    isLoaded: () => boolean;
    __lazy: true;
};

DeferProps#

TSX
type DeferProps =
    & Define.Prop<'fallback', JSXElement | (() => JSXElement)>
    & Define.Slot<'default'>;

PortalProps#

TSX
type PortalProps = 
    & Define.Prop<'to', string | Element>
    & Define.Prop<'disabled', boolean>;

ViewFn#

TSX
type ViewFn = () => JSXElement | JSXElement[] | undefined;

ComponentOptions#

TSX
interface ComponentOptions {
    name?: string;
}

AsyncState#

TSX
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#

TSX
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#

TSX
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.