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

# React Integration

> Using Crosslink in React applications

`@crosslink/react` provides thin bindings over `@crosslink/sdk-browser`. The hooks add no state machine of their own -- the client already owns pairing, reconnection, and transport selection; the hooks just subscribe to it with `useSyncExternalStore`, so a connection change is reflected on the same render tick.

## Setup

```bash theme={null}
npm install @crosslink/sdk-browser @crosslink/react
```

## Provider

Wrap your app in `CrosslinkProvider`. It creates a client from `options` (or accepts an existing `client`) and exposes connection state through context.

```jsx theme={null}
import { CrosslinkProvider } from "@crosslink/react";

function App() {
  return (
    <CrosslinkProvider
      options={{ deviceName: "React App" }}
      onAuthorized={(rpc, client) => console.log("Connected, RPC ready")}
      onUnauthorized={(state) => console.log("Not authorized:", state)}
    >
      <RemoteControl />
    </CrosslinkProvider>
  );
}
```

## Hooks

### `useCrosslink()`

Returns the full context value: `{ client, state, rpc, pairedApps, connected }`.

```jsx theme={null}
import { useCrosslink } from "@crosslink/react";

function StatusBar() {
  const { state, connected } = useCrosslink();
  return <p>{connected ? "Connected" : `State: ${state}`}</p>;
}
```

### `useCrosslinkState()`

Just the current `ConnectionState` (`"offline"`, `"discovering"`, `"pairing"`, `"connecting"`, `"direct"`, `"turn-relayed"`, `"crosslink-relayed"`, `"reconnecting"`, `"unauthorized"`, `"revoked"`, `"protocol-incompatible"`).

```jsx theme={null}
import { useCrosslinkState } from "@crosslink/react";

function StateLabel() {
  const state = useCrosslinkState();
  return <span>{state}</span>;
}
```

### `useCrosslinkRpc()`

The `RpcClient` while a connected state exists, or `null` otherwise.

```jsx theme={null}
import { useCrosslinkRpc } from "@crosslink/react";

function DataViewer() {
  const rpc = useCrosslinkRpc();
  const [data, setData] = useState(null);

  const fetchData = async () => {
    if (!rpc) return;
    setData(await rpc.call("data.get"));
  };

  return (
    <div>
      <button onClick={fetchData} disabled={!rpc}>Refresh</button>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}
```

### `useCrosslinkCall()`

Calls a method and tracks `pending`/`error`. A call started before the connection is up throws immediately rather than queueing -- the client already queues idempotent calls across a reconnect, and a second queue here would reorder them.

```jsx theme={null}
import { useCrosslinkCall } from "@crosslink/react";

function NextTrackButton() {
  const { call, pending, error } = useCrosslinkCall();
  return (
    <button onClick={() => call("app.next")} disabled={pending}>
      {pending ? "..." : "Next Track"}
    </button>
  );
}
```

### `useCrosslinkEvent(eventName, onEvent)`

Subscribes to a host event for as long as the component is mounted and a connection exists. Resubscribes automatically after a reconnect.

```jsx theme={null}
import { useCrosslinkEvent } from "@crosslink/react";

function EventLog() {
  const [events, setEvents] = useState([]);
  useCrosslinkEvent("data.changed", (payload) => {
    setEvents((prev) => [...prev, { time: Date.now(), payload }]);
  });

  return (
    <ul>
      {events.map((e, i) => <li key={i}>{JSON.stringify(e.payload)}</li>)}
    </ul>
  );
}
```

### `useCrosslinkClientState(client)`

Subscribes to a specific client's state without needing a `CrosslinkProvider`. Useful outside the provider tree, e.g. during pairing.

```jsx theme={null}
import { useCrosslinkClientState } from "@crosslink/react";

function StandaloneStatus({ client }) {
  const state = useCrosslinkClientState(client);
  return <p>State: {state}</p>;
}
```

### `isConnected(state)`

A plain helper -- `true` for `"direct"`, `"crosslink-relayed"`, and `"turn-relayed"`.

```jsx theme={null}
import { isConnected } from "@crosslink/react";

isConnected("direct"); // true
isConnected("connecting"); // false
```

## Pairing

You do not build a pairing screen. `@crosslink/react` mounts the same pairing
card every Crosslink application shows:

```jsx theme={null}
import { CrosslinkPairingCard } from "@crosslink/react";

export function Settings() {
  return (
    <CrosslinkPairingCard
      source
      onDeviceConnected={(deviceId) => console.log("paired", deviceId)}
    />
  );
}
```

`source` points it at the Crosslink system endpoints your host exposes. The card
mints the pairing session, renders the QR and the nine-digit code, refreshes both
before they expire, mints a new one when a device redeems the old one, and lists
and revokes paired devices — no state, effects or fetches of your own. Your
name, icon and colours come from the host's `application` block; pass
`appName`, `appIcon` or `brand` only to override one of them here.

The component is a thin mount around `createPairingCard`; it is the same
implementation a Vue or plain-JS page gets, not a React reimplementation. See
[Pairing Card](/client/pairing-card) for the full option list.

### On the phone

A React mobile app does not run a pairing flow either. Crosslink's mobile
bootstrap does that and hands you a connected channel:

```jsx theme={null}
import { useEffect, useState } from "react";

export function App() {
  const [rpc, setRpc] = useState(null);

  useEffect(() => {
    // `crosslink` is published by the boot script Crosslink injects.
    const off = window.crosslink.onConnected(setRpc);
    const offDown = window.crosslink.onDisconnected(() => setRpc(null));
    return () => {
      off();
      offDown();
    };
  }, []);

  if (!rpc) return null; // Crosslink is showing its own connection screen
  return <Notes rpc={rpc} />;
}
```

Returning `null` is correct: while there is no channel, Crosslink is rendering
its pairing, install, offline or revoked screen over the page.

## Error handling

```jsx theme={null}
function CrosslinkApp() {
  const [error, setError] = useState(null);

  // The pairing card renders its own errors; this is for RPC calls your app
  // makes after the connection exists.
  const call = async (rpc, method) => {
    try {
      await rpc.call(method);
    } catch (err) {
      if (err.message.includes("fingerprint")) {
        setError("Security warning: host fingerprint mismatch");
      } else if (err.message.includes("PAIRING_FAILED")) {
        setError("Pairing rejected by host");
      } else {
        setError(err.message);
      }
    }
  };

  return (
    <div>
      {error && <div className="error">{error}</div>}
    </div>
  );
}
```

## Best practices

* Use `CrosslinkProvider` at the root of the tree that needs Crosslink state
* Use `isConnected(state)` rather than comparing against a single `"connected"` string -- a connected session can be `direct`, `crosslink-relayed`, or `turn-relayed`
* Show SAS to the user during pairing
* Review capabilities before approving
* `useCrosslinkEvent` and `useCrosslinkCall` clean up subscriptions and mounted-state tracking automatically on unmount
