18.4 Control Flow and Props
Solid.js provides built-in components for conditional and list rendering. Instead of JavaScript's if, map, and switch, you use Show, For, Index, and Switch/Match. These components integrate tightly with the fine-grained reactivity system to guarantee optimal DOM updates.
Why Use Built-in Control Flow Components?
The Problem with JavaScript Expressions
// ❌ Ternary operator — both branches are evaluated whenever the condition changes
function BadComponent() {
const [show, setShow] = createSignal(true);
// <HeavyComponent /> JSX is evaluated even when show() is false
return (
<div>
{show() ? <HeavyComponent /> : <EmptyState />}
</div>
);
}
// ✅ Show — children are only evaluated when condition is true (lazy evaluation)
function GoodComponent() {
const [show, setShow] = createSignal(true);
return (
<div>
<Show when={show()} fallback={<EmptyState />}>
<HeavyComponent />
</Show>
</div>
);
}
Because Solid.js JSX component functions run only once, using a ternary operator does not actually cause the component to re-run. However, using Show gives you explicit control over when children are created and destroyed.
<Show> — Conditional Rendering
Basic Usage
import { Show } from 'solid-js';
import { createSignal } from 'solid-js';
function LoginStatus() {
const [isLoggedIn, setIsLoggedIn] = createSignal(false);
const [username, setUsername] = createSignal('John Doe');
return (
<div>
<Show
when={isLoggedIn()}
fallback={<button onClick={() => setIsLoggedIn(true)}>Login</button>}
>
{/* Rendered only when when is truthy */}
<div>
<span>Welcome, {username()}!</span>
<button onClick={() => setIsLoggedIn(false)}>Logout</button>
</div>
</Show>
</div>
);
}
Narrowing Types with the Render Prop Pattern
Passing children as a function to Show gives you the narrowed type of the when prop's value.
interface User {
name: string;
email: string;
role: 'admin' | 'user';
}
function UserCard() {
const [user, setUser] = createSignal<User | null>(null);
return (
<Show
when={user()}
fallback={<p>No user information found</p>}
>
{/* (u) => ... form: u has type User (null is excluded) */}
{(u) => (
<div class="user-card">
<h2>{u().name}</h2>
<p>{u().email}</p>
<Show when={u().role === 'admin'}>
<span class="badge">Admin</span>
</Show>
</div>
)}
</Show>
);
}
The when Prop in Detail
// Number (0 is falsy)
<Show when={count()}> ... </Show>
// String (empty string is falsy)
<Show when={errorMessage()}> ... </Show>
// Object/Array (null is falsy)
<Show when={data()}> ... </Show>
// Compound condition
<Show when={isAdmin() && hasPermission('write')}> ... </Show>
<For> — List Rendering
For is the standard way to render arrays. Each item is tracked by reference. This means that when items move within the array, their DOM nodes are moved rather than recreated.
Basic Usage
import { For } from 'solid-js';
import { createSignal } from 'solid-js';
interface Todo {
id: number;
text: string;
done: boolean;
}
function TodoList() {
const [todos, setTodos] = createSignal<Todo[]>([
{ id: 1, text: 'Exercise', done: false },
{ id: 2, text: 'Read a book', done: true },
{ id: 3, text: 'Write code', done: false },
]);
const toggleTodo = (id: number) => {
setTodos(prev =>
prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
);
};
return (
<ul>
{/* each: the Signal array to iterate */}
{/* fallback: content shown when the array is empty */}
<For
each={todos()}
fallback={<li>No todos yet.</li>}
>
{/* First argument: current item (value), second: index (Signal) */}
{(todo, index) => (
<li style={todo.done ? 'text-decoration: line-through' : ''}>
<span style="color: gray; margin-right: 0.5rem;">{index() + 1}.</span>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
)}
</For>
</ul>
);
}
How For Optimizes Rendering
Before: [A, B, C, D, E]
After: [A, C, B, D, E] (B and C swapped)
React (map + key): Destroy B, C DOM nodes → recreate them
Solid (For): Move B, C DOM nodes → much faster
Using For with Sorting and Filtering
function SortableList() {
const [items, setItems] = createSignal([
{ id: 1, name: 'Banana', price: 1500 },
{ id: 2, name: 'Apple', price: 2000 },
{ id: 3, name: 'Strawberry', price: 5000 },
{ id: 4, name: 'Grape', price: 3000 },
]);
const [sortBy, setSortBy] = createSignal<'name' | 'price'>('name');
const [ascending, setAscending] = createSignal(true);
const sorted = createMemo(() => {
const key = sortBy();
const asc = ascending();
return [...items()].sort((a, b) => {
const diff = a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0;
return asc ? diff : -diff;
});
});
return (
<div>
<div style="margin-bottom: 1rem;">
Sort by:
<button onClick={() => setSortBy('name')}>Name</button>
<button onClick={() => setSortBy('price')}>Price</button>
<button onClick={() => setAscending(a => !a)}>
{ascending() ? 'Ascending' : 'Descending'}
</button>
</div>
<ul>
<For each={sorted()}>
{(item) => (
<li>{item.name} — {item.price.toLocaleString()}</li>
)}
</For>
</ul>
</div>
);
}
<Index> — Index-based Rendering
Unlike For, Index keeps the index (position) stable. When an array item changes, the component at that index is updated in place.
For vs Index Differences
| Item | <For> | <Index> |
|---|---|---|
| Tracking basis | Item identity (reference) | Array index (position) |
| Item argument type | Value (immutable) | Signal (getter) |
| Index argument type | Signal (getter) | Value (immutable) |
| Best for | Arrays of objects (with keys) | Primitive arrays or frequent positional changes |
| On item change | DOM node moved | In-place update at that position |
import { Index } from 'solid-js';
import { createSignal } from 'solid-js';
function IndexExample() {
const [items, setItems] = createSignal(['Apple', 'Banana', 'Cherry']);
const updateItem = (idx: number, value: string) => {
setItems(prev => prev.map((item, i) => i === idx ? value : item));
};
return (
<div>
{/* Index: item is a Signal, index is a plain number */}
<Index each={items()}>
{(item, index) => (
<div>
<span>#{index}: </span>
{/* Must call item() to read the value */}
<input
value={item()}
onInput={(e) => updateItem(index, e.target.value)}
/>
</div>
)}
</Index>
</div>
);
}
Choosing Between For and Index
// ✅ Array of objects → use For (tracks by id reference)
<For each={users()}>
{(user) => <UserCard name={user.name} email={user.email} />}
</For>
// ✅ Primitive array + in-place editing → use Index
<Index each={tableRows()}>
{(row, rowIndex) => (
<Index each={row()}>
{(cell, colIndex) => (
<td onInput={(e) => updateCell(rowIndex, colIndex, e.target.value)}>
{cell()}
</td>
)}
</Index>
)}
</Index>
// ✅ Frequently changing sliding window → use Index
// e.g., chat message list (updates by position)
<Switch> + <Match> — Multiple Condition Handling
Use Switch and Match when you need to select among multiple conditions.
Basic Usage
import { Switch, Match } from 'solid-js';
type Status = 'loading' | 'error' | 'empty' | 'success';
function DataView() {
const [status, setStatus] = createSignal<Status>('loading');
const [data, setData] = createSignal<string[]>([]);
const [error, setError] = createSignal('');
return (
<div>
{/* Switch: only the first Match with a truthy when is rendered */}
<Switch fallback={<p>Unknown state.</p>}>
<Match when={status() === 'loading'}>
<LoadingSpinner />
</Match>
<Match when={status() === 'error'}>
<ErrorMessage message={error()} />
</Match>
<Match when={status() === 'empty'}>
<EmptyState message="No data available." />
</Match>
<Match when={status() === 'success'}>
<DataList items={data()} />
</Match>
</Switch>
{/* Test buttons */}
<div style="margin-top: 1rem;">
<button onClick={() => setStatus('loading')}>Loading</button>
<button onClick={() => { setStatus('error'); setError('Network error'); }}>Error</button>
<button onClick={() => setStatus('empty')}>Empty</button>
<button onClick={() => { setStatus('success'); setData(['Item 1', 'Item 2']); }}>Success</button>
</div>
</div>
);
}
Narrowing Types with Render Props
type Result =
| { type: 'ok'; value: string }
| { type: 'err'; message: string };
function ResultView() {
const [result, setResult] = createSignal<Result>({ type: 'ok', value: 'Success!' });
return (
<Switch>
<Match when={result().type === 'ok' && result()}>
{(r) => <p style="color: green">Success: {(r() as { type: 'ok'; value: string }).value}</p>}
</Match>
<Match when={result().type === 'err' && result()}>
{(r) => <p style="color: red">Error: {(r() as { type: 'err'; message: string }).message}</p>}
</Match>
</Switch>
);
}
<ErrorBoundary> — Error Handling
Catches JavaScript errors thrown in child components and displays a fallback UI.
Basic Usage
import { ErrorBoundary } from 'solid-js';
function BrokenComponent() {
throw new Error('An unexpected error occurred!');
return <div>This code will never run</div>;
}
function App() {
return (
<ErrorBoundary
fallback={(err, reset) => (
<div style="border: 2px solid red; padding: 1rem; border-radius: 8px;">
<h3>Something went wrong</h3>
<p>{err.message}</p>
<button onClick={reset}>Try Again</button>
</div>
)}
>
<BrokenComponent />
</ErrorBoundary>
);
}
Nested ErrorBoundaries
function Dashboard() {
return (
{/* App-level error boundary */}
<ErrorBoundary fallback={(err) => <CriticalError error={err} />}>
<Header />
{/* Independent error boundaries per widget */}
<div class="widgets">
<ErrorBoundary fallback={<WidgetError name="Weather" />}>
<WeatherWidget />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError name="Stocks" />}>
<StockWidget />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError name="News" />}>
<NewsWidget />
</ErrorBoundary>
</div>
</ErrorBoundary>
);
}
<Suspense> — Async Loading Handling
Suspense displays a fallback while a child component's createResource is loading.
Basic Usage
import { Suspense, createResource } from 'solid-js';
async function fetchData() {
await new Promise(r => setTimeout(r, 1500)); // Simulated delay
return { name: 'John Doe', email: 'john@example.com' };
}
function UserInfo() {
const [user] = createResource(fetchData);
// When data is loading inside a Suspense boundary, the fallback is shown automatically
return <div>{user()?.name}</div>;
}
function App() {
return (
<Suspense fallback={<div class="spinner">Loading...</div>}>
<UserInfo />
</Suspense>
);
}
Coordinating Multiple Loaders with SuspenseList
import { SuspenseList, Suspense } from 'solid-js';
function Dashboard() {
return (
{/* revealOrder: 'forwards' | 'backwards' | 'together' */}
<SuspenseList revealOrder="forwards" tail="collapsed">
<Suspense fallback={<Skeleton />}>
<UserCard />
</Suspense>
<Suspense fallback={<Skeleton />}>
<RecentOrders />
</Suspense>
<Suspense fallback={<Skeleton />}>
<Statistics />
</Suspense>
</SuspenseList>
);
}
Maintaining Props Reactivity
In Solid.js, props are a read-only reactive object. Because component functions run only once, you must always access props like a function to get the latest values.
Props Behave Like Functions
// ✅ props always returns the latest value from the parent via a Proxy object
function Greeting(props) {
// props.name always reflects the latest name passed by the parent
return <h1>Hello, {props.name}!</h1>;
}
// Parent component
function Parent() {
const [name, setName] = createSignal('John');
return (
<div>
<Greeting name={name()} />
<button onClick={() => setName('Jane')}>Change Name</button>
</div>
);
}
Never Destructure Props
// ❌ Destructuring props — value is frozen to its initial state!
function BadGreeting({ name, age }) {
// name = 'John', age = 30 — permanently fixed
// Even if the parent changes name to 'Jane', it won't update here
return <div>{name} (age {age})</div>;
}
// ✅ Always receive the full props object and access it directly
function GoodGreeting(props) {
return <div>{props.name} (age {props.age})</div>;
}
mergeProps — Setting Default Values
mergeProps reactively merges default values into props.
import { mergeProps } from 'solid-js';
interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
}
function Button(props: ButtonProps) {
// Set defaults — reactivity is preserved
const merged = mergeProps(
{ variant: 'primary', size: 'md', disabled: false },
props
);
return (
<button
class={`btn btn-${merged.variant} btn-${merged.size}`}
disabled={merged.disabled}
onClick={merged.onClick}
>
{merged.label}
</button>
);
}
// Usage
<Button label="Save" />
<Button label="Delete" variant="danger" size="lg" />
splitProps — Splitting Props
Use splitProps when you need to pass only some props to a child component or DOM element.
import { splitProps } from 'solid-js';
interface InputProps {
label: string;
error?: string;
// + all HTML input attributes
[key: string]: any;
}
function LabeledInput(props: InputProps) {
// Separate component-specific props from input element props
const [local, inputProps] = splitProps(props, ['label', 'error']);
return (
<div class="form-group">
<label>{local.label}</label>
{/* inputProps contains all props except label and error */}
<input {...inputProps} class={local.error ? 'input-error' : ''} />
{local.error && <span class="error-msg">{local.error}</span>}
</div>
);
}
// Usage
<LabeledInput
label="Email"
type="email"
value={email()}
onInput={(e) => setEmail(e.target.value)}
error={emailError()}
placeholder="example@email.com"
/>
Practical Example: Dashboard
import {
createSignal, createResource, createMemo,
For, Show, Switch, Match, Suspense, ErrorBoundary
} from 'solid-js';
// Type definitions
interface Stat {
label: string;
value: number;
change: number;
unit: string;
}
interface Order {
id: number;
customer: string;
amount: number;
status: 'pending' | 'processing' | 'done' | 'cancelled';
date: string;
}
// Mock API functions
async function fetchStats(): Promise<Stat[]> {
await new Promise(r => setTimeout(r, 800));
return [
{ label: "Today's Revenue", value: 4250000, change: 12.5, unit: '' },
{ label: 'New Orders', value: 48, change: -3.2, unit: '' },
{ label: 'Active Users', value: 1284, change: 8.1, unit: '' },
{ label: 'Conversion Rate', value: 3.8, change: 0.5, unit: '%' },
];
}
async function fetchOrders(): Promise<Order[]> {
await new Promise(r => setTimeout(r, 1200));
return [
{ id: 1001, customer: 'Alice', amount: 150000, status: 'done', date: '2026-03-21' },
{ id: 1002, customer: 'Bob', amount: 89000, status: 'processing', date: '2026-03-21' },
{ id: 1003, customer: 'Carol', amount: 230000, status: 'pending', date: '2026-03-21' },
{ id: 1004, customer: 'Dave', amount: 45000, status: 'cancelled', date: '2026-03-20' },
{ id: 1005, customer: 'Eve', amount: 178000, status: 'done', date: '2026-03-20' },
];
}
// Stat card component
function StatCard(props: { stat: Stat }) {
const isPositive = () => props.stat.change >= 0;
return (
<div class="stat-card">
<p class="stat-label">{props.stat.label}</p>
<p class="stat-value">
{props.stat.value.toLocaleString()}{props.stat.unit}
</p>
<p class={isPositive() ? 'change positive' : 'change negative'}>
{isPositive() ? '▲' : '▼'} {Math.abs(props.stat.change)}%
</p>
</div>
);
}
// Order status badge
function StatusBadge(props: { status: Order['status'] }) {
const config = () => ({
pending: { label: 'Pending', color: '#ffa726' },
processing: { label: 'Processing', color: '#42a5f5' },
done: { label: 'Done', color: '#66bb6a' },
cancelled: { label: 'Cancelled', color: '#ef5350' },
}[props.status]);
return (
<span style={`
background: ${config().color}20;
color: ${config().color};
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8rem;
`}>
{config().label}
</span>
);
}
// Main dashboard
function Dashboard() {
const [filterStatus, setFilterStatus] = createSignal<Order['status'] | 'all'>('all');
const [stats] = createResource(fetchStats);
const [orders] = createResource(fetchOrders);
const filteredOrders = createMemo(() => {
const f = filterStatus();
if (f === 'all') return orders() ?? [];
return (orders() ?? []).filter(o => o.status === f);
});
const totalAmount = createMemo(() =>
filteredOrders().reduce((sum, o) => sum + o.amount, 0)
);
return (
<div class="dashboard">
<header class="dashboard-header">
<h1>Dashboard</h1>
<p>March 21, 2026</p>
</header>
{/* Stats section */}
<section class="stats-section">
<ErrorBoundary fallback={(err) => <p>Failed to load stats: {err.message}</p>}>
<Suspense fallback={
<div class="stats-grid">
{[1,2,3,4].map(() => <div class="stat-card skeleton" />)}
</div>
}>
<div class="stats-grid">
<For each={stats()}>
{(stat) => <StatCard stat={stat} />}
</For>
</div>
</Suspense>
</ErrorBoundary>
</section>
{/* Orders section */}
<section class="orders-section">
<div class="section-header">
<h2>Orders</h2>
<div class="filter-buttons">
<For each={(['all', 'pending', 'processing', 'done', 'cancelled'] as const)}>
{(status) => (
<button
class={filterStatus() === status ? 'filter-btn active' : 'filter-btn'}
onClick={() => setFilterStatus(status)}
>
{status === 'all' ? 'All' :
status === 'pending' ? 'Pending' :
status === 'processing' ? 'Processing' :
status === 'done' ? 'Done' : 'Cancelled'}
</button>
)}
</For>
</div>
</div>
<ErrorBoundary fallback={(err) => <p>Failed to load orders: {err.message}</p>}>
<Suspense fallback={<p>Loading orders...</p>}>
<Show
when={filteredOrders().length > 0}
fallback={<p class="empty">No orders match this status.</p>}
>
<table class="orders-table">
<thead>
<tr>
<th>Order #</th>
<th>Customer</th>
<th>Amount</th>
<th>Status</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<For each={filteredOrders()}>
{(order) => (
<tr>
<td>#{order.id}</td>
<td>{order.customer}</td>
<td>{order.amount.toLocaleString()}</td>
<td><StatusBadge status={order.status} /></td>
<td>{order.date}</td>
</tr>
)}
</For>
</tbody>
<tfoot>
<tr>
<td colSpan={2}><strong>Total</strong></td>
<td><strong>{totalAmount().toLocaleString()}</strong></td>
<td colSpan={2} />
</tr>
</tfoot>
</table>
</Show>
</Suspense>
</ErrorBoundary>
</section>
</div>
);
}
export default Dashboard;
Practical Example: Dynamic List — Drag and Drop
import { createSignal, For } from 'solid-js';
interface Item {
id: number;
text: string;
color: string;
}
function DragDropList() {
const [items, setItems] = createSignal<Item[]>([
{ id: 1, text: 'First item', color: '#ffcdd2' },
{ id: 2, text: 'Second item', color: '#c8e6c9' },
{ id: 3, text: 'Third item', color: '#bbdefb' },
{ id: 4, text: 'Fourth item', color: '#fff9c4' },
{ id: 5, text: 'Fifth item', color: '#f3e5f5' },
]);
let dragIndex = -1;
const handleDragStart = (index: number) => {
dragIndex = index;
};
const handleDragOver = (e: DragEvent, index: number) => {
e.preventDefault();
if (dragIndex === index) return;
setItems(prev => {
const next = [...prev];
const [dragged] = next.splice(dragIndex, 1);
next.splice(index, 0, dragged);
dragIndex = index;
return next;
});
};
const handleDragEnd = () => {
dragIndex = -1;
};
return (
<div>
<h3>Drag to reorder</h3>
{/* For uses reference-based tracking to optimize DOM moves */}
<ul style="list-style: none; padding: 0; max-width: 300px;">
<For each={items()}>
{(item, index) => (
<li
draggable={true}
onDragStart={() => handleDragStart(index())}
onDragOver={(e) => handleDragOver(e, index())}
onDragEnd={handleDragEnd}
style={`
background: ${item.color};
padding: 1rem;
margin-bottom: 0.5rem;
border-radius: 8px;
cursor: grab;
user-select: none;
border: 2px solid transparent;
transition: border-color 0.2s;
`}
>
☰ {item.text}
</li>
)}
</For>
</ul>
</div>
);
}
export default DragDropList;
Practical Example: Error Handling Pattern
import { createSignal, createResource, ErrorBoundary, Suspense } from 'solid-js';
// API that may throw an error
async function fetchRiskyData(id: number) {
const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}: Request failed`);
const data = await res.json();
if (data.id > 10) throw new Error('Only IDs 10 and below are allowed.');
return data;
}
function RiskyDataView() {
const [id, setId] = createSignal(1);
const [data, { refetch }] = createResource(id, fetchRiskyData);
return (
<div>
<div>
<input
type="number"
value={id()}
onInput={(e) => setId(Number(e.target.value))}
min="1"
max="20"
/>
<button onClick={refetch}>Refresh</button>
</div>
<ErrorBoundary
fallback={(err, reset) => (
<div style="background: #fff3f3; border: 1px solid #f44336; padding: 1rem; border-radius: 8px; margin-top: 1rem;">
<h4 style="color: #f44336; margin: 0 0 0.5rem;">An error occurred</h4>
<p style="margin: 0 0 1rem;">{err.message}</p>
<div style="display: flex; gap: 0.5rem;">
<button onClick={reset}>Retry</button>
<button onClick={() => { setId(1); reset(); }}>Reset to Default</button>
</div>
</div>
)}
>
<Suspense fallback={<p>Loading data...</p>}>
<Show when={data()}>
<div style="margin-top: 1rem; padding: 1rem; border: 1px solid #ccc; border-radius: 8px;">
<p><strong>ID:</strong> {data()?.id}</p>
<p><strong>Title:</strong> {data()?.title}</p>
<p><strong>Completed:</strong> {data()?.completed ? '✅' : '❌'}</p>
</div>
</Show>
</Suspense>
</ErrorBoundary>
</div>
);
}
export default RiskyDataView;
Pro Tips
Tip 1: Handle Children Reactivity with the children Helper
import { children, type JSX } from 'solid-js';
interface AnimatedListProps {
children: JSX.Element | JSX.Element[];
}
function AnimatedList(props: AnimatedListProps) {
// Process children() like a memo — safely converts reactive children to an array
const resolved = children(() => props.children);
// resolved() always returns the latest array of child elements
return (
<ul>
<For each={resolved.toArray()}>
{(child) => (
<li style="animation: fadeIn 0.3s ease;">
{child}
</li>
)}
</For>
</ul>
);
}
Tip 2: Optimize Deeply Nested Show Components
// ❌ Too much nesting hurts readability
<Show when={isLoggedIn()}>
<Show when={isAdmin()}>
<Show when={hasPermission()}>
<AdminPanel />
</Show>
</Show>
</Show>
// ✅ Use createMemo to compose compound conditions
const canShowAdmin = createMemo(
() => isLoggedIn() && isAdmin() && hasPermission()
);
<Show when={canShowAdmin()}>
<AdminPanel />
</Show>
Tip 3: Optimizing Components Inside For
// Each item inside For has its own independent reactive scope
// Changing one item does not affect the others
<For each={items()}>
{(item) => (
// ✅ item is an immutable value — createSignal is not needed
// The entire block re-runs only when the item object itself is replaced
<ExpensiveItemComponent
id={item.id}
data={item.data}
/>
)}
</For>
Tip 4: Handling Empty Array Patterns
// Show + For combination instead of For's fallback
const hasItems = createMemo(() => items().length > 0);
<Show when={hasItems()} fallback={<EmptyState />}>
<For each={items()}>
{(item) => <Item data={item} />}
</For>
</Show>
// Or use For's fallback directly (more concise)
<For each={items()} fallback={<EmptyState />}>
{(item) => <Item data={item} />}
</For>
Tip 5: Optimizing Suspense Boundaries
// ❌ Wrapping everything in one Suspense waits for all to load
<Suspense fallback={<Loading />}>
<SlowComponent1 /> {/* 3 seconds */}
<SlowComponent2 /> {/* 1 second */}
<SlowComponent3 /> {/* 2 seconds */}
{/* Everything appears at once after 3 seconds */}
</Suspense>
// ✅ Individual Suspense boundaries show each component as soon as it's ready
<SlowComponent1 /> {/* Shows immediately if pre-loaded */}
<Suspense fallback={<Skeleton />}><SlowComponent2 /></Suspense> {/* After 1 second */}
<Suspense fallback={<Skeleton />}><SlowComponent1 /></Suspense> {/* After 3 seconds */}
<Suspense fallback={<Skeleton />}><SlowComponent3 /></Suspense> {/* After 2 seconds */}
Tip 6: Higher-Order Component Pattern with splitProps
import { splitProps, mergeProps } from 'solid-js';
import type { JSX } from 'solid-js';
// Wrapper component that passes through HTML attributes
interface CardProps extends JSX.HTMLAttributes<HTMLDivElement> {
title: string;
hoverable?: boolean;
}
function Card(props: CardProps) {
const [local, rest] = splitProps(
mergeProps({ hoverable: false }, props),
['title', 'hoverable', 'children', 'class']
);
return (
<div
{...rest}
class={`card ${local.hoverable ? 'card-hoverable' : ''} ${local.class ?? ''}`}
>
<div class="card-header">
<h3>{local.title}</h3>
</div>
<div class="card-body">
{local.children}
</div>
</div>
);
}
// Usage — all HTML attributes like onClick and style are forwarded
<Card
title="User Info"
hoverable
onClick={() => console.log('clicked')}
style="max-width: 400px;"
>
<p>Card content</p>
</Card>
Summary
| Component / Pattern | Purpose | Key Characteristics |
|---|---|---|
<Show> | Conditional rendering | when, fallback, render prop for type narrowing |
<For> | Array rendering | Reference-based tracking, DOM move optimization |
<Index> | Index-based rendering | Position-based tracking, item is a Signal |
<Switch>/<Match> | Multiple condition branching | Only the first truthy Match is rendered |
<ErrorBoundary> | Error handling | fallback(err, reset) signature |
<Suspense> | Async loading | Auto-integrates with createResource |
mergeProps | Setting default values | Reactive props merging |
splitProps | Splitting props | Separate component props from HTML element props |
| No destructuring | Preserve reactivity | Access props and signals directly |
In the next chapter, we will explore global state management with createStore and patterns for sharing data between components.