Best Practices
Essential patterns for optimal performance, type safety, and maintainability
Type Safety
// ✅ Good: Proper typing
const args: [string, bigint, boolean] = [address, amount, isEnabled];
const contractAddress = config.contractAddress as `0x${string}`;
const method = 'balanceOf' as const;
// ✅ Use contract factories
import { VOT3__factory } from '@vechain/vechain-kit/contracts';
const abi = VOT3__factory.abi;
// Avoid: Manual ABI definitions
const functionAbi = contractAbi.find((e) => e.name === "delegates");
Query Optimization
// ✅ Good: Conditional enablement
return useCallClause({
abi,
address: contractAddress,
method: 'getData',
args: [userAddress],
queryOptions: {
enabled: !!contractAddress && !!userAddress && isConnected,
},
});
// ✅ Good: Configure caching
queryOptions: {
staleTime: 30000, // 30 seconds for price data
refetchInterval: 60000, // Refetch every minute
}
Data Transformation
// ✅ Good: Transform in select
return useCallClause({
abi: VOT3__factory.abi,
address: contractAddress,
method: 'convertedB3trOf' as const,
args: [address ?? ''],
queryOptions: {
enabled: !!address,
select: (data) => ({
balance: ethers.formatEther(data[0]),
formatted: humanNumber(ethers.formatEther(data[0])),
}),
},
});
// Avoid: Transform in component
const transformedData = useMemo(() => ({
balance: data?.[0]?.toString(),
}), [data]); // Causes re-renders
Error Handling
// ✅ Good: Comprehensive error handling
if (error) {
if (error.message.includes('reverted')) {
return <div>Contract call failed. Check parameters.</div>;
}
if (error.message.includes('network')) {
return <div>Network error. <button onClick={refetch}>Retry</button></div>;
}
return <div>Error: {error.message}</div>;
}
// ✅ Good: Retry logic
queryOptions: {
retry: (failureCount, error) => {
if (error.message.includes('reverted')) return false;
return failureCount < 3;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
}
Query Key Management
// ✅ Good: Use getCallClauseQueryKey (without args)
export const getCurrentAllocationsRoundIdQueryKey = (
address: string,
networkType: NETWORK_TYPE
) =>
getCallClauseQueryKey({
abi: XAllocationVoting__factory.abi,
address: getConfig(networkType).contractAddress as `0x${string}`,
method: 'currentRoundId' as const,
});
// ✅ Good: Use getCallClauseQueryKeyWithArgs
export const getTokenBalanceQueryKey = (
address: string,
networkType: NETWORK_TYPE
) =>
getCallClauseQueryKeyWithArgs({
abi: VOT3__factory.abi,
address: getConfig(networkType).contractAddress as `0x${string}`,
method: 'balanceOf' as const,
args: [address],
});
// ✅ Good: Query invalidation after transactions
const mutation = useBuildTransaction({
clauseBuilder: buildClauses,
onTxConfirmed: () => {
queryClient.invalidateQueries({ queryKey: getTokenBalanceQueryKey(userAddress, networkType) });
},
});
Performance Tips
// ✅ Good: Memoize expensive calculations
const queryKey = useMemo(() =>
getTokenBalanceQueryKey(userAddress, tokenAddress),
[userAddress, tokenAddress]
);
// ✅ Good: Batch multiple calls
const results = await executeMultipleClausesCall({
thor,
calls: addresses.map((address) => ({
abi: ERC20__factory.abi,
functionName: 'balanceOf',
address: address as `0x${string}`,
args: [userAddress],
})),
});
Security
// ✅ Good: Input validation
if (!isAddress(recipient)) {
throw new Error('Invalid recipient address');
}
const amountBN = BigInt(amount);
if (amountBN <= 0n) {
throw new Error('Amount must be positive');
}
// ✅ Good: Safe BigInt handling
const formatTokenAmount = (amount: bigint, decimals: number): string => {
try {
return ethers.formatUnits(amount, decimals);
} catch (error) {
return '0';
}
};
Single Contract Call Pattern
import { VOT3__factory } from '@vechain/vechain-kit/contracts';
import { useCallClause, getCallClauseQueryKeyWithArgs } from '@vechain/vechain-kit';
const abi = VOT3__factory.abi;
const method = 'convertedB3trOf' as const;
export const useTokenBalance = (address?: string) => {
const { network } = useVeChainKitConfig();
const contractAddress = getConfig(network.type).contractAddress as `0x${string}`;
return useCallClause({
abi,
address: contractAddress,
method,
args: [address ?? ''],
queryOptions: {
enabled: !!address,
select: (data) => ({
balance: ethers.formatEther(data[0]),
formatted: humanNumber(ethers.formatEther(data[0])),
}),
},
});
};
Multiple Contract Calls Pattern
import { useQuery } from '@tanstack/react-query';
import { executeMultipleClausesCall } from '@vechain/vechain-kit';
export const useMultipleTokenData = (addresses: string[]) => {
const thor = useThor();
return useQuery({
queryKey: ['MULTIPLE_TOKENS', addresses],
queryFn: async () => {
const results = await executeMultipleClausesCall({
thor,
calls: addresses.map((address) => ({
abi: ERC20__factory.abi,
functionName: 'balanceOf',
address: address as `0x${string}`,
args: [userAddress],
})),
});
return addresses.map((address, index) => ({
address,
balance: ethers.formatEther(results[index][0]),
}));
},
enabled: !!addresses.length,
});
};
Transaction Building Pattern
import { useBuildTransaction, useWallet } from '@vechain/vechain-kit';
export const useTokenTransfer = () => {
const { account } = useWallet();
const thor = useThor();
return useBuildTransaction({
clauseBuilder: (recipient: string, amount: string) => {
if (!account?.address) return [];
const { clause } = thor.contracts
.load(tokenAddress, ERC20__factory.abi)
.clause.transfer(recipient, ethers.parseEther(amount));
return [{
...clause,
comment: `Transfer ${amount} tokens to ${recipient}`,
}];
},
onTxConfirmed: () => {
queryClient.invalidateQueries({ queryKey: ['TOKEN_BALANCE'] });
},
});
};
Multi-Clause Transactions
const useApproveAndSwap = () => {
const { account } = useWallet();
const thor = useThor();
return useBuildTransaction({
clauseBuilder: (tokenAddress: string, amount: string) => {
if (!account?.address) return [];
return [
// Approve
{
...thor.contracts
.load(tokenAddress, ERC20__factory.abi)
.clause.approve(swapAddress, ethers.parseEther(amount)).clause,
comment: 'Approve token spending',
},
// Swap
{
...thor.contracts
.load(swapAddress, SwapContract__factory.abi)
.clause.swap(tokenAddress, ethers.parseEther(amount)).clause,
comment: 'Execute swap',
},
];
},
});
};
Error Handling
const useContractCall = (address: string) => {
return useCallClause({
abi: ContractABI,
address: address as `0x${string}`,
method: 'getData',
args: [],
queryOptions: {
enabled: !!address,
retry: (failureCount, error) => {
if (error.message.includes('reverted')) return false;
return failureCount < 3;
},
},
});
};
Last updated
Was this helpful?