BindJS gives you three layers of state, each with a clear scope.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.
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.
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:
.id(...) so reordering or insertion does not reassign state to the wrong child:
.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 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.
- Flattened fields —
store.countandstore.labelread 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 acountfield gets asetCount, alabelgets asetLabel. - 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.
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 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:
- 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 ?? 10rather thanenv.margindirectly — 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. |
?? 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(...). |
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.
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 viauseEnvironment(). They’re surfaced at the layout level, through .ignoresSafeArea(...), GeometryReader, and .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 sameuseEnvironment() value can drive whole-component branching, not only modifier values. A common pattern is to check an environment flag and return a different tree:
What to read next
Hooks
An index of every hook with one-paragraph use cases.
Composition and slots
How environment-aware components fit into a layout.
Talking to the MCP host
The host bridge
useMCPHost() returns when rendering inside an MCP host.useEnvironment
The hook reference, with examples.