---
title: Update State
description: Actions that read and write client-side state — set values, toggle booleans, and manipulate lists, all without server calls.
icon: pen-to-square
---

import { ComponentPreview } from '/snippets/component-preview.mdx'

State is client-side data that your UI can read and write. Components display state values, actions change them, and updates are instant — no server round-trip needed.

## Setting Up State

Use `set_initial_state()` to define starting values. Any component in your UI can then read them through `.rx` or `Rx()` references:

<ComponentPreview json={{"view":{"cssClass":"gap-3","type":"Column","children":[{"name":"brightness","type":"Slider","min":0.0,"max":100.0,"disabled":false},{"content":"Brightness: {{ brightness }}%","type":"Muted"}]},"state":{"brightness":50}}} playground="ZnJvbSBwcmVmYWJfdWkuYXBwIGltcG9ydCBzZXRfaW5pdGlhbF9zdGF0ZQpmcm9tIHByZWZhYl91aS5jb21wb25lbnRzIGltcG9ydCBTbGlkZXIsIE11dGVkLCBDb2x1bW4KCnN0YXRlID0gc2V0X2luaXRpYWxfc3RhdGUoYnJpZ2h0bmVzcz01MCkKCndpdGggQ29sdW1uKGdhcD0zKToKICAgIFNsaWRlcihuYW1lPSJicmlnaHRuZXNzIiwgbWluPTAsIG1heD0xMDApCiAgICBNdXRlZChmIkJyaWdodG5lc3M6IHtzdGF0ZS5icmlnaHRuZXNzfSUiKQo=">
<CodeGroup>
```python Python icon="python"
from prefab_ui.app import set_initial_state
from prefab_ui.components import Slider, Muted, Column

state = set_initial_state(brightness=50)

with Column(gap=3):
    Slider(name="brightness", min=0, max=100)
    Muted(f"Brightness: {state.brightness}%")
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "cssClass": "gap-3",
    "type": "Column",
    "children": [
      {
        "name": "brightness",
        "type": "Slider",
        "min": 0.0,
        "max": 100.0,
        "disabled": false
      },
      {"content": "Brightness: {{ brightness }}%", "type": "Muted"}
    ]
  },
  "state": {"brightness": 50}
}
```
</CodeGroup>
</ComponentPreview>

Drag the slider — the text updates instantly. The Slider has `name="brightness"`, which means it automatically reads from and writes to the `brightness` state key. Most form controls (Input, Checkbox, Switch, Select, etc.) support this `name` prop for automatic state binding.

For MCP tools, you can also provide initial state through `AppResult.state`:

```python AppResult State
from prefab_ui import AppResult

@mcp.tool()
async def settings() -> AppResult:
    return AppResult(
        view=...,
        state={"volume": 75, "muted": False},
    )
```

## SetState

`SetState` explicitly sets a state key to a value. Both arguments are required — the key and the value to write:

<ComponentPreview json={{"view":{"cssClass":"gap-3","type":"Column","children":[{"cssClass":"gap-2","type":"Row","children":[{"type":"Button","label":"Improbability","variant":"outline","size":"default","disabled":false,"onClick":{"action":"setState","key":"drive","value":"improbability"}},{"type":"Button","label":"Conventional","variant":"outline","size":"default","disabled":false,"onClick":{"action":"setState","key":"drive","value":"conventional"}},{"type":"Button","label":"Bistromath","variant":"outline","size":"default","disabled":false,"onClick":{"action":"setState","key":"drive","value":"bistromath"}}]},{"type":"Badge","label":"Active drive: {{ drive }}","variant":"default"}]},"state":{"drive":"improbability"}}} playground=" ">
<CodeGroup>
```python Python icon="python"
from prefab_ui.app import set_initial_state
from prefab_ui.components import Badge, Button, Column, Row
from prefab_ui.actions import SetState

state = set_initial_state(drive="improbability")

with Column(gap=3):
    with Row(gap=2):
        Button("Improbability", variant="outline",
               on_click=SetState("drive", "improbability"))
        Button("Conventional", variant="outline",
               on_click=SetState("drive", "conventional"))
        Button("Bistromath", variant="outline",
               on_click=SetState("drive", "bistromath"))
    Badge(f"Active drive: {state.drive}")
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "cssClass": "gap-3",
    "type": "Column",
    "children": [
      {
        "cssClass": "gap-2",
        "type": "Row",
        "children": [
          {
            "type": "Button",
            "label": "Improbability",
            "variant": "outline",
            "size": "default",
            "disabled": false,
            "onClick": {"action": "setState", "key": "drive", "value": "improbability"}
          },
          {
            "type": "Button",
            "label": "Conventional",
            "variant": "outline",
            "size": "default",
            "disabled": false,
            "onClick": {"action": "setState", "key": "drive", "value": "conventional"}
          },
          {
            "type": "Button",
            "label": "Bistromath",
            "variant": "outline",
            "size": "default",
            "disabled": false,
            "onClick": {"action": "setState", "key": "drive", "value": "bistromath"}
          }
        ]
      },
      {"type": "Badge", "label": "Active drive: {{ drive }}", "variant": "default"}
    ]
  },
  "state": {"drive": "improbability"}
}
```
</CodeGroup>
</ComponentPreview>

