Lifecycle States

What a generated component looks like when its data is old or incomplete, there's nothing to show, its offer has ended, a newer version replaced it, or the call failed.

Example: Lifecycle States

Pull last week's ad performance again.

Active ads, last 7 days

Sorts by this column. Alt with the left or right arrow moves the column.

Summer sale — carouselProspecting$1,8400.620.8%Active
Lookalike — video 15sProspecting$1,2600.811.1%Active
Brand story — staticAwareness$9400.940.6%Active
New arrivals — reelsProspecting$7201.421.9%Active
Free shipping — staticRetargeting$4102.102.4%Learning

3 selected

Generated UI stays in the conversation long after it was useful. A hotel card from yesterday still shows yesterday's prices, and a table pulled this morning may already be wrong. The Renderer handles these states once, for every component, so no component has to.

The states

StateWhenWhat the user seesCan they act?
streamingThe agent is still generating propsThe component's own loading stateNo (components disable actions)
readyNormalThe componentYes
emptyThe tool returned nothing (empty: "No overdue invoices")An empty state with that sentence, plus Check again when onRefresh is setRefresh only
partialSome data is missing (partial: "1 of 4 accounts didn't respond")A notice above the componentYes
failedThe tool call errored (status: "failed")A short error card with the reason, plus Try again when onRetry is setRetry only
cancelledThe user stopped the reply while this call was still streaming (status: "cancelled")"Stopped before it finished. Nothing from this part was used." plus Try again when onRetry is setRetry only
stalefetchedAt is older than staleAfterMs"Data from 3 hours ago. It may have changed." plus Refresh when onRefresh is setYes
expiredexpiresAt has passed"This expired 2 hours ago. Ask for an update before acting on it." The component is dimmed and inertNo
supersededA newer call replaced it (supersededBy)"Replaced by a newer version below." with a link to the latest. Dimmed and inertNo

A call that already has a result (the user acted) is a finished record: it never shows as stale, expired or superseded.

Usage

<GenUIRenderer
  call={{
    id: part.toolCallId,
    name: "book_hotel",
    props: part.input,
    status: "ready",
    fetchedAt: offer.quotedAt,      // when the prices were fetched
    staleAfterMs: 15 * 60_000,      // show "may have changed" after 15 min
    expiresAt: offer.validUntil,    // turn the card off when the quote ends
  }}
  catalog={catalog}
  onResult={sendToAgent}
  onRefresh={(call) => askAgent(`Refresh ${call.name}`)}
/>

When the user presses Stop, a tool call that was mid-stream never finishes: with the AI SDK its part stays input-streaming forever, and a half-drawn component would sit in the conversation. Pass the calls through markCancelled after you stop the stream, and every call that was still streaming shows "Stopped" instead. Finished calls are left alone.

import { markCancelled } from "@/components/genui/genui-renderer"

const calls = stopped ? markCancelled(allCalls) : allCalls

Mark replaced cards with markSuperseded, per tool, so two different tables in one reply don't replace each other:

const calls = markSuperseded(allCalls, { names: ["book_hotel", "propose_changes"] })

With the AI SDK, fromAISDK maps an output-error tool part to status: "failed" and copies its errorText into error.

Rules for hosts and agents

  • Set expiresAt on anything bookable or priced: hotel and car offers, cart prices, seats, showtimes. An expired offer must not be actionable.
  • Set fetchedAt and staleAfterMs on live data: balances, stock quotes, flight status, tables pulled from a tool.
  • Supersede instead of stacking: when the agent revises a proposal, the old card turns inert and links to the new one.
  • Empty is an answer, not an error: say what was checked ("No overdue invoices. Everything due this month is paid.") instead of rendering an empty table.
  • Partial results say what's missing so the user doesn't mistake them for the full picture.
  • Failed calls say what happened in plain words and whether anything changed ("nothing was changed").

Accessibility

  • The notice is a role="status" line above the component; the failure card is role="alert".
  • Expired and superseded components use inert, so they can't be focused or clicked by mistake; the notice explains why.
  • Time-based states start after mount (the server renders the plain component), so server and client markup match.

On this page