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.
- You have connected the wallet — see Connect Wallet
- You know which package to use — see Integration Options
Event Reference
The SDK defines 6 events in the SDKEventPayloads interface:
| Event | Payload | When it fires |
|---|---|---|
connect | ConnectResult | { status: 'connecting' } | Connection starts or completes |
disconnect | { reason?: string } | Wallet session ends |
lock | { reason?: string } | Wallet locked by user or inactivity (also triggers disconnect) |
error | Error | Unrecoverable errors (e.g. iframe communication failure) |
accountChanged | WalletAccount | User 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:
- Stage 1:
{ status: 'connecting' }— emitted immediately whenconnect()is called and the modal opens - 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:
lockfires with{ reason?: string }disconnectfires immediately after withreason: "locked"
sdk.on('lock', ({ reason }) => {
console.log('Wallet locked:', reason);
});
sdk.on('disconnect', ({ reason }) => {
console.log('Disconnected:', reason); // "locked" when triggered by lock
});
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:
reconnectingfires with{ isReconnecting: true }- The SDK queries the wallet iframe to check if the session is still valid
- If valid,
connectfires with the fullConnectResult reconnectingfires with{ isReconnecting: false }
If the session has expired, reconnecting fires with false and no connect event follows — the SDK stays disconnected silently.
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 Event | Hook | State Updated |
|---|---|---|
connect | useWallet() | isConnected → true, isConnecting → false, accounts, selectedAccount |
disconnect | useWallet() | isConnected → false, accounts cleared |
lock | useWallet() | Same as disconnect |
error | useKea() | error |
accountChanged | useWallet() | selectedAccount updated |
reconnecting | useWallet() | 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
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
lockseparately fromdisconnect
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:
- React
- Vanilla JS
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>;
let isConnecting = false;
sdk.on('reconnecting', ({ isReconnecting }) => {
statusEl.textContent = isReconnecting ? 'Restoring session...' : '';
});
sdk.on('connect', (payload) => {
if ('status' in payload) {
isConnecting = true;
statusEl.textContent = 'Connecting...';
} else {
isConnecting = false;
statusEl.textContent = `Connected: ${payload.accounts[0]?.address}`;
}
});
sdk.on('error', (err) => {
statusEl.textContent = `Error: ${err.message}`;
});
React to Disconnect
Detect the transition from connected to disconnected and show a notification:
- React
- Vanilla JS
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>;
}
let wasConnected = false;
sdk.on('connect', () => {
wasConnected = true;
bannerEl.hidden = true;
});
sdk.on('disconnect', ({ reason }) => {
if (wasConnected) {
bannerEl.textContent = reason === 'locked'
? 'Wallet locked. Please reconnect.'
: 'Wallet disconnected.';
bannerEl.hidden = false;
setTimeout(() => { bannerEl.hidden = true; }, 5000);
}
wasConnected = false;
});
Update UI on Account Switch
React to the active account changing:
- React
- Vanilla JS
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>
);
sdk.on('accountChanged', (account) => {
activeAddressEl.textContent = account.address;
activeNameEl.textContent = account.label;
// Update button states
document.querySelectorAll('.account-btn').forEach((btn) => {
btn.classList.toggle('active', btn.dataset.address === account.address);
});
});
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
});
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.
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
- Handle Accounts — Work with multiple accounts and switch the active account
- Sign Transactions — Build and sign transactions with the active account
- Error Handling — Handle user rejection, timeouts, and connection failures
- BrowserSDK Reference — Full API reference for all methods and events