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

# Manage State

> Decide what the model sees

Between [tool](/build/tools) calls, the model is blind: it doesn't watch the user's interactions with the UI. Yet, to answer the user's next message, the model often needs to know what's on screen. State is how the [view](/build/view) closes that gap, and the design decision is always the same: what should the model see, what shouldn't, and when.

Here's a complete view, the `carousel` mounted by a tool call to `search-products`, holding shared state, hidden state, and a narration for the model:

```tsx views/carousel.tsx theme={null}
import { useState } from "react";

import { useViewState } from "skybridge/web";
import { useCallTool, useToolInfo } from "../helpers.js"; // generated, type-safe from server schema

export default function Carousel() {
  const { output, responseMetadata } = useToolInfo<"search-products">();
  const { callTool: checkout } = useCallTool("create-checkout");

  const [cart, setCart] = useViewState<{ ids: string[] }>({ ids: [] }); // shared with the model, survives remounts
  const [hovered, setHovered] = useState<string | null>(null); // invisible to the model, gone on remount

  return (
    <div data-llm={cart.ids.length ? `Cart holds ${cart.ids.length} items` : "Cart is empty"}>
      <ProductGrid
        products={output.products}
        images={responseMetadata.images}
        cart={cart.ids}
        highlighted={hovered}
        onHover={setHovered}
        onAdd={(id) => setCart({ ids: [...cart.ids, id] })}
        onCheckout={() => checkout({ productIds: cart.ids })}
      />
    </div>
  );
}
```

The sections below cover the decision itself, sharing and persisting state with [`useViewState`](/api-reference/use-view-state), and narrating the screen with [`data-llm`](/api-reference/data-llm).

## Decide Who Sees What

Every piece of state belongs to one of two shared channels, plus plain React for everything the model has no business knowing:

| Use Case                                                 | API                                                      | Model sees it | Survives remount |
| -------------------------------------------------------- | -------------------------------------------------------- | ------------- | ---------------- |
| Data the conversation builds on (cart, selection, draft) | [`useViewState`](/api-reference/use-view-state)          | Yes           | Yes              |
| A plain-words description of what's on screen            | [`data-llm`](/api-reference/data-llm)                    | Yes           | Recomputed       |
| Anything neither the model nor the next session needs    | [`useState`](https://react.dev/reference/react/useState) | No            | No               |

The model doesn't watch the view live: it reads state when the conversation comes back to it, meaning on the next user message. Models can't write the state directly. Each view instance owns its state; models typically see one instance per view, whichever had its state most recently updated.

<Info>
  **ChatGPT** pushes state updates to the model context depending on the display mode: updates from a PiP or fullscreen view are always pushed; updates from an inline view are pushed only while the instance has just mounted and no conversation turn has happened since.
</Info>

## Persist with `useViewState`

[`useViewState`](/api-reference/use-view-state) is `useState` with two superpowers:

* the host persists it: a closed and reopened conversation remounts the view instances with their previous state
* the model can read it: on the next conversation turn

```tsx views/carousel.tsx highlight={3,10,18,21-22} theme={null}
import { useState } from "react";

import { useViewState } from "skybridge/web";
import { useCallTool, useToolInfo } from "../helpers.js"; // generated, type-safe from server schema

export default function Carousel() {
  const { output, responseMetadata } = useToolInfo<"search-products">();
  const { callTool: checkout } = useCallTool("create-checkout");

  const [cart, setCart] = useViewState<{ ids: string[] }>({ ids: [] });
  const [hovered, setHovered] = useState<string | null>(null);

  return (
    <div data-llm={cart.ids.length ? `Cart holds ${cart.ids.length} items` : "Cart is empty"}>
      <ProductGrid
        products={output.products}
        images={responseMetadata.images}
        cart={cart.ids}
        highlighted={hovered}
        onHover={setHovered}
        onAdd={(id) => setCart({ ids: [...cart.ids, id] })}
        onCheckout={() => checkout({ productIds: cart.ids })}
      />
    </div>
  );
}
```

`cart` is shared because the conversation needs it: the model reads it to recommend a matching item or answer "what's in my cart?". `hovered` stays in plain `useState` because a hover ends before the user's next message: by the time the model could read it, the value is stale.

## Narrate the Screen with `data-llm`

While [`useViewState`](/api-reference/use-view-state) pushes structured data to the model context, [`data-llm`](/api-reference/data-llm) describes what the user currently sees in natural language. It's recomputed on every render and synced to the model:

```tsx views/carousel.tsx highlight={14} theme={null}
import { useState } from "react";

import { useViewState } from "skybridge/web";
import { useCallTool, useToolInfo } from "../helpers.js"; // generated, type-safe from server schema

export default function Carousel() {
  const { output, responseMetadata } = useToolInfo<"search-products">();
  const { callTool: checkout } = useCallTool("create-checkout");

  const [cart, setCart] = useViewState<{ ids: string[] }>({ ids: [] });
  const [hovered, setHovered] = useState<string | null>(null);

  return (
    <div data-llm={cart.ids.length ? `Cart holds ${cart.ids.length} items` : "Cart is empty"}>
      <ProductGrid
        products={output.products}
        images={responseMetadata.images}
        cart={cart.ids}
        highlighted={hovered}
        onHover={setHovered}
        onAdd={(id) => setCart({ ids: [...cart.ids, id] })}
        onCheckout={() => checkout({ productIds: cart.ids })}
      />
    </div>
  );
}
```

This is what lets the user speak in references: "what do you think of this one?". The model resolves *this one* from the `data-llm` narration. Nest `data-llm` on inner elements and the model receives an indented outline of the screen.

<Card title="All done!" type="tip" href="/build/auth" horizontal icon="check">
  You now know how to manage state. Learn who's behind every tool call in the next chapter.
</Card>

## Go Further

<Columns cols={3}>
  <Card title="Register Tools" icon="wrench" href="/build/tools">
    Define what humans and agents can do
  </Card>

  <Card title="Create Views" icon="palette" href="/build/view">
    Craft interactive UIs rendered in conversation
  </Card>

  <Card title="Authenticate Users" icon="key" href="/build/auth">
    Know who's behind every tool call
  </Card>
</Columns>