### Dot-paths

State keys can contain dots to reach into nested objects. `profile.name` means "the `name` field inside the `profile` object." A purely numeric segment like `0` means an array index, so `todos.0.done` means "the `done` field of the first item in the `todos` array."

The `name` prop works with dot-paths too — type in the inputs below to see the card update:

<ComponentPreview json={{"view":{"cssClass":"gap-3","type":"Column","children":[{"type":"Card","children":[{"type":"CardHeader","children":[{"type":"CardTitle","content":"{{ profile.name }}"},{"type":"CardDescription","content":"{{ profile.title }}"}]}]},{"name":"profile.name","type":"Input","inputType":"text","placeholder":"Name","disabled":false,"readOnly":false,"required":false},{"name":"profile.title","type":"Input","inputType":"text","placeholder":"Title","disabled":false,"readOnly":false,"required":false}]},"state":{"profile":{"name":"Arthur Dent","title":"Sandwich Maker, Lamuella"}}}} playground=" ">
<CodeGroup>
```python Python icon="python"
from prefab_ui.app import set_initial_state
from prefab_ui.components import (
    Card, CardDescription, CardHeader, CardTitle,
    Column, Input,
)

state = set_initial_state(
    profile={
        "name": "Arthur Dent",
        "title": "Sandwich Maker, Lamuella",
    },
)

with Column(gap=3):
    with Card():
        with CardHeader():
            CardTitle(state.profile.name)
            CardDescription(state.profile.title)
    Input(name="profile.name", placeholder="Name")
    Input(name="profile.title", placeholder="Title")
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "cssClass": "gap-3",
    "type": "Column",
    "children": [
      {
        "type": "Card",
        "children": [
          {
            "type": "CardHeader",
            "children": [
              {"type": "CardTitle", "content": "{{ profile.name }}"},
              {"type": "CardDescription", "content": "{{ profile.title }}"}
            ]
          }
        ]
      },
      {
        "name": "profile.name",
        "type": "Input",
        "inputType": "text",
        "placeholder": "Name",
        "disabled": false,
        "readOnly": false,
        "required": false
      },
      {
        "name": "profile.title",
        "type": "Input",
        "inputType": "text",
        "placeholder": "Title",
        "disabled": false,
        "readOnly": false,
        "required": false
      }
    ]
  },
  "state": {"profile": {"name": "Arthur Dent", "title": "Sandwich Maker, Lamuella"}}
}
```
</CodeGroup>
</ComponentPreview>

Integer segments address array items. Inside a [ForEach](/components/for-each) loop, combine this with `{{ $index }}` to target the current row:

```python Array Paths
set_initial_state(
    todos=[
        {"text": "Find Magrathea", "done": False},
        {"text": "Have lunch", "done": True},
    ],
)

with ForEach("todos"):
    Checkbox(name="todos.{{ $index }}.done")
    Text("{{ $item.text }}")
```

`SetState("todos.0.done", True)` would also work for a hardcoded index — but `{{ $index }}` is how you target whichever row the user is interacting with.

