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:

Exposed Methods

The <For> component exposes several methods via its optional expose prop (v0.8.0+), allowing you to interact with the list imperatively.

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:

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:

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: &lt;{currentTag}&gt;</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 functions that should only run once during an instance's lifetime. Call memo inside a component to capture the current rendering context, then reuse the returned function to access the cached result without re-running the original logic.

Parameters:

Returns: A function that, when called, returns the cached result from the initial invocation.

Usage Notes

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>
	)
}

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>
	)
}