Skip to content
Get started

Use it with React

Load the SDK once in the browser, follow the user's wallets, keep the session in context, and sync in the background.

These patterns keep the SDK out of server rendering and tie a session to the component tree. They come from the 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:

use-nimbus-sdk.ts
"use client";

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

export type  =
  | { : "loading" }
  | { : "loaded"; : NimbusSdk }
  | { : "failed"; : string };

let : <NimbusSdk> | undefined;

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

/** Returns the SDK once it has loaded in the browser. */
export function ():  {
  const [, ] = <>({ : "loading" });
  (() => {
    let  = true;
    ().(
      () =>  && ({ : "loaded",  }),
      (: unknown) =>
         && ({ : "failed", : () }),
    );
    return () => {
       = false;
    };
  }, []);
  return ;
}
  • "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:

wallet-picker.tsx
"use client";

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

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

/** Renders one button per wallet. */
export function (: {
  : NimbusSdk;
  : (: Wallet) => void;
}) {
  const  = (.);
  if (. === 0) {
    return <>Install a Solana wallet to continue.</>;
  }
  return (
    <>
      {.(() => (
        < ={.}>
          < ="button" ={() => .()}>
            < ={.} ="" ={20} ={20} />
            {.}
          </>
        </>
      ))}
    </>
  );
}

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.

account-provider.tsx
"use client";

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

interface Account {
  : NimbusSession;
  : Wallet;
  : ;
}

interface AccountState {
  : Account | undefined;
  : (: Wallet) => <string>;
  : () => void;
}

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

const  = <AccountState | undefined>();

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

  async function (: Wallet): <string> {
    const  = await (, {  });
    if (. !== "connected") return .;
    const { ,  } = .;
    const  = await (
      ,
      { :  },
      {  },
    );
    if (. === "opened") {
      ({ : ., ,  });
    }
    return .;
  }

  (() => {
    if ( === ) return;
    return () => (.);
  }, []);

  (() => {
    if ( === ) return;
    const  = ..["standard:events"] as
      | WalletEvents
      | undefined;
    return ?.("change", ({  }) => {
      const  = ?.(
        () => . === ..,
      );
      if ( === false) ();
    });
  }, []);

  const  = { , , : () => () };
  return (
    <. ={}>
      {.}
    </.>
  );
}

/** Returns the account state; call it below `AccountProvider`. */
export function (): AccountState {
  const  = ();
  if ( === ) throw new ("useAccount is outside AccountProvider");
  return ;
}
  • 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 compares the three places it can be kept.

Show progress

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

withdraw-button.tsx
"use client";

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

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

  async function (): <void> {
    const  = await (
      await (),
      { : , : 5_000_000n, : . },
      { : () => (.) },
    );
    (.);
  }

  return (
    <>
      < ="button" ={ !== } ={() => void ()}>
        Withdraw 5 USDC
      </>
      { ===  ? null : <>{}</>}
    </>
  );
}

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 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:

use-background-sync.ts
"use client";

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

/** Syncs every 30 seconds while the session is open, then calls `onSynced`. */
export function (
  : NimbusSession | undefined,
  : () => void,
): void {
  (() => {
    if ( === ) return;
    const  = new ();
    let  = false;
    async function (): <void> {
      if ( ||  === ) return;
       = true;
      try {
        const  = await (
          ,
          {},
          { : . },
        );
        if (. === "ingested") ();
      } catch () {
        if (!..) .();
      } finally {
         = false;
      }
    }
    void ();
    const  = (() => void (), 30_000);
    return () => {
      ();
      .();
    };
  }, [, ]);
}
  • 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 covers what a sync reads and what each result means.

Next

Last updated

On this page