Skip to content

Built-in Components

PreffX ships with four built-in components accessible via the second argument (utils) of every component. They solve common UI patterns without extra dependencies.

ComponentPurposeAvailability
ForRender lists with reactive itemsutils.For
CatchCatch and handle render errorsutils.Catch
PortalRender children into a different DOM nodeutils.Portal
DeferShow a placeholder until a value is readyutils.Defer

For

Reactive list renderer. Accepts a signal of items (or a plain array) and re-renders only the items that change. Supports an optional fallback shown when the list is empty.

Type Signature

typescript
const For: PC<{
    items: Signal<any[]>;
    callback: (item: any) => any;
    fallback?: any;
}>

Props

PropTypeRequiredDescription
itemsSignal<any[]>YesA signal holding the array of items. When the signal changes, For efficiently reconciles the list — added items are rendered, removed items are cleaned up.
callback(item: any) => anyYesFunction called for each item. Returns the JSX to render for that item. The returned value is cached and only recalculated when the item value itself changes.
fallbackanyNoContent rendered when the array is empty.

Usage

tsx
import type { PC } from 'preffx';

type Item = { id: number; name: string };

const App: PC = (_, { signal, For }) => {
    const items = signal<Item[]>([
        { id: 1, name: 'Alpha' },
        { id: 2, name: 'Beta' },
        { id: 3, name: 'Gamma' },
    ]);

    return (
        <ul>
            <For
                items={items}
                callback={(item: Item) => (
                    <li key={item.id}>{item.name}</li>
                )}
                fallback={<li>No items</li>}
            />
        </ul>
    );
};

Reactive updates

When you mutate the array in the signal, the DOM updates automatically:

tsx
// Add an item
items.value = [...items.value, { id: 4, name: 'Delta' }];

// Remove an item
items.value = items.value.filter((i) => i.id !== 2);

// Replace all items
items.value = [];
// -> fallback "No items" is shown

Signal items inside the array

Each item in the array can itself be a signal. When an item's signal value changes, only that item's output is re-computed:

tsx
const items = signal([
    signal({ id: 1, name: 'Alpha' }),
    signal({ id: 2, name: 'Beta' }),
]);

// Update the second item — only "Beta" re-renders
items.value[1].value = { id: 2, name: 'BETA' };

Catch

Catches errors thrown (or returned) by child components and renders a fallback instead. Inspired by React Error Boundaries.

Type Signature

typescript
const Catch: PC<{
    fallback?: any;
    children?: any | any[];
}>

Props

PropTypeRequiredDescription
childrenany | any[]NoChildren to render. If any child is (or returns) an Error instance, the error is caught.
fallbackanyNoContent to show when errors are caught. Can be a static value or a function component that receives { errors, ...extraProps }.

Fallback as static content

tsx
import type { PC } from 'preffx';

const App: PC = (_, { Catch }) => {
    return (
        <div>
            <Catch fallback={<div>Something went wrong</div>}>
                <MaybeBrokenComponent />
            </Catch>
        </div>
    );
};

Fallback as a function component

The fallback function receives errors (the array of caught errors) plus any additional props passed to <Catch>:

tsx
const App: PC = (_, { Catch }) => {
    return (
        <Catch
            fallback={({ errors }) => (
                <div class="error-fallback">
                    <h3>{errors.length} error(s) occurred</h3>
                </div>
            )}
        >
            <UnstableComponent />
        </Catch>
    );
};

Passing extra props to the fallback

tsx
const App: PC = (_, { Catch }) => {
    return (
        <Catch
            fallback={({ errors, severity }) => (
                <div class={severity}>
                    Errors: {errors.length}
                </div>
            )}
            severity="critical"
        >
            <UnstableComponent />
        </Catch>
    );
};

How errors are detected

Catch scans all children recursively (using flat(Infinity)). Any child that is an Error instance (via instanceof Error) triggers the fallback.


Portal

Renders children into a different DOM node outside the component's root. Useful for modals, tooltips, dropdowns, and notifications.

Type Signature

typescript
const Portal: PC<{
    root: HTMLElement;
    children?: any | any[];
}>

Props

PropTypeRequiredDescription
rootHTMLElementYesThe target DOM element where children should be rendered.
childrenany | any[]NoContent to render inside the portal root.

Basic usage

tsx
import type { PC } from 'preffx';

const Modal: PC = (_, { Portal }) => {
    const portalRoot = document.getElementById('modal-root')!;
    return (
        <Portal root={portalRoot}>
            <div class="modal-overlay">
                <div class="modal-content">Hello from portal!</div>
            </div>
        </Portal>
    );
};

Reactive content inside a portal

Children inside a portal are fully reactive — signals update them automatically:

tsx
const Notification: PC<{ message: string }> = (
    { message },
    { signal, Portal },
) => {
    const count = signal(0);

    return (
        <div>
            <Portal root={document.getElementById('toast-root')!}>
                <span id="notification-count">{count}</span>
            </Portal>
            <button onClick={() => (count.value += 1)}>+</button>
        </div>
    );
};

Cleanup

When the parent component is destroyed, Portal automatically cleans up its children and clears the portal root via root.replaceChildren().

Without a root

If root is null or undefined, Portal renders nothing:

tsx
<Portal root={null} children={<span>Won't appear</span>} />
// renders nothing

Defer

Shows an initial placeholder value while the actual value is being prepared (e.g., waiting for a Promise to resolve, or a signal to receive a non‑promise value).

Type Signature

typescript
const Defer: PC<{
    initial?: any;
    value: Signal<any>;
}>

Props

PropTypeRequiredDescription
initialanyNoPlaceholder content rendered while value contains a Promise or is not yet ready.
valueSignal<any>YesA signal whose value is observed. When the signal's resolved value is not a Promise, it replaces the initial content.

Defer a Promise

tsx
import type { PC } from 'preffx';

const SlowData: PC = (_, { signal, Defer }) => {
    const data = signal(
        new Promise<string>((resolve) =>
            setTimeout(() => resolve('Loaded!'), 2000)
        )
    );

    return (
        <Defer
            initial={<div>Loading...</div>}
            value={data}
        />
    );
};

Rendering sequence:

  1. Immediately shows Loading...
  2. After 2 seconds, when the Promise resolves, shows Loaded!

Defer a plain (non‑promise) value

If the signal already holds a plain value (not a Promise), Defer renders it immediately — the initial is skipped:

tsx
<Defer
    initial="Waiting..."
    value={signal('Real content')}
/>
// Immediately renders "Real content"

Reactive switch from Promise to plain value

When a signal changes from a Promise to a plain value, Defer switches from the placeholder to the real content:

tsx
const DynamicDefer: PC = (_, { signal, effect, Defer, onMount }) => {
    const value = signal(new Promise(() => {})); // pending promise

    onMount(() => {
        setTimeout(() => {
            value.value = 'Finally resolved!';
        }, 1000);
    });

    return <Defer initial="Loading..." value={value} />;
};

Array children via Defer

Defer works with any content, including arrays of JSX nodes:

tsx
const items = signal([
    <span id="a">Alpha</span>,
    <span id="b">Beta</span>,
]);

return (
    <Defer
        initial={[<div>Loading...</div>]}
        value={items}
    />
);
// Renders <span>Alpha</span><span>Beta</span> immediately

Cleanup

When the parent component is destroyed, Defer cleans up its internal signal tracking automatically.

Released under the Apache-2.0 License.