<Warning>
If an intermediate path segment is missing or the wrong type (e.g. an integer segment pointing at a non-array), the update is a no-op with a console warning.
</Warning>

## ToggleState

`ToggleState` flips a boolean — `True` becomes `False`, `False` becomes `True`. Pair it with [If](/components/conditional) for show/hide patterns:

<ComponentPreview json={{"view":{"cssClass":"gap-2","type":"Column","children":[{"type":"Button","label":"Toggle Details","variant":"outline","size":"default","disabled":false,"onClick":{"action":"toggleState","key":"showDetails"}},{"type":"Condition","cases":[{"when":"{{ showDetails }}","children":[{"type":"Alert","variant":"default","children":[{"type":"AlertTitle","content":"Here are the details!"}]}]}]}]}}} playground=" ">
<CodeGroup>
```python Python icon="python"
from prefab_ui.components import Alert, AlertTitle, Button, Column
from prefab_ui.components.control_flow import If
from prefab_ui.actions import ToggleState
from prefab_ui.rx import Rx

showDetails = Rx("showDetails")

with Column(gap=2):
    Button("Toggle Details", variant="outline",
           on_click=ToggleState("showDetails"))
    with If(showDetails):
        with Alert():
            AlertTitle("Here are the details!")
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "cssClass": "gap-2",
    "type": "Column",
    "children": [
      {
        "type": "Button",
        "label": "Toggle Details",
        "variant": "outline",
        "size": "default",
        "disabled": false,
        "onClick": {"action": "toggleState", "key": "showDetails"}
      },
      {
        "type": "Condition",
        "cases": [
          {
            "when": "{{ showDetails }}",
            "children": [
              {
                "type": "Alert",
                "variant": "default",
                "children": [{"type": "AlertTitle", "content": "Here are the details!"}]
              }
            ]
          }
        ]
      }
    ]
  }
}
```
</CodeGroup>
</ComponentPreview>

## AppendState and PopState

`AppendState` adds an item to a state array. `PopState` removes one by index. Together with [ForEach](/components/for-each), they let you build dynamic lists entirely client-side.

Type a name and click Add to see it appear in the list. Click × to remove a row.

<ComponentPreview json={{"view":{"cssClass":"gap-3","type":"Column","children":[{"let":{"_loop_1":"{{ $item }}","_loop_1_idx":"{{ $index }}"},"type":"ForEach","key":"crew","children":[{"cssClass":"gap-2 items-center","type":"Row","children":[{"content":"{{ $item }}","type":"Text"},{"type":"Button","label":"\u00d7","variant":"ghost","size":"sm","disabled":false,"onClick":{"action":"popState","key":"crew","index":"{{ $index }}"}}]}]},{"cssClass":"gap-2","type":"Row","children":[{"name":"new_member","type":"Input","inputType":"text","placeholder":"Crew member name...","disabled":false,"readOnly":false,"required":false},{"type":"Button","label":"Add","variant":"default","size":"default","disabled":false,"onClick":[{"action":"appendState","key":"crew","value":"{{ new_member }}"},{"action":"setState","key":"new_member","value":""}]}]},{"content":"{{ crew | length }} crew members","type":"Muted"}]},"state":{"crew":["Arthur Dent","Ford Prefect","Trillian"],"new_member":""}}} playground=" ">
<CodeGroup>
```python Python icon="python"
from prefab_ui.app import set_initial_state
from prefab_ui.components import (
    Button, Column, ForEach, Input, Muted, Row, Text,
)
from prefab_ui.actions import AppendState, PopState, SetState

state = set_initial_state(
    crew=["Arthur Dent", "Ford Prefect", "Trillian"],
    new_member="",
)

with Column(gap=3):
    with ForEach("crew"):
        with Row(gap=2, align="center"):
            Text("{{ $item }}")
            Button(
                "×", variant="ghost", size="sm",
                on_click=PopState("crew", "{{ $index }}"),
            )
    with Row(gap=2):
        Input(name="new_member", placeholder="Crew member name...")
        Button("Add", on_click=[
            AppendState("crew", state.new_member),
            SetState("new_member", ""),
        ])
    Muted(f"{state.crew.length()} crew members")
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "cssClass": "gap-3",
    "type": "Column",
    "children": [
      {
        "let": {"_loop_1": "{{ $item }}", "_loop_1_idx": "{{ $index }}"},
        "type": "ForEach",
        "key": "crew",
        "children": [
          {
            "cssClass": "gap-2 items-center",
            "type": "Row",
            "children": [
              {"content": "{{ $item }}", "type": "Text"},
              {
                "type": "Button",
                "label": "\u00d7",
                "variant": "ghost",
                "size": "sm",
                "disabled": false,
                "onClick": {"action": "popState", "key": "crew", "index": "{{ $index }}"}
              }
            ]
          }
        ]
      },
      {
        "cssClass": "gap-2",
        "type": "Row",
        "children": [
          {
            "name": "new_member",
            "type": "Input",
            "inputType": "text",
            "placeholder": "Crew member name...",
            "disabled": false,
            "readOnly": false,
            "required": false
          },
          {
            "type": "Button",
            "label": "Add",
            "variant": "default",
            "size": "default",
            "disabled": false,
            "onClick": [
              {"action": "appendState", "key": "crew", "value": "{{ new_member }}"},
              {"action": "setState", "key": "new_member", "value": ""}
            ]
          }
        ]
      },
      {"content": "{{ crew | length }} crew members", "type": "Muted"}
    ]
  },
  "state": {"crew": ["Arthur Dent", "Ford Prefect", "Trillian"], "new_member": ""}
}
```
</CodeGroup>
</ComponentPreview>

