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.

el-control-flow.tsx Edit in Playground Open in new tab
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).

PropTypeDescription
whenunknownThe condition. Only its truthiness matters.
elseJSX.ElementRendered while when is falsy.
childrenJSX.ElementRendered 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.

PropTypeDescription
ofreadonly any[] | null | undefinedThe items. null and undefined render nothing.
key(item: any) => unknownThe identity of an item. Default: the item itself (objects by identity, primitives by value).
children(item: () => any, index: () => number) => JSX.ElementRenders 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 a key, 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.