← Back to articles
May 202610 min read

Common Web3 Frontend Development Mistakes and How to Avoid Them

The most common Web3 frontend bugs: poor wallet error handling, over-fetching RPC data, weak loading states, wrong network handling, and bad mobile UX. Practical fixes with code examples.

Web3FrontendUXBest Practices

Why Web3 frontends fail differently

Traditional frontend bugs are visual glitches or slow pages. Web3 frontend bugs cost users money — wrong network transactions, unlimited token approvals, or stale balance displays that lead to bad trades. These mistakes show up in almost every Web3 dashboard I review. Here is how to avoid them.

Poor wallet error handling

Users reject transactions constantly. Your app must handle rejection, insufficient gas, nonce conflicts, and network changes without crashing or showing raw JSON errors.

// hooks/useSafeWrite.ts
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { getWalletErrorMessage } from '@/lib/wallet-errors';

export function useSafeWrite() {
  const { writeContract, data: hash, error, isPending, reset } = useWriteContract();
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });

  const safeWrite = async (params: WriteContractParams) => {
    try {
      writeContract(params);
    } catch (err) {
      // Error surfaced via the error property
    }
  };

  return {
    write: safeWrite,
    hash,
    error: error ? getWalletErrorMessage(error) : null,
    isPending,
    isConfirming,
    isSuccess,
    reset,
  };
}

Fix: map every known wallet error to a user-friendly string. Never show "MetaMask - RPC Error: Internal JSON-RPC error."

Over-fetching on-chain data

Calling balanceOf in a loop for 50 tokens fires 50 RPC requests. Hitting eth_getLogs without block range limits times out. Fetching full transaction history on every page load burns rate limits.

// Bad: N RPC calls
for (const token of tokens) {
  const balance = await client.readContract({
    address: token.address,
    abi: erc20Abi,
    functionName: 'balanceOf',
    args: [userAddress],
  });
}

// Good: 1 RPC call via multicall
const balances = await client.multicall({
  contracts: tokens.map((token) => ({
    address: token.address,
    abi: erc20Abi,
    functionName: 'balanceOf',
    args: [userAddress],
  })),
});

Fix: batch with multicall3, paginate history, and configure React Query staleTime so data is not refetched on every mount.

Weak loading states

A blank screen during wallet connection or data loading makes users think the app is broken. Every async operation needs a visible loading indicator within 100ms.

// components/BalanceCard.tsx
export function BalanceCard({ address }: { address: string }) {
  const { data, isLoading, isFetching } = useTokenBalances(address);

  if (isLoading) return <BalanceSkeleton />;
  if (!data) return <EmptyBalance />;

  return (
    <div className="relative">
      {isFetching && <RefreshIndicator className="absolute top-2 right-2" />}
      <BalanceDisplay balances={data} />
    </div>
  );
}

Fix: skeleton loaders matching final layout dimensions. Distinguish isLoading (first fetch) from isFetching (background refresh).

Bad mobile responsiveness

DeFi users connect from mobile wallets daily. Desktop-only layouts lose the majority of wallet-native users. Tables overflow, connect buttons are too small, and WalletConnect deep links break navigation.

Fix: test at 375px width. Use horizontal scroll for tables. Full-width tap targets. Test WalletConnect flow on a real phone.

Not handling wrong networks

Users on Polygon trying to interact with an Ethereum-only contract get cryptic errors. Detect chain mismatches and offer one-click switching.

const { chainId } = useAccount();
const { switchChain } = useSwitchChain();

if (chainId !== expectedChainId) {
  return (
    <button onClick={() => switchChain({ chainId: expectedChainId })}>
      Switch to {chainName}
    </button>
  );
}

Ignoring caching

Fetching the same token metadata on every page visit wastes API credits and slows the app. React Query with proper staleTime eliminates redundant requests.

useQuery({
  queryKey: ['token-meta', address],
  queryFn: () => fetchTokenMetadata(address),
  staleTime: 3_600_000, // 1 hour — metadata rarely changes
  gcTime: 86_400_000,  // 24 hours in cache
});

Poor transaction feedback

After a user signs a transaction, silence is terrifying. Show: submitted (with explorer link) → confirming (block count) → confirmed (success message) or failed (retry option).

Bad SEO on Web3 landing pages

Single-page apps with client-only rendering do not rank. Protocol landing pages, tool pages, and documentation need server-rendered HTML with proper meta tags, sitemaps, and structured data. Use Next.js App Router with generateMetadata for every public page.

FAQ

**What is the single most impactful fix?** Add loading skeletons everywhere. It costs an hour of work and dramatically improves perceived quality.

**How do I audit my app for these mistakes?** Connect with a wallet on mobile, reject a transaction, switch to the wrong network, and load a wallet with 20+ tokens. Every failure point should have a clear UI response.

**Should I use a Web3-specific error monitoring tool?** Sentry works well. Add custom tags for wallet type, chain ID, and contract address to debug production issues.

**How do I prevent RPC key exposure?** Route all RPC calls through server-side API routes. Use environment variables without the NEXT_PUBLIC_ prefix for secrets.

Want to work together? I build Web3 dashboards and DeFi interfaces.