Skip to main content

SDK Events & Listeners

The BrowserSDK emits events whenever wallet state changes — connection, disconnection, account switches, locking, and errors. Your app can subscribe to these events to keep the UI in sync with the wallet. In React, the KeaProvider handles all event subscriptions automatically and surfaces state through hooks; in vanilla JavaScript, you subscribe manually using on, off, and once.

Prerequisites

Event Reference

The SDK defines 6 events in the SDKEventPayloads interface:

EventPayloadWhen it fires
connectConnectResult | { status: 'connecting' }Connection starts or completes
disconnect{ reason?: string }Wallet session ends
lock{ reason?: string }Wallet locked by user or inactivity (also triggers disconnect)
errorErrorUnrecoverable errors (e.g. iframe communication failure)
accountChangedWalletAccountUser switches active account via selectAccount()
reconnecting{ isReconnecting: boolean }Silent auto-reconnect starts or ends

See also: SDKEvent, EventCallback

Event Lifecycle

Before writing listeners, it helps to understand when and how events fire. Three behaviors are worth knowing upfront.

The Connection Flow

The connect event fires twice during a connection:

  1. Stage 1: { status: 'connecting' } — emitted immediately when connect() is called and the modal opens
  2. Stage 2: Full ConnectResult — emitted when the user approves the connection in the wallet
sdk.on('connect', (payload) => {
if ('status' in payload) {
console.log('Modal opened, waiting for user...'); // { status: 'connecting' }
} else {
console.log('Connected!', payload.accounts); // ConnectResult
}
});

The Lock → Disconnect Cascade

When the wallet is locked (by the user or due to inactivity), two events fire in quick succession:

  1. lock fires with { reason?: string }
  2. disconnect fires immediately after with reason: "locked"
sdk.on('lock', ({ reason }) => {
console.log('Wallet locked:', reason);
});

sdk.on('disconnect', ({ reason }) => {
console.log('Disconnected:', reason); // "locked" when triggered by lock
});
tip

Listen to disconnect for general session-end cleanup. Only listen to lock if you need a specific "Wallet Locked" UI — otherwise disconnect covers both cases.

Silent Reconnection

When autoConnect is enabled, the SDK silently restores sessions on page reload. During this process:

  1. reconnecting fires with { isReconnecting: true }
  2. The SDK queries the wallet iframe to check if the session is still valid
  3. If valid, connect fires with the full ConnectResult
  4. reconnecting fires with { isReconnecting: false }

If the session has expired, reconnecting fires with false and no connect event follows — the SDK stays disconnected silently.

tip

Use isReconnecting for a subtle loading indicator (e.g., "Restoring session..."). Reserve isConnecting for user-initiated connection attempts that show the full modal.

Subscribing to Events

The SDK provides three subscription methods on the BrowserSDK instance:

on(event, callback) — Persistent Listener

Registers a callback that fires every time the event is emitted. Stays active until explicitly removed.

const onConnect = (result) => {
console.log('Connected:', result);
};
sdk.on('connect', onConnect);

off(event, callback) — Remove Listener

Removes a previously registered callback. You must pass the same function reference used with on().

sdk.off('connect', onConnect); // must be the same reference

once(event, callback) — One-Shot Listener

Fires the callback exactly once, then auto-removes. Useful for waiting on a single event.

sdk.once('connect', (result) => {
console.log('First connect only:', result);
// Automatically unsubscribed — future connects won't trigger this
});

Type-Safe Listeners (TypeScript)

The SDK exports EventCallback<T> for type-safe event handlers:

import type { EventCallback } from '@kea-wallet/browser-sdk';

const onAccount: EventCallback<'accountChanged'> = (account) => {
// account is typed as WalletAccount
console.log(account.address, account.label);
};
sdk.on('accountChanged', onAccount);

React Cleanup Pattern

When using manual subscriptions in React (via useKea()), always clean up in the useEffect return:

import { useEffect } from 'react';
import { useKea } from '@kea-wallet/react-sdk';

function AccountLogger() {
const { sdk } = useKea();

useEffect(() => {
if (!sdk) return;

const handler = (account) => {
console.log('Account changed:', account.address);
};

sdk.on('accountChanged', handler);
return () => sdk.off('accountChanged', handler);
}, [sdk]);
}

React Integration

For React apps, KeaProvider subscribes to all 6 SDK events internally and maps them to React state. In most cases, you don't need manual event subscriptions — just read state from hooks.

State Mapping

KeaProvider translates SDK events into hook state automatically:

SDK EventHookState Updated
connectuseWallet()isConnected → true, isConnecting → false, accounts, selectedAccount
disconnectuseWallet()isConnected → false, accounts cleared
lockuseWallet()Same as disconnect
erroruseKea()error
accountChangeduseWallet()selectedAccount updated
reconnectinguseWallet()isReconnecting
import { useWallet, useKea } from '@kea-wallet/react-sdk';