The "Add" button chains two actions: `AppendState` pushes the input value onto the array, then `SetState` clears the input. Each × button uses `PopState` with `{{ $index }}` — a variable provided by `ForEach` that gives the current row's position.

`AppendState` can also insert at a specific position with `index=0` to prepend or negative indices to count from the end. If the key doesn't exist yet, it creates a new array automatically.

## Valid State Keys

State keys must be identifiers: letters, numbers, and underscores, starting with a letter or underscore (`volume`, `_count`, `item_2`). No hyphens (they conflict with expressions) and no periods (the dot is the path separator). These rules are validated in Python when you create the action.

## API Reference

<Card icon="code" title="SetState Parameters">
<ParamField body="key" type="str" required>
  State key or dot-path to set (e.g. `"count"` or `"todos.0.done"`).
</ParamField>

<ParamField body="value" type="Any" default='"{{ $event }}"'>
  Value to assign. Defaults to the value from the triggering interaction (slider position, input text, checkbox state, etc.).
</ParamField>
</Card>

<Card icon="code" title="ToggleState Parameters">
<ParamField body="key" type="str" required>
  State key or dot-path to toggle.
</ParamField>
</Card>

<Card icon="code" title="AppendState Parameters">
<ParamField body="key" type="str" required>
  State key or dot-path to the target array.
</ParamField>

<ParamField body="value" type="Any" default='"{{ $event }}"'>
  Value to append.
</ParamField>

<ParamField body="index" type="int | str | None" default="None">
  Insert position. `None` appends to end. Supports negative indices and template strings like `"{{ $index }}"`.
</ParamField>
</Card>

<Card icon="code" title="PopState Parameters">
<ParamField body="key" type="str" required>
  State key or dot-path to the target array.
</ParamField>

<ParamField body="index" type="int | str" required>
  Index to remove. Supports negative indices and template strings like `"{{ $index }}"`.
</ParamField>
</Card>

## Protocol Reference

```json SetState
{
  "action": "setState",
  "key": "string (required)",
  "value": "any (required)"
}
```

```json ToggleState
{
  "action": "toggleState",
  "key": "string (required)"
}
```

```json AppendState
{
  "action": "appendState",
  "key": "string (required)",
  "value": "any (required)",
  "index?": "number | string"
}
```

```json PopState
{
  "action": "popState",
  "key": "string (required)",
  "index": "number | string (required)"
}
```

For the complete protocol schema, see [SetState](/protocol/set-state), [ToggleState](/protocol/toggle-state), [AppendState](/protocol/append-state), [PopState](/protocol/pop-state).
