---
title: "@sigilry/react"
description: "React Query hooks and context for Canton wallet integration."
url: "https://sigilry.org/packages/react"
---

`@sigilry/react` provides React hooks and context for interacting with the Sigilry provider. It builds on `@sigilry/dapp` and uses `@tanstack/react-query` for async state.

## Install

```bash
yarn add @sigilry/react@^3 @sigilry/dapp@^3 @sigilry/canton-json-api@^1 @tanstack/react-query
```

## Provider setup

```tsx
import { CantonReactProvider } from "@sigilry/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

export function AppRoot() {
  return (
    <QueryClientProvider client={queryClient}>
      <CantonReactProvider>
        <App />
      </CantonReactProvider>
    </QueryClientProvider>
  );
}
```

## Provider discovery

When multiple Canton wallets may be installed, let the user pick one. `useDiscovery()` returns the live list of discovered wallets; render them with `WalletPicker`; pass the selected provider to `CantonReactProvider` via its controlled `provider` prop. See [Provider Discovery](/concepts/discovery/) for the underlying model.

```tsx
import { useState } from "react";
import {
  CantonReactProvider,
  useDiscovery,
  WalletPicker,
  type DiscoveredWallet,
} from "@sigilry/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { SpliceProvider } from "@sigilry/dapp";

const queryClient = new QueryClient();

function WalletGate({ children }: { children: React.ReactNode }) {
  const wallets = useDiscovery();
  const [provider, setProvider] = useState<SpliceProvider | null>(null);

  if (!provider) {
    return (
      <WalletPicker
        wallets={wallets}
        onSelect={(w: DiscoveredWallet) => setProvider(w.getProvider())}
        emptyState={<p>No Canton wallet detected.</p>}
      />
    );
  }

  // The controlled `provider` prop binds every hook below to the chosen wallet.
  return <CantonReactProvider provider={provider}>{children}</CantonReactProvider>;
}

export function AppRoot() {
  return (
    <QueryClientProvider client={queryClient}>
      <WalletGate>
        <App />
      </WalletGate>
    </QueryClientProvider>
  );
}
```

Without a `provider` prop, `CantonReactProvider` falls back to the injected `window.canton` (the single-wallet path), so adopting discovery is opt-in per consumer.

## Push event subscriptions

`useCanton()` exposes subscription helpers for the CIP-103 push events — `onAccountsChanged` and `onStatusChanged` are the §4.2.2 sync events; `onConnected` is the login-flow event. Each returns an unsubscribe function and is safe to use inside an effect.

```tsx
import { useEffect } from "react";
import { useCanton } from "@sigilry/react";

export function StatusWatcher() {
  const { onStatusChanged, onConnected, onAccountsChanged } = useCanton();

  useEffect(() => {
    const offStatus = onStatusChanged((status) => console.log("status", status));
    const offConnected = onConnected((status) => console.log("connected", status));
    const offAccounts = onAccountsChanged((accounts) => console.log("accounts", accounts));
    return () => {
      offStatus();
      offConnected();
      offAccounts();
    };
  }, [onStatusChanged, onConnected, onAccountsChanged]);

  return null;
}
```

Most apps don’t need these directly — `useAccounts`, `useSession`, and `useConnect` already react to the same events internally. Reach for the raw subscriptions to observe transitions, e.g. log “session expired” when `onAccountsChanged` fires with an empty array.

## Connect and read accounts

```tsx
import { useAccounts, useActiveAccount, useConnect } from "@sigilry/react";

export function WalletStatus() {
  const { connect, isPending } = useConnect();
  const { data: accounts, isConnected } = useAccounts();
  const { data: activeAccount } = useActiveAccount();

  if (!isConnected) {
    return (
      <button onClick={connect} disabled={isPending}>
        {isPending ? "Connecting..." : "Connect"}
      </button>
    );
  }

  return (
    <div>
      <p>Active: {activeAccount?.hint ?? "Unknown"}</p>
      <ul>
        {accounts.map((account) => (
          <li key={account.partyId}>{account.partyId}</li>
        ))}
      </ul>
    </div>
  );
}
```

## Ledger API request

```tsx
import { useLedgerApi } from "@sigilry/react";
import { zGetLedgerEndResponse } from "@sigilry/canton-json-api";

export function LedgerEndButton() {
  const { requestAsync, data, isPending } = useLedgerApi({
    parse: zGetLedgerEndResponse.parse,
  });

  const fetchLedgerEnd = async () => {
    await requestAsync({
      requestMethod: "get",
      resource: "/v2/state/ledger-end",
    });
  };

  return (
    <div>
      <button onClick={fetchLedgerEnd} disabled={isPending}>
        Fetch ledger end
      </button>
      {data && <span>Offset: {String(data.data.offset)}</span>}
    </div>
  );
}
```

## Session awareness

```tsx
import { useSession } from "@sigilry/react";

export function SessionBadge() {
  const { sessionState, timeRemainingFormatted, reauthenticate } = useSession();

  return (
    <div>
      <span>Status: {sessionState.status}</span>
      <span>Time remaining: {timeRemainingFormatted}</span>
      {sessionState.status === "expiring_soon" && (
        <button onClick={reauthenticate}>Refresh session</button>
      )}
    </div>
  );
}
```

## Key exports

* `CantonReactProvider`, `useCanton`
* `useConnect`, `useDisconnect`
* `useAccounts`, `useActiveAccount`
* `useLedgerApi`, `useLedgerEnd`, `useLedgerUpdates`
* `useContractStream`, `useActiveContracts`, `useExerciseChoice`, `useSubmitCommand`
* `useSession`, `parseJwt`
* `useDiscovery`, `WalletPicker` (+ `DiscoveredWallet`, `SpliceProviderInfo` types)

## Connection state

`useCanton()` exposes a discriminated union `connectionState`:

* `disconnected`
* `connecting`
* `connected` (includes `accounts` and `networkId`)
* `session_expired`
* `error`
