Elements
If / Each
<If> and <Each> are the components for conditional and repeated content. They create no native view; they decide which
elements exist. Both are compiled into calls to the native renderer, which updates only what changed: <If> swaps a
branch when the condition’s truthiness changes, <Each> adds, moves and removes rows by key instead of rebuilding the
list.
import { mount, signal, Text, View } from "nsh";
import { ui } from "./palette";
interface Todo {
id: number;
title: string;
done: boolean;
}
let nextId = 4;
function App() {
const todos = signal<Todo[]>([
{ id: 1, title: "Read the elements page", done: true },
{ id: 2, title: "Try <If> and <Each>", done: false },
{ id: 3, title: "Ship the app", done: false },
]);
const showDone = signal(true);
const visible = () => (showDone() ? todos() : todos().filter((t) => !t.done));
const toggle = (id: number) => todos.update((list) => list.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
const add = () => todos.update((list) => [...list, { id: nextId++, title: `Task ${nextId - 1}`, done: false }]);
const remove = (id: number) => todos.update((list) => list.filter((t) => t.id !== id));
const button = { borderRadius: 10, paddingHorizontal: 12, paddingVertical: 8 } as const;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 12 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>if / each</Text>
<View style={{ flexDirection: "row", gap: 8 }}>
<View
onPress={add}
activeOpacity={0.6}
style={{ ...ui().accentFill, ...button }}
>
<Text style={{ color: ui().onAccent, fontWeight: "600" }}>Add</Text>
</View>
<View
onPress={() => showDone.set(!showDone())}
activeOpacity={0.6}
style={{ ...ui().raised, ...button }}
>
<Text style={{ color: ui().text }}>{showDone() ? "Hide done" : "Show done"}</Text>
</View>
</View>
<If
when={visible().length > 0}
else={<Text style={{ color: ui().muted, textAlign: "center", marginTop: 40 }}>Nothing to show.</Text>}
>
<View style={{ gap: 8 }}>
<Each of={visible()} key={(t: Todo) => t.id}>
{(todo, index) => (
<View
style={{ ...ui().row, flexDirection: "row", alignItems: "center", gap: 12, borderRadius: 12, padding: 12 }}
>
<View
onPress={() => toggle(todo().id)}
hitSlop={8}
style={{
width: 22,
height: 22,
borderRadius: 11,
borderWidth: 2,
borderColor: ui().accent,
backgroundColor: todo().done ? ui().accent : "transparent",
}}
/>
<Text
style={{
flex: 1,
color: todo().done ? ui().faint : ui().text,
textDecorationLine: todo().done ? "line-through" : "none",
}}
>
{index() + 1}. {todo().title}
</Text>
<If when={todo().done}>
<View onPress={() => remove(todo().id)} hitSlop={8}>
<Text style={{ color: ui().down }}>Remove</Text>
</View>
</If>
</View>
)}
</Each>
</View>
</If>
<Text style={{ fontSize: 12, color: ui().muted, fontVariant: ["tabular-nums"] }}>
{todos().filter((t) => t.done).length} of {todos().length} done
</Text>
</View>
);
}
mount(App);<View>
<If when={user()} else={<Text>Signed out</Text>}>
<Text>Hello, {user()!.name}</Text>
</If>
<Each of={todos()}>{(todo) => <Text>{todo().title}</Text>}</Each>
</View>
They are globals: they need no import (a binding of the same name in scope takes their place). Inside JSX expressions you can still use plain JavaScript (cond ? a : b, items.map(…)), but those
rebuild their elements every time what they read changes; <If> and <Each> keep the elements that stay.
#<If>
Renders its children while when is truthy, else the else element (or nothing).
| Prop | Type | Description |
|---|---|---|
when | unknown | The condition. Only its truthiness matters. |
else | JSX.Element | Rendered while when is falsy. |
children | JSX.Element | Rendered while when is truthy. |
The branch is created again only when the truthiness of when changes. A when that goes from one truthy value to
another (user() changing from one object to another) keeps the branch, and the expressions inside it update on their
own.
const loading = signal(true);
<If when={!loading()} else={<Text style={{ color: "#6e7781" }}>Loading…</Text>}>
<Profile />
</If>;
#<Each>
Renders children(item, index) for each item of of.
| Prop | Type | Description |
|---|---|---|
of | readonly any[] | null | undefined | The items. null and undefined render nothing. |
key | (item: any) => unknown | The identity of an item. Default: the item itself (objects by identity, primitives by value). |
children | (item: () => any, index: () => number) => JSX.Element | Renders one row. |
The row function gets accessors, not values: call item() and index(). When of changes, rows whose key is still
there are kept (and moved if their position changed); their item() and index() update, and only the expressions
reading them run again. New keys get new rows; rows whose key is gone are removed.
interface Todo {
id: number;
title: string;
done: boolean;
}
const todos = signal<Todo[]>([]);
<Each of={todos()} key={(t: Todo) => t.id}>
{(todo, index) => (
<View style={{ flexDirection: "row", gap: 8 }}>
<Text>{index() + 1}.</Text>
<Text style={{ textDecorationLine: todo().done ? "line-through" : "none" }}>{todo().title}</Text>
</View>
)}
</Each>;
#Choosing a key
- Items replaced by new objects on every update (immutable updates,
{ ...t, done: true }) need akey, usually an id. Without it each new object is a new row and its elements are created again. - Arrays of strings or numbers work without a key: they are compared by value.
- Each row has its own owner: effects and cleanups created in a row end when the row is removed.
#<Each> or a list
<Each> creates every row. That is right for tens or a few hundred items, in a <View> or a <Scroll>.
For long or unbounded data, use a list: it creates only the rows near the viewport and recycles them.
See JSX and components and reactivity for how expressions update.