Skip to main content

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.

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.
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:
// 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:
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 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.
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 fieldsstore.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 settersstore.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 setterstore.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 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:
// 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.
KeyTypeNotes
colorScheme'light' | 'dark'System preference. Overridable per subtree via .environment('colorScheme', ...).
displayScalenumberDevice pixel ratio (logical to physical).
localestringBCP-47 locale identifier ('en-US', 'fr-FR').
layoutDirection'leftToRight' | 'rightToLeft'Effective layout direction.
openURL(url, callback?) => voidOpens a URL via the host’s URL handler. Replace per subtree with OpenURLAction.
Recommended — present when the platform exposes the underlying signal. Always read with ?? or a guard.
KeyTypeNotes
platformstring'web', 'iOS', 'Android', etc.
screen{ width, height }Logical pixel size of the rendering surface.
dynamicTypeSizestringThe 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.
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(...), 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.
UseWhen the value isExamples
useStateLocal to one componentHover, toggle, draft text, transient animation
useStoreShared between components by keyFilter state, cart, multi-step form
.environment + useEnvironmentContext for a subtree, set by an ancestorTheme, 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:
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, with the full picture of how the lookup and the environment cooperate.
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.

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.