
# Derived state and conditions

Two questions come up while packing: how much is left, and what exactly is left. Both answers are derived from the items, so they are computed values rather than more state to keep in sync.

## Derived values

Update `src/store.ts`:

```ts
// The packing list's state, what follows from it, and every way it changes.
import { computed, signal } from "zerolith";

export interface Item
{
    id: number;
    name: string;
    packed: boolean;
}

export type Filter = "all" | "left" | "packed";

export const items = signal<Item[]>([
    { id: 1, name: "Passport", packed: true },
    { id: 2, name: "Phone charger", packed: false },
    { id: 3, name: "Rain jacket", packed: false }
], { name: "items" });
export const filter = signal<Filter>("all", { name: "filter" });

export const packedCount = computed(() =>
    items.get().filter((item) => item.packed).length);
export const shown = computed(() =>
{
    const which = filter.get();
    return which === "all"
        ? items.get()
        : items.get().filter((item) => item.packed === (which === "packed"));
});

export function toggle(id: number): void
{
    items.set(items.get().map((item) =>
        (item.id === id ? { ...item, packed: !item.packed } : item)));
}

export function add(name: string): void
{
    const id = Math.max(0, ...items.get().map((item) => item.id)) + 1;
    items.set([...items.get(), { id, name, packed: false }]);
}

export function remove(id: number): void
{
    items.set(items.get().filter((item) => item.id !== id));
}
```

`filter` is new state: which items to show. `packedCount` and `shown` are derived: they read `items` and `filter` and recompute when either changes. Nothing ever sets them directly, so they can never disagree with the items.

A computed value is lazy and cached. It recomputes only when something it read has changed and something reads it again, so `shown` costs nothing while no page shows it.

## The filter buttons

Create `src/components/filters.ts`:

```ts
// The buttons that choose which items the list shows.
import { type View, computed, html } from "zerolith";
import { type Filter, filter } from "../store.ts";

const choices: Array<{ value: Filter; label: string }> = [
    { value: "all", label: "All" },
    { value: "left", label: "Left to pack" },
    { value: "packed", label: "Packed" }
];

export function Filters(): View
{
    return html`
        <div role="group" aria-label="Show">
            ${choices.map(({ value, label }) => html`
                <button type="button"
                    aria-pressed=${computed(() => filter.get() === value)}
                    @click=${() => filter.set(value)}>${label}</button>`)}
        </div>`;
}
```

The three buttons come from an array with `map`, since the choices are fixed; `each` is for lists that change. `aria-pressed` tells assistive technology which button is on, and the computed value keeps it current: `true` and `false` become the attribute's text.

## Conditions

Update `src/components/packing.ts`:

```ts
// The packing list page: progress, the filters, the rows, and a note when
// the filter leaves nothing to show.
import { type View, computed, each, html, when } from "zerolith";
import { items, packedCount, shown } from "../store.ts";
import { AddItem } from "./add-item.ts";
import { Filters } from "./filters.ts";
import { ItemRow } from "./item.ts";

export function Packing(): View
{
    const total = computed(() => items.get().length);
    const empty = computed(() => shown.get().length === 0);
    return html`
        <h1>Packing list</h1>
        <p>${packedCount} of ${total} packed</p>
        ${Filters()}
        <ul>${each(shown, (item) => item.id, (item) => ItemRow({ item }))}</ul>
        ${when(empty, () => html`<p>Nothing to show here.</p>`)}
        ${AddItem()}`;
}
```

`each` now renders `shown` instead of `items`, so the rows follow the filter. `when` renders its view while the condition is true and removes it when the condition turns false; an optional third argument renders while it is false. Here it shows a note when the filter leaves no rows, such as choosing Left to pack once everything is packed.

> [!NOTE]
> `when` is zerolith's if and else, the place of a `? :` in React or `@if` and `@else` in Angular. Its condition is a signal, so `empty` is a computed value; a function there fails with `when_condition_function`, since it would always count as true.

## What you learned

- Derived values are computed, never set, so they cannot drift from their sources.
- Computed values are lazy and cached.
- `when` shows a view while a condition holds; `each` is for lists that change, `map` for fixed ones.

