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 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
importruns only inside the effect, so the server never evaluates the SDK. loadOnceshares one load between every component, and survives React's Strict Mode running effects twice.currentstops 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:
"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.
"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
changeevent. When the user disconnects or switches account, the session closes. signInreturns thekindit stopped at, such asdeclined, 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:
"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 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 () => {
();
.();
};
}, [, ]);
}runningskips a round while the previous one is still going.- The
AbortControllerstops a sync between rounds when the session changes or the component unmounts. onSyncedis where you read the balance again withreadBalances. Wrap it inuseCallback, or the effect restarts on every render.
Sync covers what a sync reads and what each result means.
Next
Last updated