> ## 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.

The default must be an object; a primitive default throws. To store a primitive, wrap it yourself: `useStore("flag", { value: 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 — defined for every conforming runtime.** They're observable from the first render and update reactively when the underlying value changes. Of these five, iOS injects all of them, the web renderers inject `colorScheme` and `openURL`, and Android (bindjs-android 0.0.33) injects only `openURL`, so read the others with 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` | Locale identifier. iOS reports it in `en_US` form. |
| `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` | Set only by the web renderer, to `'web'`. |
| `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
const [expanded, setExpanded] = useState(false)

return Button(expanded ? "Show less" : "Show more", () => {
    if (reduceMotion) {
        setExpanded(!expanded)
    } else {
        withAnimation(Spring(), () => setExpanded(!expanded))
    }
})
```

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) : Empty()
    }

    return VStack({ spacing: 0 }, [
        props.asset ? Image(props.asset.image) : Empty(),
        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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.