Pattern Cookbook
Copy-paste-ready recipes for the most common Canton wallet flows, built on the real PartyLayer hooks. Every recipe has a matching live, editable scenario in PartyLayer Studio — open it, edit the code, and watch it run against a mock CIP-0103 wallet — plus a frank “When not to use” note so you reach for the right tool. PartyLayer is MIT-licensed and fully open source.
1. Connect a wallet
List the available wallets with useWallets() and connect with useConnect(). After a successful connect the active party is available from useAccount().
import { PartyLayerKit, useWallets, useConnect, useAccount } from '@partylayer/react';
function ConnectPanel() {
const { wallets } = useWallets();
const { connect, isConnecting, error } = useConnect();
const { party } = useAccount();
if (party) return <p>Connected — {party}</p>;
return (
<>
{wallets.map((w) => (
<button key={String(w.walletId)} disabled={isConnecting}
onClick={() => connect({ walletId: w.walletId })}>
{isConnecting ? 'Connecting…' : 'Connect ' + w.name}
</button>
))}
{error && <p role="alert">{error.message}</p>}
</>
);
}
export default function App() {
return (
<PartyLayerKit network="devnet" appName="My dApp">
<ConnectPanel />
</PartyLayerKit>
);
}
Try it live in Studio → “Connect a wallet”
⚠️ When not to use
Connect is a
browser wallet flow — it needs a user gesture and a wallet extension/provider, so it has no place in server-side or headless code. Don’t call
connect() on mount or automatically; trigger it from an explicit user action. To revive an
existing session after a reload, use Reconnect (recipe 4), not a fresh connect.
2. Sign a message
useSignMessage() asks the connected wallet to sign an arbitrary string and returns the signature, the signing party, and the original message.
import { useSignMessage } from '@partylayer/react';
function SignButton() {
const { signMessage, isSigning, error } = useSignMessage();
async function onSign() {
const signed = await signMessage({ message: 'Sign in to My dApp' });
if (signed) {
console.log(signed.signature, signed.partyId, signed.message);
}
}
return (
<>
<button onClick={onSign} disabled={isSigning}>
{isSigning ? 'Signing…' : 'Sign message'}
</button>
{error && <p role="alert">{error.message}</p>}
</>
);
}
Try it live in Studio → “Sign a message”
⚠️ When not to use
A signed message proves control of a key at a
single moment — it is not an ongoing session and it changes no on-chain state. Don’t use it as a session substitute (track the session via
useAccount() /
useSession()), and don’t use it to move funds or update a contract — that’s a transaction (recipe 3).
3. Submit a transaction
useSubmitTransaction() submits a signed transaction and resolves a receipt ({ transactionHash, commandId, updateId }). The wallet also emits a txChanged lifecycle (pending → signed → executed) you can subscribe to for a live status.
import { useState } from 'react';
import { useSubmitTransaction } from '@partylayer/react';
function SubmitButton({ signedTx }: { signedTx: unknown }) {
const { submitTransaction, isSubmitting, error } = useSubmitTransaction();
const [updateId, setUpdateId] = useState<string | null>(null);
async function onSubmit() {
const receipt = await submitTransaction({ signedTx });
if (receipt) setUpdateId(receipt.updateId ?? null);
}
return (
<>
<button onClick={onSubmit} disabled={isSubmitting}>
{isSubmitting ? 'Submitting…' : 'Submit transaction'}
</button>
{updateId && <p>Executed — updateId {updateId}</p>}
{error && <p role="alert">{error.message}</p>}
</>
);
}
Try it live in Studio → “Submit a transaction” (watch the Pending → Signed → Executed stepper).
⚠️ When not to use
Submit is
capability-gated: it only works when the connected wallet’s adapter implements it, otherwise it throws
CapabilityNotSupportedError. Don’t assume every wallet can submit — check capabilities first (recipe 9). For sign-only proof without broadcasting, use recipe 2.
4. Reconnect a session (transient resilience)
useSession().restore() re-probes the live wallet and rehydrates the session: status moves connected → reconnecting → connected. This is the same path that runs on a page reload, so a transient provider drop heals itself with no fresh login.
import { useSession, useAccount } from '@partylayer/react';
function ReconnectButton() {
const { restore } = useSession();
const { status } = useAccount(); // 'connected' | 'reconnecting' | 'disconnected' | ...
return (
<button onClick={() => restore()}>
Reconnect (status: {status})
</button>
);
}
Try it live in Studio → “Session resilience — reconnect”
⚠️ When not to use
restore() re-probes an
existing session (reload / transient drop) — it is not a fresh login. If there is no session to revive it simply lands
disconnected; for a first-time connection use recipe 1. And after an
explicit disconnect (recipe 5) restore won’t bring it back — that’s intentional.
5. Handle a terminal disconnect
useSession().disconnect() ends the session deliberately. It is terminal: the session is cleared and never auto-reconnects, which is exactly the resilience boundary — a transient drop reconnects, an explicit disconnect does not.
import { useSession, useAccount } from '@partylayer/react';
function DisconnectButton() {
const { disconnect } = useSession();
const { isConnected } = useAccount();
if (!isConnected) return <span>Disconnected</span>;
return <button onClick={() => disconnect()}>Disconnect</button>;
}
Try it live in Studio → “Session resilience — disconnect”
⚠️ When not to use
Use this only for a
user-intended sign-out. Don’t call
disconnect() on a transient network blip you actually want to recover from — that suppresses auto-reconnect; let the resilience path (recipe 4) handle transient drops instead.
6. PartyLayer + React Query
PartyLayer has no React Query dependency, but it composes cleanly with it (the wagmi pattern): model the session as a useQuery and connect/sign/submit as useMutation, then invalidateQueries to refetch.
import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { PartyLayerKit, usePartyLayer, useConnect } from '@partylayer/react';
function Session() {
const client = usePartyLayer();
const qc = useQueryClient();
const { connect } = useConnect();
const session = useQuery({ queryKey: ['session'], queryFn: () => client.getActiveSession() });
const connectMut = useMutation({
mutationFn: (walletId: string) => connect({ walletId }),
onSuccess: () => qc.invalidateQueries({ queryKey: ['session'] }),
});
return <pre>{session.data ? String(session.data.partyId) : 'no session'}</pre>;
}
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<PartyLayerKit network="devnet" appName="My dApp">
<Session />
</PartyLayerKit>
</QueryClientProvider>
);
}
Try it live in Studio → “React Query + DevTools”
⚠️ When not to use
Only reach for this if you already use (or want) React Query. PartyLayer’s built-in hooks are reactive on their own — for non-React apps or simple cases, adding a query client is extra weight for no gain.
7. Multi-framework (React / Vue / Vanilla)
The same connect flow across the three bindings. React uses useConnect(); Vue uses @partylayer/vue’s plugin + composables; Vanilla uses the SDK client directly.
// React — @partylayer/react
import { useConnect, useWallets } from '@partylayer/react';
const { wallets } = useWallets();
const { connect } = useConnect();
// connect({ walletId: wallets[0].walletId })
<!-- Vue — @partylayer/vue -->
<script setup>
import { useSession, useAccount } from '@partylayer/vue';
const { connect, isConnecting } = useSession();
const { party } = useAccount();
</script>
<template>
<button v-if="!party" @click="connect()" :disabled="isConnecting">Connect</button>
<p v-else>Connected: {{ party }}</p>
</template>
<!-- main.js: install the plugin with a CIP-0103 provider -->
<!-- app.use(createPartyLayerSession({ provider })) -->
// Vanilla — @partylayer/sdk
import { createPartyLayer } from '@partylayer/sdk';
const client = createPartyLayer({ network: 'devnet', app: { name: 'My dApp' } });
const wallets = await client.listWallets();
const session = await client.connect({ walletId: wallets[0].walletId });
console.log(session.partyId);
Try it live in Studio → “Framework toggle” (React / Vue / Vanilla, same demo).
⚠️ When not to use
The toggle is a
teaching device, not a runtime switch. Pick one binding for your app — don’t ship all three or swap frameworks at runtime.
8. Error handling
useConnect() never throws — it resolves null and exposes a typed error (a PartyLayerError subclass). Branch on the class (or error.code), and clear it with reset().
import { useConnect } from '@partylayer/react';
import { UserRejectedError, WalletNotInstalledError } from '@partylayer/core';
function ConnectWithErrors({ walletId }: { walletId: string }) {
const { connect, isConnecting, error, reset } = useConnect();
async function onConnect() {
reset();
await connect({ walletId }); // resolves null on failure; sets `error`
}
function friendly(e: NonNullable<typeof error>) {
if (e instanceof UserRejectedError) return 'You cancelled the request.';
if (e instanceof WalletNotInstalledError) return 'That wallet isn’t installed.';
return 'Something went wrong — please try again.';
}
return (
<>
<button onClick={onConnect} disabled={isConnecting}>Connect</button>
{error && <p role="alert">{friendly(error)}</p>}
</>
);
}
Try it live in Studio → “Connect a wallet” — the Mock driver’s failure picker fires each path (User rejected, Insufficient traffic, Synchronizer error, Transaction timeout, Generic error). See also Error Handling.
⚠️ When not to use
Don’t swallow errors silently, and don’t surface a raw
error.code to end users — map codes to friendly copy as above. A cancelled request (
UserRejectedError) is normal, not a crash — handle it as a no-op.
9. Capability gating
Not every wallet supports every operation. Capability-gated calls like submit throw CapabilityNotSupportedError when the connected adapter lacks the capability, so check up front (or handle the error) rather than assuming.
import { usePartyLayer, useSubmitTransaction } from '@partylayer/react';
import { CapabilityNotSupportedError } from '@partylayer/core';
function SubmitIfSupported({ signedTx }: { signedTx: unknown }) {
const client = usePartyLayer();
const { submitTransaction } = useSubmitTransaction();
async function onSubmit() {
const session = await client.getActiveSession();
const caps = session?.capabilitiesSnapshot ?? [];
if (!caps.includes('submitTransaction')) {
return alert('This wallet can’t submit transactions.');
}
try {
await submitTransaction({ signedTx });
} catch (e) {
if (e instanceof CapabilityNotSupportedError) {
alert('This wallet can’t submit transactions.');
}
}
}
return <button onClick={onSubmit}>Submit (if supported)</button>;
}
Try it live in Studio → “Submit a transaction” (the demo wallet supports submit; a wallet without it throws here).
⚠️ When not to use
Don’t gate on capabilities you don’t actually call — over-checking clutters the UI. And don’t rely on a stale snapshot across reconnects; re-read capabilities after the session changes.