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
SuspenseShows the fallback until the all resources to be received inside the callbackutils.Suspense

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.

Suspense ​

Suspense is a boundary that groups resources created inside its callback. While any of them is pending, fallback renders; once all resolve, the real content appears.

Type Signature ​

typescript
const Suspense: PC<{
    callback: () => any;
    fallback?: any;
}>

Props ​

PropTypeRequiredDescription
callback() => anyYesLazily builds the content tree; resources created inside it are tracked.
fallbackanyNoRendered while any resource inside the boundary is pending.

Basic usage ​

tsx
import type { PC } from 'preffx';

const Profile: PC = (_, { resource }) => {
    const [user] = resource(async () => (await fetch('/api/user')).json());
    return <h1>{user.state.value?.name ?? '...'}</h1>;
};

export const ProfilePage: PC = (_, { Suspense }) => {
    return (
        <Suspense callback={() => <Profile />} fallback={<p>Loading...</p>} />
    );
};

SSR ​

On the server, the boundary registers its resources for preload() and embeds the resolved data; on the client it hydrates without re-fetching.

Released under the Apache-2.0 License.