Basic Components
These components are available directly from the refui package.
If
Conditionally renders content based on a condition or true prop. Works with both static values and reactive signals.
Note: The else prop has higher priority than providing a second child function for the else case. The true prop has higher priority than the condition prop.
import { If, signal, $ } from 'refui'
// With reactive signals
const App = () => {
const isLoggedIn = signal(false);
const userName = signal('John');
return (
<div>
<If condition={isLoggedIn}>
{() => <span>Welcome back, {userName}!</span>}
{() => <span>Please log in to continue.</span>}
</If>
<button on:click={() => isLoggedIn.value = !isLoggedIn.value}>
{isLoggedIn.choose('Logout', 'Login')}
</button>
</div>
);
};
// Using the 'else' prop for cleaner syntax
const AppAlternative = ({ value }) => {
return (
<If
condition={value}
else={() => <span>Condition is false.</span>}
>
{() => <span>Condition is true!</span>}
</If>
)
}
For
Renders a list of items from a signal that resolves to an array (or any value exposing the signal interface like .value and .trigger()). For is reactive to the list change itself, with a highly optimized reconcile algorithm that only executes the least necessary steps to update the list. Passing a plain array renders a static snapshot; updates and the imperative helpers require a reactive signal.
The child of <For> must be an item method that accepts { item, index } and returns a renderable value. It can be declared inline or as a separate function, but it is not constructed as a component.
For calls each item method directly inside that item's retained disposal scope. Signals, effects, and onDispose created by the method still belong to the item and are released when it leaves the list. The method itself does not receive a component $ref, a separate getCurrentSelf() identity, or its own HMR boundary. When a row needs those component features, invoke that component from the item method and request its boundary with a truthy $ref.
TypeScript users can import ForMethod<T> when declaring a reusable item method.
When a cleared list owns its parent’s complete child range and the host renderer
supports guarded bulk clearing, For clears that range once while still
running every item cleanup. Lists with siblings, and renderers without the
optional capability, use ordinary individual removal.
Items are tracked by the value of each entry by default, but when you're replacing the whole array by loading it from other sources, provide a track prop with the name of the key property in your data objects. Keys must be unique within the list. You can also set indexed={true} to receive a signal containing the item's current index in the child props object: ({ item, index }).
Note: If you directly modify a non-signal property on an item from the list, the UI will not update. For lists with reactive items that need granular updates, use the UnKeyed component instead.
import { signal, For, $ } from 'refui';
// Define a separate item method for better organization
const renderTodoItem = ({ item, index }) => {
const toggleTodo = () => {
item.completed.value = !item.completed.value;
};
return (
<li>
<span
style:textDecoration={item.completed.choose('line-through', 'unset')}
>
{$(() => index.value + 1)}. {item.text}
</span>
<button on:click={toggleTodo}>
{item.completed.choose('Undo', 'Complete')}
</button>
</li>
);
};
export const TodoList = () => {
const newTodoText = signal('');
const todos = signal([
{ text: 'Learn rEFui', completed: signal(false) },
{ text: 'Build an app', completed: signal(false) },
]);
const addTodo = () => {
const newTodo = {
text: newTodoText.peek(),
completed: signal(false),
};
todos.value.push(newTodo);
todos.trigger();
newTodoText.value = ''
};
return (
<div>
<input value={newTodoText} on:input={(e) => { newTodoText.value = e.target.value }} />
<button on:click={addTodo}>Add Todo</button>
<ul>
<For entries={todos} indexed={true}>
{renderTodoItem}
</For>
</ul>
</div>
);
};
// Alternatively, you can use an inline function for simpler cases:
const SimpleTodoList = () => {
const todos = signal([
{ id: 1, text: 'Learn rEFui' },
{ id: 2, text: 'Build an app' },
]);
return (
<ul>
<For entries={todos}>
{({ item }) => <li>{item.text}</li>}
</For>
</ul>
);
};
Optimization Tip: Keep Updates Local
For large lists, avoid global state that triggers a full list re-render. Instead:
- Track by Key: Use a stable, unique
trackprop so only changed items are reconciled when the full list is recreated (for example, after a request). - Localized State: Use
onConditionor class toggles within individual list items to handle per-row selection or visibility states. This ensures that changing the state of one row doesn't affect the entire list.
Exposed Methods
The <For> component exposes several methods via its optional expose prop (v0.8.0+), allowing you to interact with the list imperatively.
getItem(key): Retrieves the original data item associated with a given key. (Only available whentrackis used).remove(key): Removes an item from the list by its key. (Only available whentrackis used).clear(): Removes all items from the list.
Here's an example of how to use them:
import { signal, For, $ } from 'refui';
const InteractiveList = () => {
const listApi = signal(null);
const items = signal([
{ id: 1, text: 'First' },
{ id: 2, text: 'Second' },
{ id: 3, text: 'Third' },
]);
const removeItem = () => {
// Remove item with id 2
listApi.value?.remove(2);
};
const clearList = () => {
listApi.value?.clear();
};
return (
<div>
<For
entries={items}
track="id"
expose={(api) => { listApi.value = api; }}
>
{({ item }) => <div>{item.text}</div>}
</For>
<button on:click={removeItem}>Remove Second</button>
<button on:click={clearList}>Clear All</button>
</div>
);
};
Fn
Executes a function that returns a render function ((R) => Node). This is useful for complex conditional logic that doesn't neatly fit into an If component. Fn is also the building block for all other built-in components except for For. The R parameter can be omitted when using Reflow renderer.
Performance Tip: Define the returned render functions outside the Fn scope to prevent them from being recreated on every render cycle.
import { Fn, read } from 'refui'
const renderA = (R) => <div>Condition: 123</div>
const renderB = (R) => <div>Condition: 456</div>
const renderDefault = (R) => <div>Nothing matched!</div>
const App = ({ condition }) => {
return (
<Fn>
{() => {
switch (read(condition)) {
case 123:
return renderA
case 456:
return renderB
default:
return renderDefault
}
}}
</Fn>
)
}
Inline helper functions that you place directly in JSX are evaluated immediately—rEFui keeps calling the returned value with renderer object as the only parameter until it resolves to a concrete node. Because this evaluation happens synchronously during render, no reactive tracking is established. Use them only for constant branches or to invoke pure helpers. When you need the branching to respond to signals, keep the logic inside <Fn> or derive a computed signal instead.
Caveat (signals inside Fn): Avoid creating a new signal and immediately reading it inside the Fn handler body itself (the function you pass as <Fn>{handler}</Fn>). That handler runs in a tracked reactive scope; if it allocates a signal and then updates it elsewhere, it can schedule itself again, creating a new signal without the update and making the updated value useless. Instead, create signals either:
- Outside the
Fn(e.g. in the parent component), or - Inside the render function that the handler returns (
(R) => ...),
so that updates happen from stable reactive owners rather than from the Fn control body.
Note: Although Fn is much more efficient when updating than re-rendering the whole tree in other immediate mode frameworks like React, it's still more expensive than signals for rendering texts only. If you want simple conditional text like adding s/es to plural nouns, just use a computed signal.
Advanced Usage: ctx and catch
The Fn component accepts two additional props for more advanced scenarios:
ctx: A value or signal that is passed as the first argument to the child handler function. This is useful for providing context to the handler without creating closures in the render path.catch: A function that gets called if an error is thrown during the rendering of the handler's result. It receives theerror, the componentname, and thectxas arguments, allowing you to create robust error boundaries.
Here's how you can use them together:
import { Fn, read, signal } from 'refui'
// This component might throw an error
const UserProfile = ({ user }) => {
if (!user || !user.name) {
throw new Error("User name is missing!");
}
return <div>Welcome, {user.name}</div>;
};
const App = () => {
const userSignal = signal({ name: 'John Doe' });
// A handler to render error states
const renderError = (error, name, ctx) => (
<div style="color: red;">
<p>Oops! Something went wrong in "{name}":</p>
<p><b>{error.message}</b></p>
<p>Context when error occurred:</p>
<pre>{JSON.stringify(read(ctx), null, 2)}</pre>
</div>
);
// The handler function passed to <Fn>. It receives the context.
const userProfileHandler = (user) => UserProfile({ user });
setTimeout(() => userSignal.value = { name: null }, 2000); // Simulate an error condition
return (
<Fn ctx={userSignal} catch={renderError} name="UserProfileBoundary">
{userProfileHandler}
{renderError}
{/* Alternatively, handleError can be written as the second child of Fn */}
</Fn>
);
};
Dynamic
Renders a component that can change over time. The component can be specified as a string (for HTML tags) or a component function, which can be wrapped in a signal for dynamic updates.
When the selected value is a function, HMR is inactive, and neither current nor $ref contains a truthy ref, Dynamic invokes the function directly inside its existing replacement scope. Its signals, effects, and cleanup still follow the selected view's lifetime without allocating a second component wrapper.
Supplying a truthy current/$ref keeps the renderer's full component-construction path so the ref receives the selected component instance. HMR builds also retain that path so selected components remain replaceable. String tag names always go through the renderer.
Advanced Usage - Dynamic Components with Props:
import { signal, derivedExtract, Dynamic } from 'refui';
const Card = ({ title, color = 'white' }) => (
<div style={`background: ${color}; padding: 20px; border-radius: 8px;`}>
<h3>{title}</h3>
</div>
);
const Alert = ({ message, type = 'info' }) => (
<div style={`border: 2px solid ${type === 'error' ? 'red' : 'blue'}; padding: 10px;`}>
{message}
</div>
);
const DynamicDemo = () => {
const currentComponent = signal(Card);
// Use a single signal for all props
const props = signal({
title: 'My Card',
color: 'lightblue',
message: 'This is an alert!',
type: 'info'
});
const switchComponent = () => {
currentComponent.value = currentComponent.value === Card ? Alert : Card;
};
// Create individual reactive signals for each prop
const { title, color, message, type } = derivedExtract(props);
return (
<div>
<Dynamic
is={currentComponent}
title={title}
color={color}
message={message}
type={type}
/>
<button on:click={switchComponent}>Switch Component</button>
</div>
);
};
You can also use the <Dynamic> component with the is prop for the same effect:
const ComponentSwitcher = () => {
const currentTag = signal('button');
const message = signal('Click to change tag!');
return (
<div>
<Dynamic
is={currentTag}
on:click={() => {
currentTag.value = currentTag.value === 'button' ? 'div' : 'button';
message.value = `Now I'm a ${currentTag.value}!`;
}}
style="padding: 10px; border: 1px solid #ccc; margin: 5px;"
>
{message}
</Dynamic>
<p>Current element: <{currentTag}></p>
</div>
);
};
Render
Renders a component instance that was created separately using createComponent. This is useful for manually managing component lifecycles or rendering components stored in signals.
import { Render, createComponent, signal } from 'refui'
const MyComponent = ({ message }) => <div>Message: {message}</div>;
const App = () => {
const componentInstance = createComponent(MyComponent, { message: 'Hello World!' })
const currentInstance = signal(componentInstance)
return (
<div>
<h1>Rendered Component:</h1>
<Render from={currentInstance} />
<button on:click={() => {
const newInstance = createComponent(MyComponent, { message: 'Updated!' })
currentInstance.value = newInstance
}}>
Update Instance
</button>
</div>
)
};
memo
Provides component-scoped memoization for one function evaluation. Call memo inside a component to capture the current context, then reuse the returned function to access its immediate cached result without re-running the original logic.
Parameters:
fn: A function that produces the value you want to cache. It is executed the first time the memoized wrapper runs.
Returns: A function that, when called, returns the cached result from the initial invocation.
Usage Notes
- The wrapped function runs at most once during the memo wrapper's lifetime. Subsequent calls return the cached value.
- The captured context ensures that any signals read during the first execution are tracked correctly, and
onDisposehandlers registered insidefnare tied to the component lifecycle. - Because the value never re-computes automatically, avoid reading reactive data inside
fnif you expect it to change. Use signals or derived values outside ofmemowhen you need updates. - Call
memoinline inside the component factory or inside the returned render function. Hoistingmemooutside the component will capture the wrong context and break caching. If you prefer to prepare helpers up front, use the provideduseMemowrapper and invoke it inside the component. The corresponding factory for retained subtrees isuseKeepAlive. memo(Page)caches the render function returned byPage; it does not cache the concrete renderer node produced by that render function. A later mount can therefore create a fresh renderer subtree while retaining the memoized setup result. UsekeepAlive(Page)when the concrete subtree must survive temporary detachment.- Once the owning component disposes, the memoized wrapper no longer has a live context; calling it after teardown behaves like an untracked function call.
Unlike React or Solid, memo here captures the current reactive context and defers execution until the returned function is actually run (for example when a conditional branch is selected). This allows inline usage inside JSX-style control flow without introducing dedicated hooks.
Inline Branching Example
import { memo, signal, If, $ } from 'refui'
const ToggleMessage = () => {
const isOpen = signal(false)
return (
<div>
<button on:click={() => isOpen.value = !isOpen.value}>
{isOpen.choose('Hide', 'Show')} details
</button>
<If condition={isOpen}>
{memo(() => <p class="details">Detailed view created once</p>)}
{memo(() => <p class="summary">Summary created once</p>)}
</If>
</div>
)
}
keepAlive
Keeps both a component's setup result and its first concrete renderer result alive for the lifetime of the captured owner scope. This is the retained-subtree operation to use for pages or other expensive views that should be detached and later reattached without reconstructing their nodes.
Parameters:
component: The component template whose setup and rendered subtree should be retained.
Returns: A component template suitable for Dynamic or ordinary renderer component creation.
Usage Notes
- Call
keepAliveinside the component or scope that should own the retained subtree. It captures that scope in the same way asmemo. - If the helper must be declared outside components, use
useKeepAlive(component)and invoke the returned factory once inside each intended owner. - Removing the result from
Dynamicdetaches its node but does not dispose its signals, effects, keyedForcache, or renderer subtree. - Re-selecting it reattaches the same concrete node. The component setup and render function do not run again.
- Disposing the owning scope releases the retained subtree and all of its nested resources.
- A retained subtree is renderer-specific after its first mount and should not be mounted concurrently into multiple parents.
import { Dynamic, keepAlive, signal } from 'refui'
const currentPage = signal(null)
const App = () => {
const PlayerPage = keepAlive(Player)
currentPage.value = PlayerPage
return <Dynamic is={currentPage} />
}
useKeepAlive Factory Example
useKeepAlive(component) prepares a module-safe factory without capturing a component scope yet. Invoke that factory inside a component to create a retained component template owned by that particular component instance. Separate invocations never share their concrete nodes or cleanup lifetimes.
import { Dynamic, signal, useKeepAlive } from 'refui'
const preparePlayerPage = useKeepAlive(Player)
const App = () => {
const currentPage = signal(null)
const PlayerPage = preparePlayerPage()
currentPage.value = PlayerPage
return <Dynamic is={currentPage} />
}
This has the same retention behavior as calling keepAlive(Player) directly inside App: temporary detachment preserves the concrete subtree, while disposal of that App instance releases it.
useMemo Wrapper Example
The exported useMemo helper builds a wrapper you can invoke inside the component to obtain the memoized branch. This is useful when you want to reuse the same memo across multiple render positions.
Each call to useMemo returns a function; invoke that function inside your component so the enclosed memo call captures the correct reactive context.
import { useMemo, signal, If, $ } from 'refui'
const renderDetails = useMemo((R) => <p class="details">Detailed view created once</p>)
const renderSummary = useMemo((R) => <p class="summary">Summary created once</p>)
const ToggleMessage = () => {
const isOpen = signal(false)
return (
<div>
<button on:click={() => isOpen.value = !isOpen.value}>
{isOpen.choose('Hide', 'Show')} details
</button>
<If condition={isOpen}>
{renderDetails()}
{renderSummary()}
</If>
</div>
)
}