# React hooks

> @s3nd/react: hooks for sending a file or a snapshot, reading a code back, and a sync code input that repairs what the user typed. Never sees a storage credential, never pulls the AWS SDK into your bundle.

Canonical: https://www.s3nd.sh/react · Markdown: https://www.s3nd.sh/react.md · Français: https://www.s3nd.sh/fr/react

Send a file or a snapshot, read a code back, and an input that repairs the code as the user types it. Its whole dependency tree is the protocol package and nanoid, with React as a peer. The AWS SDK stays on your server.

```sh
npm install @s3nd/react
```
**point it at the transfer routes**
```tsx
import { S3ndProvider } from '@s3nd/react'

export default function Providers({ children }) {
  return <S3ndProvider baseUrl="/api/transfers">{children}</S3ndProvider>
}
```

## A file input, a code.
sendFile() takes a File straight off an input, keeping its name and type. Failures land in error rather than rejecting, because an event handler should not need a try/catch.

```tsx
import { useSendTransfer } from '@s3nd/react'

function DropFile() {
  const { sendFile, transfer, isPending, error } = useSendTransfer()

  return (
    <>
      <input
        type="file"
        disabled={isPending}
        onChange={(event) => event.target.files?.[0] && sendFile(event.target.files[0])}
      />
      {transfer && <p>Read this out to them: {transfer.code}</p>}
      {error && <p>{error.message}</p>}
    </>
  )
}
```

The hook posts to the transfer routes on your server, which hold the bucket credentials. The browser never sees a key, and your `authorize` function decides who may send.

`transfer` carries the code, the kind, the size and the expiry. Show the code grouped in fours; the receiving side accepts it with or without the spaces.

## Look it up, show it, then download.
load() fetches what a code holds without moving the bytes, so the user sees a filename and a size before anything is downloaded. loadBytes() brings the file across.

```tsx
import { useReceiveTransfer, useSyncCodeInput } from '@s3nd/react'

function PickUp() {
  const input = useSyncCodeInput()
  const { load, loadBytes, transfer, notFound, isPending } = useReceiveTransfer()

  async function download() {
    const bytes = await loadBytes(input.code!)
    if (bytes) saveToDisk(new Blob([bytes]), transfer?.filename ?? 'file') // your helper
  }

  return (
    <>
      <input {...input.inputProps} placeholder="K7QP 2M4X" />
      <button onClick={() => load(input.code!)} disabled={!input.isComplete || isPending}>
        Look it up
      </button>
      {notFound && <p>Unknown or expired code.</p>}
      {transfer?.kind === 'file' && (
        <button onClick={download}>
          Download {transfer.filename} · {transfer.size} bytes
        </button>
      )}
    </>
  )
}
```

## What the user typed stays untouched.
useSyncCodeInput does the repair in the browser, before any request. Rewriting the field under the cursor is the one thing that makes these inputs miserable, so it never does.

`value` is verbatim. `code` is the canonical form to submit, `null` while what is typed cannot be one. `isComplete` is the moment to enable the button.

`inputProps` carries the keyboard and autofill hints a one-time code wants: `autoComplete="one-time-code"`, capitals, no autocorrect, and a numeric keyboard when the alphabet is digits.

Pass the same shape your server configured, `{ length: 4, alphabet }`, and both halves follow.

## The same hooks carry a snapshot.
Structured state goes through send() as a snapshot, and comes back inline in data. Loading and applying are deliberately separate: only your code knows its object stores, and the user should see what is about to replace their data.

```ts
const { send, transfer } = useSendTransfer()

// Structured state goes as a snapshot, with your schema version.
await send(await exportDatabase(), { version: 3 })

// On the other device: load, show, then apply.
const { load, transfer, data } = useReceiveTransfer<DatabaseDump>()
await load(code)
// transfer.device, transfer.createdAt → show them
// importDatabase(data!) → only after the user confirms
```

The IndexedDB example in the repository has a complete export and import pair against a real object store, and the use-case page walks the whole flow.

## A user hammering a button gets one answer.
Every call aborts the one before it, a late reply from a superseded call is dropped rather than published, and nothing is written after unmount.

- **Client hooks, App Router ready** — Every export is a client hook and the build carries `'use client'`, so it drops straight into the Next.js App Router. React 18 or later.
- **Tokens and custom clients** — Pass `headers` to the provider for a token, or `client` to bring your own, which is also how you drive it in tests with no network at all.
- **Status you can render** — `status` is idle, pending, success or error, and `notFound` covers both an unknown and an expired code, the way the protocol does.

- `useSendTransfer()`: send, sendFile, transfer, status, isPending, error, reset
- `useReceiveTransfer()`: load, loadBytes, burn, transfer, data, notFound, status, isPending, error, reset
- `useSyncCodeInput()`: value, setValue, code, isComplete, error, reset, inputProps
- `useTransferClient()`: the underlying client, for anything the hooks do not cover
