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";
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: 8, paddingHorizontal: 12, paddingVertical: 8 } as const;
return (
<View style={{ flex: 1, backgroundColor: "#f6f8fa", padding: 20, paddingTop: 24, gap: 12 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: "#1f2328" }}>if / each</Text>
<View style={{ flexDirection: "row", gap: 8 }}>
<View onPress={add} activeOpacity={0.6} style={{ ...button, backgroundColor: "#1f883d" }}>
<Text style={{ color: "#ffffff" }}>Add</Text>
</View>
<View onPress={() => showDone.set(!showDone())} activeOpacity={0.6} style={{ ...button, backgroundColor: "#ddf4ff" }}>
<Text style={{ color: "#0969da" }}>{showDone() ? "Hide done" : "Show done"}</Text>
</View>
</View>
<If
when={visible().length > 0}
else={<Text style={{ color: "#6e7781", textAlign: "center", marginTop: 40 }}>Nothing to show.</Text>}
>
<View style={{ gap: 8 }}>
<Each of={visible()} key={(t: Todo) => t.id}>
{(todo, index) => (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 12,
backgroundColor: "#ffffff",
borderRadius: 10,
padding: 12,
borderWidth: 1,
borderColor: "#d0d7de",
}}
>
<View
onPress={() => toggle(todo().id)}
hitSlop={8}
style={{
width: 22,
height: 22,
borderRadius: 11,
borderWidth: 2,
borderColor: "#1f883d",
backgroundColor: todo().done ? "#1f883d" : "transparent",
}}
/>
<Text
style={{
flex: 1,
color: todo().done ? "#8c959f" : "#1f2328",
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: "#cf222e" }}>Remove</Text>
</View>
</If>
</View>
)}
</Each>
</View>
</If>
<Text style={{ fontSize: 12, color: "#6e7781" }}>
{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.