# React

> A provider and two hooks for React 19. One shared identification per page, server-rendering safe, with the result as plain state.

Last updated: 2026-09-17

`@fingerly/react` wraps the [JavaScript SDK](https://docs.fingerly.io/docs/sdks/javascript) in a provider and hooks. However many components ask, a page makes one identification and every component reads the same answer.

## Requirements

- React 19 or newer.
- A [public key](https://docs.fingerly.io/docs/api-keys) with your site in its allowed origins.

> **Note:** Building with Next.js? Use [`@fingerly/next`](https://docs.fingerly.io/docs/sdks/nextjs), which adds a Server Component friendly provider and helpers for Server Actions.

## Install

```bash npm
npm install @fingerly/react
```

```bash pnpm
pnpm add @fingerly/react
```

```bash yarn
yarn add @fingerly/react
```

## Add the provider

```tsx main.tsx
import { createRoot } from 'react-dom/client'
import { FingerlyProvider } from '@fingerly/react'

createRoot(document.getElementById('root')!).render(
  <FingerlyProvider apiKey="fly_pk_us_production_…">
    <App />
  </FingerlyProvider>,
)
```

The provider collects nothing when it mounts. It accepts `apiKey`, `endpoint`, `endpoints`, `fallbackToDefaultEndpoint`, `submit` and `consent`, with the meanings the [JavaScript agent](https://docs.fingerly.io/reference/javascript-agent#load) gives them. Changing `consent` takes effect without remounting, and `setConsent()` is also on the store from `useFingerly()`.

## Identify on an action

```tsx Checkout.tsx
import { useIdentify } from '@fingerly/react'

export function Checkout({ orderId }: { orderId: string }) {
  const { identify, isLoading, error } = useIdentify({ tag: () => 'checkout:' + orderId })

  async function onSubmit() {
    const { requestId } = await identify()
    await submitOrder({ orderId, requestId })
  }

  return (
    <>
      <button onClick={onSubmit} disabled={isLoading}>
        {isLoading ? 'Checking…' : 'Pay now'}
      </button>
      {error && <p>{error.message}</p>}
    </>
  )
}
```

## Identify on mount

```tsx RiskBanner.tsx
const { suspectLevel, isReady } = useIdentify({ immediate: true })
```

`immediate` runs in an effect, so it never runs during server rendering. `suspend: true` suspends the component instead, for use inside `<Suspense>`.

## useIdentify

**Options**

- `immediate` (boolean, default `false`): Identify when the component mounts.
- `suspend` (boolean, default `false`): Suspend during render until the identification finishes.
- `tag` (string | () => string | undefined): The tag to send. A function is read at the moment of submission. A `tag` passed to `identify()` overrides it.

**Returns**

- `requestId` (string | null): Identifies this identification. Send it to your server with the action it protects.
- `visitorId` (string | null): The stable identifier the server resolved for this browser.
- `visitorIsNew` (boolean): Whether your organization is seeing this visitor for the first time.
- `visitorConfidence` (number): How sure the identification is, from 0 to 100.
- `identifiable` (boolean): `false` when the browser gave too little to identify anyone.
- `duplicate` (boolean): `true` when the server had already answered this request.
- `state` (string | null): `enriched`, or `unavailable` when the network lookup could not run.
- `suspectScore` (number | null): The server's weighted score. `null` when nothing was scored, which is not the same as `0`.
- `suspectLevel` (string | null): `low`, `medium` or `high`.
- `triggers` (IdentifyTrigger[]): The signals the server scored, heaviest first.
- `verdicts` (Verdicts | null): The local, advisory [client-side verdicts](https://docs.fingerly.io/docs/client-verdicts).
- `error` (Error | null): The last failure, if the identification failed.
- `isLoading` (boolean): An identification is in flight.
- `isReady` (boolean): The identification has finished, successfully or not.
- `isSuspicious` (boolean): Any local verdict matched at `medium` confidence or above.
- `identify` ((options?: { tag?: string; force?: boolean }) => Promise<IdentifyResult>): Identify, or return the page's existing identification. `force` collects afresh.
- `refresh` ((options?: { tag?: string }) => Promise<IdentifyResult>): Identify again, ignoring the shared result.

`identify` and `refresh` are stable across renders. A failed identification is forgotten, so the next call tries again.

## useVerdict

Reads one local verdict, with a confidence floor. It never triggers identification itself.

```tsx AutomationNotice.tsx
import { useVerdict } from '@fingerly/react'

function AutomationNotice() {
  const { matched, confidence } = useVerdict('automation', { min: 'high' })
  if (!matched) return null
  return <aside>Automated access suspected ({confidence})</aside>
}
```

Returns `matched`, `verdict`, `confidence` and `reasons`. `min` defaults to `'medium'`. Verdict names are `incognito`, `shields`, `tor`, `emulator`, `automation` and `farm`.

> **Warning:** Local verdicts are advisory and can be edited by whoever controls the browser. Make decisions on your server with the [stored event](https://docs.fingerly.io/docs/server-side-verification).

## Server rendering

- On the server the hooks return the unidentified state, so hydration always matches.
- Calling `identify()` during server rendering rejects with `FingerlyServerError`. Call it from an effect or an event handler.
- For a server that renders many requests, create a store per request with `createFingerlyStore(options)` and pass it as `<FingerlyProvider store={store}>`.

## Errors

Failures are stored in `error` and rethrown from `identify()`. `TransportError` is re-exported with its `status` and `retryable` fields. Using a hook outside a provider throws an error that says where to add one.
