> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metabind.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# State and environment

> How BindJS components manage local state, share state across the tree, and pass context down through environment values

BindJS gives you three layers of state, each with a clear scope. `useState` keeps a single component's local state. `useStore` shares state across components by key. `.environment(key, value)` and `useEnvironment()` pass context down a subtree without threading props through every level. The three compose: a component can hold local state, read shared state from a store, and adapt its output to environment values its parents have set.

This page explains how each layer works, how to choose between them, and how environment values flow through a render. The per-hook reference pages cover the exact signatures.

## Component-local state with `useState`

`useState` is the right choice when the value belongs to one component and no other component cares about it: a toggle, a draft text field, a hover flag, a transient animation target.

```typescript theme={null}
const body = (props, children) => {
    const [count, setCount] = useState(0)

    return VStack({ spacing: 16 }, [
        Text(`Count: ${count}`).font("headline"),
        Button("Increment", () => setCount(count + 1)),
    ])
}
```

The runtime keys state by the component's deterministic position in the tree, so the same component instance receives the same value across re-renders. Each instance has its own state — two `Counter()` calls in a parent's body keep independent counts.

Hooks must run unconditionally on every render. Always declare `useState` at the top of the body and gate the use, not the call:

```typescript theme={null}
// Wrong — conditional hook call corrupts state across renders
if (props.editable) {
    const [draft, setDraft] = useState("")
}

// Right — hook always runs; condition gates how the value is used
const [draft, setDraft] = useState("")
if (props.editable) {
    // render the editor
}
```

For dynamic lists, give each item a stable `.id(...)` so reordering or insertion does not reassign state to the wrong child:

```typescript theme={null}
ForEach(items, (item) =>
    ProductCard({ name: item.name }).id(item.id)
)
```

Without `.id(...)`, the runtime sees "the third child changed" when the list reorders and reassigns state to the wrong row. The id must be stable across renders for the same logical item — a database id, UUID, or slug works; an array index does not.

See [`useState`](/bindjs/functions/useState) for the full reference.

## Shared state with `useStore`

`useStore` is the right choice when several components need to read or write the same value. Filter state shared between a sidebar and a results pane, a cart that several screens read, a draft form passed between steps — anything where prop drilling would be painful or where the value outlives any one component.

```typescript theme={null}
const body = (props, children) => {
    const store = useStore("counter", { count: 0, label: "clicks" })

    return VStack({ spacing: 12 }, [
        Text(`${store.count} ${store.label}`),
        Button("Add", () => store.setCount(store.count + 1)),
        Button("Rename", () => store.setLabel("taps")),
    ])
}
```

A store has three things:

* **Flattened fields** — `store.count` and `store.label` read the current values. The runtime tracks which fields the body touched and re-renders the body when those fields change.
* **Per-field setters** — `store.setCount(5)` updates one field. Setters are auto-generated from the default object's keys, so a `count` field gets a `setCount`, a `label` gets a `setLabel`.
* **A full-state setter** — `store.set(prev => ({ ...prev, count: prev.count + 1 }))` replaces the whole state in one call, which is what you want when several fields move together.

Pass a `scope` argument to namespace a store — for example, per content instance, per route, or per modal — so two unrelated trees do not collide on the same key.

For primitive defaults the store wraps the value as `{ value: T }`. `useStore("flag", false)` produces a store with `store.value` (boolean) and `store.setValue(next)`.

See [`useStore`](/bindjs/functions/useStore) for the full reference.

## Environment values

Environment values pass context down the component tree without threading props through every intermediate component. A parent sets values with `.environment(key, value)`; descendants read them with `useEnvironment()`. The runtime layers values like a stack — descendants see the deepest value for each key.

A parent and a child reading the same environment in one example:

```typescript theme={null}
// Parent — set values for the subtree
export default defineComponent({
    body: (props, children) =>
        VStack(children)
            .environment("margin", 20)
            .environment("colorScheme", "dark")
            .environment("preview", "thumbnail"),
})

// Child — read them
export default defineComponent({
    properties: { title: PropertyString({ title: "Title" }) },
    body: (props, children) => {
        const env = useEnvironment()
        const margin = env.margin ?? 10
        const isDark = env.colorScheme === "dark"

        return Text(props.title)
            .padding("horizontal", margin)
            .foregroundStyle(isDark ? Color("white") : Color("black"))
    },
})
```

A few details about how environment scoping works:

* **Set values are visible only to the modified subtree.** After the subtree finishes rendering, the runtime restores the prior environment for siblings and ancestors. Values do not leak upward or sideways.
* **Deeper values win.** A descendant `useEnvironment()` call observes the merged environment as of the deepest enclosing `.environment(key, value)` for each key.
* **Always provide a fallback when reading.** Use `env.margin ?? 10` rather than `env.margin` directly — the key may not be set in every render context.

### What the runtime injects

The runtime injects a base set of environment keys before any component code runs. They fall into three groups.

**Core — every conforming runtime injects these.** They're observable from the first render and update reactively when the underlying value changes. These five are the only keys you can rely on without a fallback.

