butr
API reference

@usebutr/react

The provider and every hook, grouped by what they do.

Provider

WalletManagerProvider

import type { WalletManagerConfig } from "@usebutr/core";
import { WalletManagerProvider } from "@usebutr/react";
import { autoDiscovery } from "@usebutr/wallets";

const config: WalletManagerConfig = {
  sources: [autoDiscovery()],
  storageKeyPrefix: "my-app",
};

export const Providers = ({ children }: { children: React.ReactNode }) => (
  <WalletManagerProvider config={config}>{children}</WalletManagerProvider>
);
PropTypeNotes
childrenReact.ReactNodeRequired.
configWalletManagerConfigRead once at mount; define it at module scope. Every field is in the core reference.
initialStateWalletSnapshotSeeds the manager synchronously, typically from readWalletSnapshot in a Server Component. See Hydration.

The provider creates one manager per mount, so a server render never shares wallet state between requests, and starts it in an effect, so nothing runs on the server. WalletManagerProviderProps is exported.

useWalletManager(): WalletManager

The manager behind the nearest provider. Its actions are stable references, and getState() reads without subscribing, so they are safe in callbacks and effect dependencies.

const { disconnect, disconnectAll, requestAccounts, setAccount, setActive, setSelection } =
  useWalletManager();

See WalletManager for every member.

Connect

useConnect(): UseConnectResult

The connect action with the state of its latest attempt.

FieldTypeNotes
connect(connectorId: string) => voidFire and forget: a failure lands in error and status, never in a rejection.
connectAsync(connectorId: string) => Promise<ConnectedWallet>Resolves the connected wallet; rejects with a ConnectionError.
status"idle" | "connecting" | "success" | "error"The latest attempt.
errorConnectionError | nullThe latest attempt's failure.
connectingIdstring | nullThe wallet the in-flight attempt is for.
reset() => voidClears error and returns status to "idle", e.g. from a dismiss button.
const { connect, connectingId, error } = useConnect();

return wallets.map((adapter) => (
  <button disabled={connectingId !== null} key={adapter.id} onClick={() => connect(adapter.id)}>
    {connectingId === adapter.id ? "Connecting…" : adapter.name}
  </button>
));

Use connectAsync when the next step needs the wallet, such as signing in right after connecting.

Read state

HookReturnsNotes
useDiscoveredWallets()ReadonlyArray<WalletAdapter>Every adapter the sources announced, including fromAdapters ones. One per platform a wallet speaks.
useConnectedWallets()ReadonlyArray<ConnectedWallet>The pool as an array; stable while the pool is unchanged.
useWallet(connectorId?)ConnectedWallet | undefinedThe active wallet when connectorId is omitted; undefined for null.
useSelectedWallet(platform)ConnectedWallet<P> | undefinedThe wallet selected for platform, typed to that platform's adapter.
useAccounts(connectorId?)ReadonlyArray<Account>The active wallet's when connectorId is omitted; empty for null.
useConnectionStatus()ConnectionStatus"connected" | "connecting" | "disconnected" | "reconnecting".
useIsHydrated()booleanThe start-up hydration pass has finished. Always true when seeded from initialState.
useIsReconnecting(connectorId?)booleanThe wallet is still a shadow seeded from initialState; its connect, getAccounts and getSigner reject until this turns false.
useDiscoveredWalletsByPlatform()Map<ChainPlatform, Array<WalletAdapter>>In CHAIN_PLATFORMS order, empty platforms omitted.
useConnectedWalletsByPlatform()Map<ChainPlatform, Array<ConnectedWallet>>Same ordering.
useWalletState(selector)TAny slice of WalletState.

useWallet, useAccounts and useIsReconnecting read the active wallet when connectorId is omitted, and no wallet when it is null, so useWallet(maybeId ?? null) never falls back to the active one. useWallet, useSelectedWallet and useAccounts re-render only when that entry's adapter, active account or account list changes.

useSelectedWallet(platform)

Returns ConnectedWallet<P>, so the connector is already narrowed to that platform's adapter and its transaction type:

const wallet = useSelectedWallet("svm");

const send = async (serializedTx: Uint8Array) => {
  if (wallet?.connector.sendTx) {
    return wallet.connector.sendTx(serializedTx);
  }
};

useWalletState(selector)

A custom selector with shallow equality on the result, so returning an inline object or array does not loop:

const { activeConnectorId, isUserDisconnected } = useWalletState((state) => ({
  activeConnectorId: state.activeConnectorId,
  isUserDisconnected: state.isUserDisconnected,
}));

useDiscoveredWalletsByPlatform() and useConnectedWalletsByPlatform()

A multi-chain wallet announces one adapter per platform it speaks, so the flat list repeats a brand once per chain. These bucket it by platform:

const byPlatform = useDiscoveredWalletsByPlatform();

return [...byPlatform].map(([platform, wallets]) => (
  <section key={platform}>
    <h3>{platform}</h3>
    {wallets.map((adapter) => (
      <WalletButton adapter={adapter} key={adapter.id} />
    ))}
  </section>
));

The result is memoised on the source list. Outside React, groupByPlatform does the same bucketing.

Async hooks

type AsyncState<T> =
  | { data: null; error: null; status: "idle" }
  | { data: null; error: null; status: "loading" }
  | { data: T; error: null; status: "success" }
  | { data: null; error: Error; status: "error" };

A result shows only for the wallet that produced it, so switching wallets never renders the previous wallet's data. error is always an Error; a wallet that throws a string or a bare object is wrapped, with the original as its cause.

useSigner(wallet): AsyncState<WalletSigner>

The signer for wallet, the entry from useWallet() or useSelectedWallet(platform). Narrow data on kind. Stays "idle" without a wallet and while the wallet is reconnecting.

const wallet = useSelectedWallet("evm");
const signer = useSigner(wallet);

const client = useMemo(
  () =>
    signer.data?.kind === "eip1193"
      ? createWalletClient({ transport: custom(signer.data.provider) })
      : null,
  [signer.data],
);

useBalance(wallet, options?): UseBalanceResult

type UseBalanceOptions = {
  account?: Account; // the wallet's active account when omitted
  token?: string; // an ERC-20 address on EVM; the native asset when omitted
};

type UseBalanceResult = AsyncState<Balance> & { refetch: () => void };
const wallet = useSelectedWallet("evm");
const { data, refetch, status } = useBalance(wallet);

Reads through wallet.connector.getBalance, and stays "idle" without a wallet. Of butr's adapters only the injected and WalletConnect EVM ones define it, so the hook stays "idle" for Solana, Sui, Bitcoin, Polkadot and Ledger wallets, and while a wallet is reconnecting. See balances.

Types

WalletManagerProviderProps, UseConnectResult, ConnectionStatus, AsyncState, UseBalanceOptions, UseBalanceResult.

Source: packages/react/src/index.ts, context.tsx, hooks/state.ts, hooks/connect.ts, hooks/grouped.ts, hooks/async-resources.ts.