function WalletStatus() {
const {
isConnected,
isConnecting,
isReconnecting,
accounts,
selectedAccount,
connect,
disconnect,
} = useWallet();

const { error } = useKea();

if (isReconnecting) return <div>Restoring session...</div>;
if (isConnecting) return <div>Connecting...</div>;
if (error) return <div>Error: {error.message}</div>;

if (!isConnected) {
return <button onClick={() => connect()}>Connect</button>;
}

return (
<div>
<p>Connected with {accounts.length} account(s)</p>
<p>Active: {selectedAccount?.address}</p>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
);
}

When to Use Manual Subscriptions

tip

Use manual sdk.on() (via useKea()) only when you need:

  • Side effects on specific events — analytics tracking, logging, toast notifications
  • Custom behavior not covered by hook state — e.g., playing a sound on disconnect
  • Fine-grained control — listening to lock separately from disconnect
Prefer hooks over manual subscriptions

In React, prefer reading useWallet() / useKea() state over manual sdk.on() listeners. Manual subscriptions inside useEffect can cause stale closures if dependencies aren't handled correctly. The hooks are reactive and always return current state.

Common Patterns

Loading State During Connection

Handle all wallet states — reconnecting, connecting, connected, error, and disconnected:

const { isConnecting, isReconnecting, isConnected, connect } = useWallet();
const { error } = useKea();

if (isReconnecting) return <Spinner text="Restoring session..." />;
if (isConnecting) return <Spinner text="Connecting..." />;
if (error) return <div>Error: {error.message}</div>;
if (!isConnected) return <button onClick={() => connect()}>Connect</button>;
return <div>Connected!</div>;

React to Disconnect

Detect the transition from connected to disconnected and show a notification:

import { useEffect, useRef, useState } from 'react';
import { useWallet } from '@kea-wallet/react-sdk';

function DisconnectNotification() {
const { isConnected } = useWallet();
const wasConnected = useRef(false);
const [showBanner, setShowBanner] = useState(false);

useEffect(() => {
if (isConnected) {
wasConnected.current = true;
setShowBanner(false);
} else if (wasConnected.current) {
wasConnected.current = false;
setShowBanner(true);
const timer = setTimeout(() => setShowBanner(false), 5000);
return () => clearTimeout(timer);
}
}, [isConnected]);

if (!showBanner) return null;
return <div className="banner">Wallet disconnected. Your session has ended.</div>;
}

Update UI on Account Switch

React to the active account changing:

const { accounts, selectedAccount, selectAccount } = useWallet();

// selectedAccount updates reactively — no manual subscription needed
return (
<div>
<h3>Active: {selectedAccount?.address}</h3>
{accounts.map((acc) => (
<button
key={acc.address}
onClick={() => selectAccount(acc)}
disabled={acc.address === selectedAccount?.address}
>
{acc.label}{acc.address.slice(0, 8)}...
</button>
))}
</div>
);

Vanilla JavaScript

For non-React apps, use BrowserSDK directly with full event lifecycle control:

import { BrowserSDK } from '@kea-wallet/browser-sdk';

// 1. Create & initialize
const sdk = new BrowserSDK({
rpcUrl: 'https://grpc-web.alphanet.thruput.org',
autoConnect: true,
});
await sdk.initialize();

// 2. Subscribe to all events
sdk.on('connect', (result) => {
if ('status' in result) {
console.log('Connecting...');
} else {
console.log('Connected:', result.accounts);
}
});

sdk.on('disconnect', ({ reason }) => {
console.log('Disconnected:', reason);
});

sdk.on('lock', ({ reason }) => {
console.log('Wallet locked:', reason);
});

sdk.on('error', (err) => {
console.error('SDK error:', err.message);
});

sdk.on('accountChanged', (account) => {
console.log('Switched to:', account.address, account.label);
});

sdk.on('reconnecting', ({ isReconnecting }) => {
console.log('Reconnecting:', isReconnecting);
});

// 3. Connect on user click
connectBtn.addEventListener('click', async () => {
try {
const result = await sdk.connect({
metadata: { appName: 'My App' },
});
console.log('Accounts:', sdk.getAccounts());
} catch (err) {
console.error('Connection failed:', err);
}
});

// 4. Disconnect & cleanup on page unload
window.addEventListener('beforeunload', () => {
sdk.destroy(); // removes all listeners and tears down iframe
});
Cleanup required

Call sdk.destroy() when your app unmounts or the page unloads to tear down the iframe and remove all event listeners. Failing to do so may cause memory leaks. In React, KeaProvider handles cleanup automatically on unmount.

note

disconnect() ends the session but keeps the SDK instance alive for future connections. destroy() tears down the SDK entirely — call it only when you're completely done (e.g., page unload).

What's Next