| Key               | Type                             | Notes                                                                                                                |
| ----------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `colorScheme`     | `'light' \| 'dark'`              | System preference. Overridable per subtree via `.environment('colorScheme', ...)`.                                   |
| `displayScale`    | `number`                         | Device pixel ratio (logical to physical).                                                                            |
| `locale`          | `string`                         | BCP-47 locale identifier (`'en-US'`, `'fr-FR'`).                                                                     |
| `layoutDirection` | `'leftToRight' \| 'rightToLeft'` | Effective layout direction.                                                                                          |
| `openURL`         | `(url, callback?) => void`       | Opens a URL via the host's URL handler. Replace per subtree with [`OpenURLAction`](/bindjs/functions/OpenURLAction). |

**Recommended — present when the platform exposes the underlying signal.** Always read with `??` or a guard.

| Key                 | Type                | Notes                                                             |
| ------------------- | ------------------- | ----------------------------------------------------------------- |
| `platform`          | `string`            | `'web'`, `'iOS'`, `'Android'`, etc.                               |
| `screen`            | `{ width, height }` | Logical pixel size of the rendering surface.                      |
| `dynamicTypeSize`   | `string`            | The user's text-size preference.                                  |
| `systemColorScheme` | `'light' \| 'dark'` | The system's preference, never overridden by `.environment(...)`. |

**Platform extensions — scoped to a single platform, explicitly non-portable.** A component that reads `accessibility.reduceMotion` is iOS-only by definition; wrap the read with a fallback path or scope the component in its `metadata.description`.

iOS-only keys include `colorSchemeContrast`, `pixelLength`, `contentSizeCategory`, the size classes (`horizontalSizeClass` / `verticalSizeClass`), `isEnabled`, `redactionReasons`, `scenePhase`, `timeZone`, `calendar`, `now`, and the accessibility family (`differentiateWithoutColor`, `reduceMotion`, `reduceTransparency`, `invertColors`, `showButtonShapes`, `voiceOverEnabled`, `switchControlEnabled`). Web has no platform-extension keys today beyond the recommended set.

```typescript theme={null}
const env = useEnvironment()
const reduceMotion = env.accessibility?.reduceMotion ?? false

return SomeView()
    .animation(reduceMotion ? null : Spring())
```

The shape works for any platform extension: read with optional chaining, fall back to a sensible default, and the component degrades cleanly on platforms that don't expose the signal.

### Host-injected custom keys

Beyond the spec keys, the host application can inject any additional keys it wants — organization or project identifiers, secrets, base URLs, feature flags. The propagation rules are the same as for built-in keys: scoped to subtrees, overridable per `.environment(...)` call. If your component depends on a host-injected key, document the dependency in `metadata.description` so editors and any LLM consumer can reason about it.

### Safe-area insets

Safe-area insets aren't exposed via `useEnvironment()`. They're surfaced at the layout level, through [`.ignoresSafeArea(...)`](/bindjs/modifiers/ignoresSafeArea), [`GeometryReader`](/bindjs/components/GeometryReader), and [`.safeAreaInset(...)`](/bindjs/modifiers/safeAreaInset). Web doesn't have a safe-area concept; iOS and Android both support `GeometryReader`-based access.

## Choosing between them

The three layers handle different scopes. Match the scope of the data to the layer.

| Use                               | When the value is                         | Examples                                       |
| --------------------------------- | ----------------------------------------- | ---------------------------------------------- |
| `useState`                        | Local to one component                    | Hover, toggle, draft text, transient animation |
| `useStore`                        | Shared between components by key          | Filter state, cart, multi-step form            |
| `.environment` + `useEnvironment` | Context for a subtree, set by an ancestor | Theme, locale, layout density, preview mode    |

`useState` and `useStore` hold values authors mutate. `useEnvironment` reads ambient context an ancestor or the host has set — it is read-only from the descendant's side.

These compose freely. A component can hold local UI state with `useState`, read a shared cart from `useStore`, and decide its layout based on `env.preview` from `useEnvironment`. The layers do not conflict.

## Reading environment for conditional rendering

The same `useEnvironment()` value can drive whole-component branching, not only modifier values. A common pattern is to check an environment flag and return a different tree:

```typescript theme={null}
const body = (props, children) => {
    const env = useEnvironment()

    if (env.gallery) {
        return props.asset ? Image(props.asset.image) : AssetPlaceholder()
    }

    return VStack({ spacing: 0 }, [
        props.asset ? Image(props.asset.image) : AssetPlaceholder(),
        props.caption ? Text(props.caption) : Empty(),
    ])
}
```

This pattern is covered in more depth in [Composition](/bindjs/authoring/composition), with the full picture of how the lookup and the environment cooperate.

<Tip>
  Use environment for context that an ancestor decides on behalf of every descendant — color scheme, locale, layout role. Use `useStore` for state several siblings collaborate on. If a value is set by a parent and only one child reads it, a regular prop is still the simplest choice.
</Tip>

## What to read next

<CardGroup cols={2}>
  <Card title="Hooks" icon="circle-nodes" href="/bindjs/authoring/hooks">
    An index of every hook with one-paragraph use cases.
  </Card>

  <Card title="Composition and slots" icon="layer-group" href="/bindjs/authoring/composition">
    How environment-aware components fit into a layout.
  </Card>

  <Card title="Talking to the MCP host" icon="messages" href="/bindjs/authoring/mcp-host">
    The host bridge `useMCPHost()` returns when rendering inside an MCP host.
  </Card>

  <Card title="useEnvironment" icon="circle-info" href="/bindjs/functions/useEnvironment">
    The hook reference, with examples.
  </Card>
</CardGroup>
