# React hooks

URL: /code/sdk/react · Id: `page:code/sdk/react`

> startup-builder/react binds the typed client to React, with live reads, Verbs that follow their Action to the end, Events and Experiment arms.

`startup-builder/react` binds `$` to React components, with the same types and [Permissions](/business/nouns/permission). Its hooks read records and stay live, run [Verbs](/business/nouns/verb) and follow each [Action](/business/nouns/action) to its end, and hear [Events](/business/nouns/event).

## Set up
The hooks ship in the `startup-builder` package. Mount one provider at the root of the client tree:

```tsx
'use client'

import { StartupProvider } from 'startup-builder/react'

export function Providers({ children }: { children: React.ReactNode }) {
  return <StartupProvider publishableKey={process.env.NEXT_PUBLIC_STARTUP_BUILDER_KEY}>{children}</StartupProvider>
}
```

A publishable key, `pk_test_` or `pk_live_`, runs only the Verbs a [Startup](/business/nouns/startup) marks public and reads only published records. Once a person signs in through the `SignIn` [Component](/business/nouns/component), every hook acts as that person, inside their [Tenant](/business/nouns/tenant).

A Startup's own [App](/business/nouns/app) mounts the provider for you. A secret key never reaches the browser; [the key guide](/code/api/guides/keys) lists which key runs where.

## The hooks
Each hook takes an SDK method itself, such as `$.Invoice.list`, so its input and result are typed from the Verb.

| Call | Returns | Runs |
| --- | --- | --- |
| `useRead(method, input)` | `{ data, error, loading }`, kept live | `get` or any read |
| `useList(method, input)` | `{ data, more, after, loading }`, kept live | `list`, one page at a time |
| `useVerb(method)` | `{ run, preview, action, pending, error }` | Any Verb, in [Mode](/business/nouns/mode) `send` |
| `useAction(id)` | The Action, kept live | `Action.get` |
| `useEvent(name, handler)` | Nothing | A stream of Events |
| `useVariant(flag, options)` | The arm's key | `Flag.evaluate` |
| `useStartup()` | The `$` the provider holds | Anything `$` runs |

## Read records
`leaseline.app` tracks lease renewals for small property managers at $29 a month. Its billing page lists a manager's [Invoices](/business/nouns/invoice):

```tsx
'use client'

import { $ } from 'startup-builder'
import { useList } from 'startup-builder/react'

export function Invoices() {
  const { data, more, loading } = useList($.Invoice.list, { limit: 20 })
  if (loading) return <p>Loading invoices</p>

  return (
    <ul>
      {data.map((invoice) => <li key={invoice.$id}>{invoice.number}: {invoice.status}</li>)}
      {more && <button onClick={more}>Show more</button>}
    </ul>
  )
}
```

Reads stay live. When `Invoice.collected` fires, the row re-renders as paid with no refetch and no polling code. Components that read the same record share one request and one subscription.

A manager without the Permission to list Invoices gets `error`, the [forbidden](/code/api/errors/forbidden) problem, and no rows.

`more` loads the next page with the `after` cursor and appends it, so the list keeps its order as records arrive.

## Run a Verb
`useVerb` returns `run`, which sends the Verb and follows its Action until it ends:

```tsx
'use client'

import { $ } from 'startup-builder'
import { useVerb } from 'startup-builder/react'

export function SwitchToAnnual({ subscription }: { subscription: string }) {
  const { run, pending, action, error } = useVerb($.Subscription.switch)

  return (
    <>
      <button disabled={pending} onClick={() => run({ subscription, offer: 'offer_annual' })}>Switch to annual</button>
      {action?.reason === 'awaitingAction' && <p>Waiting for approval</p>}
      {error && <p>{error.fix}</p>}
    </>
  )
}
```

`pending` stays true while the Action is `pending`, whatever it waits on: a queue, an answer, or an [Approval](/business/nouns/approval) past a [Limit](/business/nouns/limit). The `ActionStatus` Component renders every reason the same way the platform's own screens do.

An Action that ends `failed` or `refused` sets `error` to its problem, with `fix` saying what to change. `preview(input)` runs the same Verb in Mode `try`: every check runs and nothing is stored. Each `run` carries its own idempotency key, so a double click sends one call.

## Hear Events
`useEvent` subscribes while the component is mounted and unsubscribes when it unmounts:

```tsx
useEvent('Invoice.collected', (event) => notify(`Paid: ${event.data.invoice.number}`))
```

The stream reconnects from the last sequence number it saw, so a laptop that sleeps misses nothing. [Realtime](/code/sdk/guides/realtime) covers selectors and cursors.

## Experiment arms
`useVariant` returns the arm of an [Experiment](/business/nouns/experiment) the reader is assigned, the same arm on every visit:

```tsx
const arm = useVariant('annual-first', { default: 'control' })

return <h2>{arm === 'annual' ? 'Two months free, billed yearly' : 'Every lease renewal, tracked'}</h2>
```

The unit is the signed-in person, or the [Visitor](/business/nouns/visitor) before sign-in. The [Exposure](/business/nouns/exposure) is recorded on the render that shows the arm, never on a prefetch. The [experiments file](/code/file-conventions/experiments) declares the arms and the [Metric](/business/nouns/metric) that judges them.

## Server and client
Server code calls `$` directly and passes what it read to the client. A hook given `initial` renders that value at once and stays live from there:

```tsx
// app/billing/page.tsx, a server component
import { $ } from 'startup-builder'
import { Plan } from './plan'

export default async function Billing({ params }: { params: { subscription: string } }) {
  const subscription = await $.Subscription.get({ id: params.subscription })
  return <Plan initial={subscription} />
}
```

```tsx
// app/billing/plan.tsx, a client component
'use client'

import { $, type Subscription } from 'startup-builder'
import { useRead } from 'startup-builder/react'

export function Plan({ initial }: { initial: Subscription }) {
  const { data } = useRead($.Subscription.get, { id: initial.$id }, { initial })
  return <p>Renews {data.renewsAt}</p>
}
```

| Code runs in | Uses | Acts as |
| --- | --- | --- |
| A server component, a route handler or a script | `$` | The secret key's [Worker](/business/nouns/worker) |
| A Startup's own [Function](/business/nouns/function) | `ctx.$` | The Function's Worker, on the Action that called it |
| A client component | The hooks | The signed-in person, or the Visitor |