# Model and update

> State as plain data, one pure reducer per message, commands returned as data, and the framework messages Gyral sends you.

Source: https://gyral.dev/docs/update/

The **model** is your component's state. **Update** is how it changes: a record of reducers,
one per message tag, each a pure function from the current state and a message to the next
state. Nothing else changes state, so every change is visible to tests and to
[devtools](https://gyral.dev/docs/devtools/).

```ts
// src/todo.ts
import { define, each, html, intentsOf, type TemplateResult } from '@gyral/core';

export interface Todo {
  readonly id: number;
  readonly text: string;
  readonly done: boolean;
}

export interface State {
  readonly todos: readonly Todo[];
  readonly nextId: number;
}

export type Msg =
  | { readonly _tag: 'Add'; readonly text: string }
  | { readonly _tag: 'Toggle'; readonly id: number }
  | { readonly _tag: 'ClearDone' };

const i = intentsOf<typeof TodoList>();

const Item = (t: Todo): TemplateResult =>
  html`<li>
    <button type="button" value=${t.id} aria-pressed=${t.done} data-intent=${i.Toggle}>
      ${t.text}
    </button>
  </li>`;

export const TodoList = define<State, Msg>()('my-todo-list', {
  init: () => ({ todos: [], nextId: 1 }),
  intent: {
    Add: ({ formData }) => {
      const text = formData?.get('text');
      return typeof text === 'string' && text.trim() !== ''
        ? { _tag: 'Add', text: text.trim() }
        : undefined;
    },
    Toggle: ({ value }) => ({ _tag: 'Toggle', id: Number(value) }),
    ClearDone: () => ({ _tag: 'ClearDone' }),
  },
  update: {
    Add: (s, m) => ({
      todos: [...s.todos, { id: s.nextId, text: m.text, done: false }],
      nextId: s.nextId + 1,
    }),
    Toggle: (s, m) => ({
      ...s,
      todos: s.todos.map((t) => (t.id === m.id ? { ...t, done: !t.done } : t)),
    }),
    ClearDone: (s) => ({ ...s, todos: s.todos.filter((t) => !t.done) }),
  },
  view: (s, i) => html`
    <form data-intent=${i.Add}>
      <label for="text">New todo</label>
      <input id="text" name="text" required />
      <button>Add</button>
    </form>
    <ul>
      ${each(s.todos, (t) => t.id, Item)}
    </ul>
    <button type="button" data-intent=${i.ClearDone}>Clear done</button>
  `,
});
```

## State is plain data

Keep state a record of JSON values: strings, numbers, booleans, `null`, arrays and plain
objects. No `Date`, `Map` or class instances. Three things depend on it:

- **Server rendering** writes each component's state into the page as JSON, and the browser
  resumes from it. A `Date` would come back as a string. In development, the server warns when
  a seed won't survive the trip.
- **Devtools** and tests can print and compare state.
- **Reducers return new objects.** State is never mutated, so "did it change?" is a reference
  check.

Model what can happen with unions rather than flags. `{ _tag: 'Loading' } | { _tag: 'Found';
repos } | { _tag: 'Failed'; message }` can't be loading and failed at once; two booleans can.

The module's `i` is `intentsOf<typeof TodoList>()`: the same typed intent names the view
receives, as a module constant, so the list's `Item` row can name an intent and stay a pure
function of its todo ([Lists](https://gyral.dev/docs/views/#lists)). The row's return type is written out, and
the view uses its own `i`, so the row and the component don't infer each other's types. `aria-pressed=${t.done}` writes `"true"` or `"false"`, the way
ARIA expects.

## One reducer per message

`update` is keyed by message tag, and its type requires a reducer for every tag in `Msg`. Add
a message and the compiler shows you every component that has to handle it.

A reducer receives the state, the message (narrowed to its variant, so `m.id` is typed) and the
context, `{ props, read }`: the component's [props](https://gyral.dev/docs/components/#props) and a reader for
[shared stores](https://gyral.dev/docs/stores/). Reducers are pure: no `fetch`, no `Date.now()`, no
`Math.random()`, no DOM.

A reducer written outside the spec, to share it or test it alone, is typed with `Reducer` and
`Variant` from `@gyral/core` (type-only exports). `Variant<Msg, 'Add'>` is the
member of `Msg` tagged `'Add'`:

```ts
// src/shelf.ts
import { define, html, type Reducer, type Variant } from '@gyral/core';

interface State {
  readonly books: readonly string[];
}
type Msg = { readonly _tag: 'Add'; readonly title: string } | { readonly _tag: 'Clear' };

export const add: Reducer<State, Msg, Variant<Msg, 'Add'>> = (s, m) => ({
  books: [...s.books, m.title],
});

export const Shelf = define<State, Msg>()('my-shelf', {
  init: () => ({ books: [] }),
  intent: { Add: ({ value }) => ({ _tag: 'Add', title: value ?? '' }), Clear: true },
  update: { Add: add, Clear: () => ({ books: [] }) },
  view: (s, i) => html`
    <button type="button" value="Middlemarch" data-intent=${i.Add}>Add Middlemarch</button>
    <button type="button" data-intent=${i.Clear}>Clear</button>
    <output>${s.books.join(', ')}</output>
  `,
});
```

## Returning commands

To do anything outside the model, a reducer returns a tuple: the next state and a list of
**commands**. A command is a description of a side effect, run by a driver. The answer comes
back as another message.

```ts
// src/save-button.ts
import { define, html } from '@gyral/core';
import { delay } from '@gyral/time';

export interface State {
  readonly saving: boolean;
}

export type Msg = { readonly _tag: 'Save' } | { readonly _tag: 'Saved' };

export const SaveButton = define<State, Msg>()('my-save-button', {
  init: () => ({ saving: false }),
  intent: { Save: true },
  update: {
    // State plus a command: answer `Saved` after half a second.
    Save: (s) => (s.saving ? s : [{ saving: true }, [delay<Msg>(500, { _tag: 'Saved' })]]),
    Saved: () => ({ saving: false }),
  },
  view: (s, i) => html`
    <button type="button" data-intent=${i.Save} ?disabled=${s.saving}>
      ${s.saving ? 'Saving…' : 'Save'}
    </button>
  `,
});
```

The return type is `Next<State, Msg>`: either a state, or `[state, commands]`. State is never an
array, so the two can't be confused. `init` can return commands too, to load data when the
component appears. `Save: true` is the short form of a parser that only sends its tag; it works
for messages with no other fields. [Effects and drivers](https://gyral.dev/docs/effects/) covers commands in depth.

## Subscriptions

Work that should keep running while some condition holds (a clock, polling, a socket, a store or
media-query watch) doesn't start in a reducer. `subscriptions(state, props)` returns the
commands that should be running now, and Gyral keeps the running set in step with it:

```ts
// src/clock.ts
import { define, html } from '@gyral/core';
import { every } from '@gyral/time';

export interface State {
  readonly running: boolean;
  readonly now: number;
}

export type Msg = { readonly _tag: 'Toggle' } | { readonly _tag: 'Tick'; readonly now: number };

export const Clock = define<State, Msg>()('my-clock', {
  init: () => ({ running: false, now: 0 }),
  // While running, tick every second; pausing stops the timer, resuming starts it again.
  subscriptions: (s) => (s.running ? [every(1000, (now): Msg => ({ _tag: 'Tick', now }))] : []),
  intent: { Toggle: true },
  update: {
    Toggle: (s) => ({ ...s, running: !s.running }),
    Tick: (s, m) => ({ ...s, now: m.now }),
  },
  view: (s, i) => html`
    <button type="button" data-intent=${i.Toggle}>${s.running ? 'Pause' : 'Start'}</button>
    <p><time>${s.now === 0 ? '—' : new Date(s.now).toLocaleTimeString()}</time></p>
  `,
});
```

After the first render and after every update and props change, Gyral compares the list with
what is running, lane by lane (a command's `key`, else its driver's name): a new lane starts, a
missing lane is stopped, a lane whose input changed restarts, and an unchanged lane keeps
running. Inputs are compared as data: functions inside them are ignored, so an inline selector
such as `(s) => s.unread` doesn't restart a lane on every update. To restart when a selector
changes, put what it depends on in the key, for example one that includes the room id.
Moving the element keeps subscriptions running, removing it stops them, and attaching it again
starts them from the current state. So there is no start in `init`, no restart on
`PropsChanged` and no reconnect handling. `every(ms, (now) => msg)` sends the time at once and
then every `ms`. In model tests, `subscriptionsFor(Clock.spec, state, props)` returns the list.

## Framework messages

Gyral sends five messages of its own, plus three to
[form-associated components](https://gyral.dev/docs/form-associated/) (`FormReset`, `FormDisabled` and
`FormStateRestore`). Their reducers are optional: leave one out and the message changes nothing.

| Message          | Sent when                                                                                                                                |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `PropsChanged`   | A declared prop changed after the first render. Has `props` and `prev`. [Props](https://gyral.dev/docs/components/#reacting-to-prop-changes)              |
| `IntentRejected` | Input failed a schema in `form()` or `field()`, or the server rejected a form. [Forms](https://gyral.dev/docs/forms/)                                     |
| `StoreChanged`   | A store the component reads changed. [Shared state](https://gyral.dev/docs/stores/)                                                                       |
| `Hydrated`       | The component is live in the browser, once, after its first render. Has `serverRendered`.                                                |
| `Errored`        | An update, parser or command of this component failed. Has `phase` and `error` (a `GyralError`). [Error handling](https://gyral.dev/docs/error-handling/) |

`Hydrated` is the hook for progressive enhancement: render the no-JavaScript version on the
server and in the first client render (so hydration matches), then switch to the enhanced UI
in the `Hydrated` reducer.

## Starting from messages

Every Gyral element has an `initialMessages` property. Messages listed there run through
`update` right after `init`, before the first render, on the server too. The server uses it to
render a form with the errors from a rejected submission through the component's own
`IntentRejected` reducer, so errors look the same with or without JavaScript.

## State in CSS and in transitions

Two optional spec fields read the model, so the model stays the one source of truth:

- **`states: (s) => ({ loading: s.results._tag === 'Loading' })`** mirrors boolean facts onto the
  element's [custom states](https://gyral.dev/docs/styling/#custom-states), for CSS:
  `:host(:state(loading))`.
- **`viewTransition: (prev, next, msg) => boolean`** renders a state change inside
  `document.startViewTransition`, for route changes and list reorders. It is skipped where the
  browser doesn't support it, or when the user prefers reduced motion.
