> For AI agents: the documentation index is at https://sdk.nimbusprivacy.xyz/llms.txt, and every page is available as Markdown at its URL with .md appended.

# Use it with React (/getting-started/react)

These patterns keep the SDK out of server rendering and tie a session to the component tree. They
come from the [example app](https://sdk.nimbusprivacy.xyz/resources/example-app), a Next.js App Router app. They work the same
in a Vite app.

## Load once, in the browser

The SDK needs IndexedDB, workers and WebAssembly threads. None of these exist while a server
renders a page. So import the SDK inside an effect, and keep one load for the whole page:

```tsx
"use client";

import type { NimbusSdk } from "@umbra/nimbus-solana-web";
import { useEffect, useState } from "react";

export type SdkState =
  | { kind: "loading" }
  | { kind: "loaded"; sdk: NimbusSdk }
  | { kind: "failed"; message: string };

let loading: Promise<NimbusSdk> | undefined;

/** Loads the SDK the first time it is called, and returns that same load after. */
async function loadOnce(): Promise<NimbusSdk> {
  loading ??= import("@umbra/nimbus-solana-web").then(({ loadNimbusSdk }) =>
    loadNimbusSdk({ cluster: "mainnet" }, { assetsBaseUrl: "/nimbus/" }),
  );
  return loading;
}

/** Returns the SDK once it has loaded in the browser. */
export function useNimbusSdk(): SdkState {
  const [state, setState] = useState<SdkState>({ kind: "loading" });
  useEffect(() => {
    let current = true;
    loadOnce().then(
      (sdk) => current && setState({ kind: "loaded", sdk }),
      (thrown: unknown) =>
        current && setState({ kind: "failed", message: String(thrown) }),
    );
    return () => {
      current = false;
    };
  }, []);
  return state;
}
```

- **`"use client"`** marks the file for the browser in the Next.js App Router. Without it, the
  hook cannot run.
- **The dynamic `import`** runs only inside the effect, so the server never evaluates the SDK.
- **`loadOnce`** shares one load between every component, and survives React's Strict Mode running
  effects twice.
- **`current`** stops a component that has unmounted from setting state.

A component that uses the SDK on every line can instead be loaded with Next.js's `next/dynamic`
and `ssr: false`, from a client component.

## Pick a wallet

Wallet extensions can register after the page loads. `followWallets` calls you back each time the
list changes, so start it in an effect:

```tsx
"use client";

import { followWallets, listWallets } from "@umbra/nimbus-solana-web";
import type { NimbusSdk, Wallet } from "@umbra/nimbus-solana-web";
import { useEffect, useState } from "react";

/** Returns the wallets that can sign on the SDK's cluster, kept current. */
export function useWallets(sdk: NimbusSdk): readonly Wallet[] {
  const [wallets, setWallets] = useState(() => listWallets(sdk));
  useEffect(() => {
    setWallets(listWallets(sdk));
    return followWallets(sdk, { onChange: setWallets });
  }, [sdk]);
  return wallets;
}

/** Renders one button per wallet. */
export function WalletPicker(props: {
  sdk: NimbusSdk;
  onPick: (wallet: Wallet) => void;
}) {
  const wallets = useWallets(props.sdk);
  if (wallets.length === 0) {
    return <p>Install a Solana wallet to continue.</p>;
  }
  return (
    <ul>
      {wallets.map((wallet) => (
        <li key={wallet.name}>
          <button type="button" onClick={() => props.onPick(wallet)}>
            <img src={wallet.icon} alt="" width={20} height={20} />
            {wallet.name}
          </button>
        </li>
      ))}
    </ul>
  );
}
```

`followWallets` returns the function that stops following. Returning it from the effect stops it
when the component unmounts.

## Keep the session in context

A session holds the user's keys in memory. Open it when they sign in, and close it when they sign
out, disconnect, or leave.

```tsx
"use client";

import {
  closeNimbusSession,
  openNimbusSession,
  openWalletConnection,
} from "@umbra/nimbus-solana-web";
import type {
  NimbusSdk,
  NimbusSession,
  TransactionVersion,
  Wallet,
} from "@umbra/nimbus-solana-web";
import { createContext, useContext, useEffect, useState } from "react";
import type { ReactNode } from "react";

interface Account {
  session: NimbusSession;
  wallet: Wallet;
  transactionVersion: TransactionVersion;
}

interface AccountState {
  account: Account | undefined;
  signIn: (wallet: Wallet) => Promise<string>;
  signOut: () => void;
}

/** The Wallet Standard's `standard:events` feature, as far as this file uses it. */
interface WalletEvents {
  on: (
    event: "change",
    listener: (properties: { accounts?: Wallet["accounts"] }) => void,
  ) => () => void;
}

const AccountContext = createContext<AccountState | undefined>(undefined);

/** Holds the signed-in account, and closes its session when it goes. */
export function AccountProvider(props: {
  sdk: NimbusSdk;
  gatewayToken: string;
  children: ReactNode;
}) {
  const { sdk, gatewayToken } = props;
  const [account, setAccount] = useState<Account>();

  async function signIn(wallet: Wallet): Promise<string> {
    const connection = await openWalletConnection(sdk, { wallet });
    if (connection.kind !== "connected") return connection.kind;
    const { signer, transactionVersion } = connection.connected;
    const opened = await openNimbusSession(
      sdk,
      { connectedWallet: signer },
      { gatewayToken },
    );
    if (opened.kind === "opened") {
      setAccount({ session: opened.session, wallet, transactionVersion });
    }
    return opened.kind;
  }

  useEffect(() => {
    if (account === undefined) return;
    return () => closeNimbusSession(account.session);
  }, [account]);

  useEffect(() => {
    if (account === undefined) return;
    const events = account.wallet.features["standard:events"] as
      | WalletEvents
      | undefined;
    return events?.on("change", ({ accounts }) => {
      const stillThere = accounts?.some(
        (entry) => entry.address === account.session.walletAddress,
      );
      if (stillThere === false) setAccount(undefined);
    });
  }, [account]);

  const state = { account, signIn, signOut: () => setAccount(undefined) };
  return (
    <AccountContext.Provider value={state}>
      {props.children}
    </AccountContext.Provider>
  );
}

/** Returns the account state; call it below `AccountProvider`. */
export function useAccount(): AccountState {
  const state = useContext(AccountContext);
  if (state === undefined) throw new Error("useAccount is outside AccountProvider");
  return state;
}
```

- **The first effect** closes a session whenever the account changes or the provider unmounts.
  Closing zeroes the keys in memory. Closing twice does nothing more.
- **The second effect** listens for the wallet's `change` event. When the user disconnects or
  switches account, the session closes.
- **`signIn`** returns the `kind` it stopped at, such as `declined`, so your UI can say why.

By default, the user's signature is kept for the tab's life, so a reload does not ask again. To
forget it on sign-out, call the store's `wipeSignatureSecret` as well.
[Sessions](https://sdk.nimbusprivacy.xyz/helpers/sessions) compares the three places it can be kept.

## Show progress

Every payment takes an `onProgress` callback. Put the stage into state, and render it:

```tsx
"use client";

import { createWithdrawalContext, withdraw } from "@umbra/nimbus-solana-web";
import type { NimbusSession } from "@umbra/nimbus-solana-web";
import type { Address } from "@solana/kit";
import { useState } from "react";

/** Withdraws 5 USDC to the user's own wallet, showing each stage. */
export function WithdrawButton(props: { session: NimbusSession; usdc: Address }) {
  const { session, usdc } = props;
  const [stage, setStage] = useState<string>();

  async function run(): Promise<void> {
    const result = await withdraw(
      await createWithdrawalContext(session),
      { mint: usdc, amount: 5_000_000n, recipient: session.walletAddress },
      { onProgress: (event) => setStage(event.stage) },
    );
    setStage(result.kind);
  }

  return (
    <>
      <button type="button" disabled={stage !== undefined} onClick={() => void run()}>
        Withdraw 5 USDC
      </button>
      {stage === undefined ? null : <p>{stage}</p>}
    </>
  );
}
```

Proofs run in the tab. Ask the user to keep it open while a payment runs, and disable the buttons
that would start another. [Show progress](https://sdk.nimbusprivacy.xyz/api-design/progress) has a message for every stage.

## Sync in the background

The private balance changes only when the device syncs. Payments sync on their own. To show
payments Alice receives, sync on an interval:

```tsx
"use client";

import { syncNoteStore } from "@umbra/nimbus-solana-web";
import type { NimbusSession } from "@umbra/nimbus-solana-web";
import { useEffect } from "react";

/** Syncs every 30 seconds while the session is open, then calls `onSynced`. */
export function useBackgroundSync(
  session: NimbusSession | undefined,
  onSynced: () => void,
): void {
  useEffect(() => {
    if (session === undefined) return;
    const controller = new AbortController();
    let running = false;
    async function syncOnce(): Promise<void> {
      if (running || session === undefined) return;
      running = true;
      try {
        const result = await syncNoteStore(
          session,
          {},
          { signal: controller.signal },
        );
        if (result.kind === "ingested") onSynced();
      } catch (thrown) {
        if (!controller.signal.aborted) console.error(thrown);
      } finally {
        running = false;
      }
    }
    void syncOnce();
    const timer = setInterval(() => void syncOnce(), 30_000);
    return () => {
      clearInterval(timer);
      controller.abort();
    };
  }, [session, onSynced]);
}
```

- **`running`** skips a round while the previous one is still going.
- **The `AbortController`** stops a sync between rounds when the session changes or the component
  unmounts.
- **`onSynced`** is where you read the balance again with `readBalances`. Wrap it in `useCallback`,
  or the effect restarts on every render.

[Sync](https://sdk.nimbusprivacy.xyz/helpers/sync) covers what a sync reads and what each result means.

## Next

- [The example app](https://sdk.nimbusprivacy.xyz/resources/example-app): A whole Next.js app built from these patterns.
- [Sessions](https://sdk.nimbusprivacy.xyz/helpers/sessions): Open, close and remember a user's private account.
- [Production checklist](https://sdk.nimbusprivacy.xyz/production/checklist): What to check before real users arrive.


Last updated 21 October 2026
