Custom Hooks
This guide covers creating custom hooks that compose the built-in hooks.Composing Hooks
usePrivacyWallet
Combine auth and balance data:Copy
import { useAuth, useBalances } from '@testinprod-io/privacy-boost-react';
function usePrivacyWallet() {
const auth = useAuth();
const balances = useBalances();
return {
// Auth state
isReady: auth.isAuthenticated,
address: auth.address,
privacyAddress: auth.privacyAddress,
// Auth methods
connect: auth.authenticateInjected,
disconnect: auth.logout,
// Balance data
balances: balances.balances,
totalShielded: balances.totalShielded,
loading: balances.loading,
};
}
// Usage
function Wallet() {
const { isReady, connect, disconnect, balances } = usePrivacyWallet();
// ...
}
useDepositFlow
Encapsulate deposit flow with state:Copy
import { useState, useCallback } from 'react';
import { useVault, useAuth } from '@testinprod-io/privacy-boost-react';
interface UseDepositFlowResult {
deposit: (tokenAddress: string, amount: string) => Promise<void>;
loading: boolean;
status: string;
error: string | null;
txHash: string | null;
}
function useDepositFlow(): UseDepositFlowResult {
const { deposit: vaultDeposit } = useVault();
const { isAuthenticated } = useAuth();
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState('');
const [error, setError] = useState<string | null>(null);
const [txHash, setTxHash] = useState<string | null>(null);
const deposit = useCallback(async (tokenAddress: string, amount: string) => {
if (!isAuthenticated) {
setError('Please connect wallet first');
return;
}
setLoading(true);
setError(null);
setTxHash(null);
setStatus('Starting deposit...');
try {
const result = await vaultDeposit({
tokenAddress: tokenAddress as `0x${string}`,
amount,
onProgress: ({ step, message, txHash }) => {
setStatus(message);
if (txHash) setTxHash(txHash);
},
});
setTxHash(result.txHash);
setStatus('Deposit complete!');
} catch (err: any) {
if (err.code === 'WALLET_USER_REJECTED') {
setStatus('');
} else {
setError(err.message);
setStatus('');
}
} finally {
setLoading(false);
}
}, [vaultDeposit, isAuthenticated]);
return { deposit, loading, status, error, txHash };
}
// Usage
function DepositForm() {
const { deposit, loading, status, error, txHash } = useDepositFlow();
return (
<div>
<button onClick={() => deposit('0x...', '1.0')}>
Deposit
</button>
{loading && <p>{status}</p>}
{error && <p className="error">{error}</p>}
{txHash && <a href={`https://etherscan.io/tx/${txHash}`}>View Tx</a>}
</div>
);
}
useTokenInfo
Fetch and cache token metadata:Copy
import { useState, useEffect } from 'react';
import { usePrivacyBoost } from '@testinprod-io/privacy-boost-react';
interface TokenInfo {
address: string;
symbol: string;
name: string;
decimals: number;
balance: bigint;
formattedBalance: string;
}
function useTokenInfo(tokenAddress: string): {
token: TokenInfo | null;
loading: boolean;
error: Error | null;
} {
const sdk = usePrivacyBoost();
const [token, setToken] = useState<TokenInfo | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!sdk || !tokenAddress) return;
async function fetchToken() {
setLoading(true);
try {
const [metadata, balance] = await Promise.all([
sdk.vault.getToken(tokenAddress),
sdk.vault.getBalance(tokenAddress),
]);
setToken({
address: metadata.address,
symbol: metadata.symbol,
name: metadata.name,
decimals: metadata.decimals,
balance,
formattedBalance: sdk.vault.formatAmount(balance, metadata.decimals),
});
} catch (err: any) {
setError(err);
} finally {
setLoading(false);
}
}
fetchToken();
}, [sdk, tokenAddress]);
return { token, loading, error };
}
useTransferForm
Form state management for transfers:Copy
import { useState, useCallback } from 'react';
import { useVault, useContacts } from '@testinprod-io/privacy-boost-react';
import { isValidPrivacyAddress } from '@testinprod-io/privacy-boost';
function useTransferForm(tokenAddress: string) {
const { send } = useVault();
const { contacts, findByAddress } = useContacts();
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Validation
const isValidRecipient = isValidPrivacyAddress(recipient);
const isValidAmount = amount !== '' && parseFloat(amount) > 0;
const canSubmit = isValidRecipient && isValidAmount && !loading;
// Get contact name if exists
const recipientContact = findByAddress(recipient);
const recipientName = recipientContact?.name || null;
const submit = useCallback(async () => {
if (!canSubmit) return;
setLoading(true);
setError(null);
try {
await send({
to: recipient,
tokenAddress: tokenAddress as `0x${string}`,
amount,
});
// Reset form on success
setRecipient('');
setAmount('');
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
}, [send, recipient, tokenAddress, amount, canSubmit]);
return {
// State
recipient,
amount,
loading,
error,
// Setters
setRecipient,
setAmount,
// Validation
isValidRecipient,
isValidAmount,
canSubmit,
recipientName,
// Actions
submit,
reset: () => {
setRecipient('');
setAmount('');
setError(null);
},
};
}
usePollingBalance
Auto-refresh balances:Copy
import { useEffect, useRef } from 'react';
import { useVault, useBalances } from '@testinprod-io/privacy-boost-react';
function usePollingBalance(intervalMs = 30000) {
const { syncAllBalances } = useVault();
const { balances, loading } = useBalances();
const intervalRef = useRef<number | null>(null);
useEffect(() => {
// Initial sync
syncAllBalances();
// Set up polling
intervalRef.current = window.setInterval(() => {
syncAllBalances();
}, intervalMs);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [syncAllBalances, intervalMs]);
return { balances, loading };
}
Best Practices
1. Handle Loading States
Copy
function useSafeOperation<T>(operation: () => Promise<T>) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [data, setData] = useState<T | null>(null);
const execute = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await operation();
setData(result);
return result;
} catch (err: any) {
setError(err);
throw err;
} finally {
setLoading(false);
}
}, [operation]);
return { execute, loading, error, data };
}
2. Memoize Expensive Computations
Copy
function useFormattedBalances() {
const { balances } = useBalances();
return useMemo(() =>
balances
.filter(b => b.shielded > 0n)
.sort((a, b) => Number(b.shielded - a.shielded))
.map(b => ({
...b,
displayAmount: `${b.formattedShielded} ${b.symbol}`,
})),
[balances]
);
}
3. Clean Up Effects
Copy
function useAsyncEffect(effect: () => Promise<void>, deps: any[]) {
useEffect(() => {
let cancelled = false;
effect().catch((err) => {
if (!cancelled) {
console.error(err);
}
});
return () => {
cancelled = true;
};
}, deps);
}