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.
| Component | Purpose | Availability |
|---|---|---|
For | Render lists with reactive items | utils.For |
Catch | Catch and handle render errors | utils.Catch |
Portal | Render children into a different DOM node | utils.Portal |
Defer | Show a placeholder until a value is ready | utils.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
const For: PC<{
items: Signal<any[]>;
callback: (item: any) => any;
fallback?: any;
}>Props
| Prop | Type | Required | Description |
|---|---|---|---|
items | Signal<any[]> | Yes | A 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) => any | Yes | Function 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. |
fallback | any | No | Content rendered when the array is empty. |
Usage
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:
// 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 shownSignal 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:
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
const Catch: PC<{
fallback?: any;
children?: any | any[];
}>Props
| Prop | Type | Required | Description |
|---|---|---|---|
children | any | any[] | No | Children to render. If any child is (or returns) an Error instance, the error is caught. |
fallback | any | No | Content to show when errors are caught. Can be a static value or a function component that receives { errors, ...extraProps }. |
Fallback as static content
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>:
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
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
const Portal: PC<{
root: HTMLElement;
children?: any | any[];
}>Props
| Prop | Type | Required | Description |
|---|---|---|---|
root | HTMLElement | Yes | The target DOM element where children should be rendered. |
children | any | any[] | No | Content to render inside the portal root. |
Basic usage
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:
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:
<Portal root={null} children={<span>Won't appear</span>} />
// renders nothingDefer
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
const Defer: PC<{
initial?: any;
value: Signal<any>;
}>Props
| Prop | Type | Required | Description |
|---|---|---|---|
initial | any | No | Placeholder content rendered while value contains a Promise or is not yet ready. |
value | Signal<any> | Yes | A signal whose value is observed. When the signal's resolved value is not a Promise, it replaces the initial content. |
Defer a Promise
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:
- Immediately shows
Loading... - 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:
<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:
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:
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> immediatelyCleanup
When the parent component is destroyed, Defer cleans up its internal signal tracking automatically.