dApp Frontend Development with React

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
dApp Frontend Development with React
Medium
~1-2 weeks
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

dApp Frontend Development with React

The main mistake when developing a dApp frontend — transferring architectural patterns from regular web applications. In a dApp, there's no "user" in the classical sense, no sessions, no server-based authorization. There's a wallet, there are signatures, there are transactions that can hang for an hour. This fundamentally changes how state management and UX are organized.

Stack: Why wagmi + viem, Not ethers.js Directly

wagmi v2 is not just a wrapper over viem. It's an opinionated layer for React with:

  • Automatic connection state management (connected/disconnecting/reconnecting)
  • Cache invalidation after transactions (via TanStack Query under the hood)
  • SSR compatibility out of the box
  • Type-safe ABI encoding through viem

viem replaced ethers.js v5 as the de-facto standard for low-level operations. Better TypeScript support, tree-shakeable, significantly smaller bundle size.

// wagmi v2 configuration
import { createConfig, http } from "wagmi";
import { mainnet, arbitrum, optimism } from "wagmi/chains";
import { injected, metaMask, coinbaseWallet } from "wagmi/connectors";

export const config = createConfig({
  chains: [mainnet, arbitrum, optimism],
  connectors: [
    injected(),
    metaMask(),
    coinbaseWallet({ appName: "MyDApp" }),
  ],
  transports: {
    [mainnet.id]: http(process.env.NEXT_PUBLIC_RPC_MAINNET),
    [arbitrum.id]: http(process.env.NEXT_PUBLIC_RPC_ARBITRUM),
    [optimism.id]: http(process.env.NEXT_PUBLIC_RPC_OPTIMISM),
  },
});

Managing Transaction State

A transaction in EVM is not an HTTP request. It goes through stages: pending in mempool → included in block → confirmed (N confirmations). User should understand what's happening at each stage.

Pattern for tracking transactions:

import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";

function MintButton() {
  const { writeContract, data: hash, isPending } = useWriteContract();
  
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
    hash,
    confirmations: 2, // wait for 2 blocks
  });

  // Three UI states:
  // isPending = transaction sent, waiting for signature in wallet
  // isConfirming = signed, waiting for inclusion in block
  // isSuccess = confirmed
  
  return (
    <button disabled={isPending || isConfirming}>
      {isPending ? "Signing..." : isConfirming ? "Waiting for block..." : "Mint"}
    </button>
  );
}

Reading Contract Data: useReadContract and Multicall

To read multiple values use useReadContracts with multicall — this batches requests into one RPC call:

import { useReadContracts } from "wagmi";

const { data } = useReadContracts({
  contracts: [
    { ...tokenContract, functionName: "balanceOf", args: [userAddress] },
    { ...tokenContract, functionName: "totalSupply" },
    { ...stakingContract, functionName: "pendingRewards", args: [userAddress] },
  ],
  // Automatically uses multicall3 if available
});

Important: wagmi uses TanStack Query for caching. By default, data is considered fresh for 4 seconds. For DeFi dashboards with fast-changing data, lower staleTime to 0 and enable refetchInterval.

Error Handling: Typical Cases

User rejected — user clicked Cancel in MetaMask. error.code === 4001. Just close the modal, don't show error toast.

Insufficient fundserror.code === -32000. Show a clear message with the missing amount.

Revert with reason string — contract threw require("Insufficient allowance"). Parse through viem:

import { ContractFunctionRevertedError } from "viem";

if (error instanceof ContractFunctionRevertedError) {
  const reason = error.data?.errorName ?? error.shortMessage;
  // Show reason to user
}

Stuck transaction — transaction pending > 5 minutes. Need UI for speed up (replace with same nonce and +10% gas) or cancel (send 0 ETH to self with same nonce). wagmi doesn't provide this out of the box, write via viem sendTransaction with explicit nonce.

Wallet UX: Common Mistakes

Don't check chainId — user is connected to mainnet, but dApp runs on Arbitrum. Without explicit check and switchChain call, user sees cryptic error. Use useChainId hook and guard component:

import { useChainId, useSwitchChain } from "wagmi";

function ChainGuard({ requiredChainId, children }) {
  const chainId = useChainId();
  const { switchChain } = useSwitchChain();
  
  if (chainId !== requiredChainId) {
    return (
      <button onClick={() => switchChain({ chainId: requiredChainId })}>
        Switch Network
      </button>
    );
  }
  return children;
}

Hydration mismatch with SSR — wallet state unknown on server, connected on client. Next.js throws hydration error. Solution: wrap wallet-dependent components in dynamic import with ssr: false, or use mounted state.

EIP-712 Signatures (Typed Data)

For off-chain actions (permit, gasless mint, snapshot voting) use typed data signatures instead of regular transactions:

import { useSignTypedData } from "wagmi";

const { signTypedData } = useSignTypedData();

const signature = await signTypedData({
  domain: { name: "MyProtocol", version: "1", chainId: 1, verifyingContract },
  types: { Order: [{ name: "tokenId", type: "uint256" }, { name: "price", type: "uint256" }] },
  primaryType: "Order",
  message: { tokenId: 42n, price: parseEther("0.1") },
});

Project Structure

src/
  abi/           # JSON ABI files for contracts
  config/        # wagmi config, chain configs
  contracts/     # typed contract instances (viem getContract)
  hooks/         # custom hooks for each contract
    useStaking.ts
    useTokenBalance.ts
  components/    # UI components
  lib/           # utilities (formatUnits wrappers, address shortener)

Generating types from ABI through wagmi CLI (wagmi generate) — eliminates manual type writing and automatically generates hooks.

Performance: What Matters in dApp

  • Bundle size: viem + wagmi ≈ 60KB gzipped. Avoid duplication (don't pull ethers.js if you already have viem)
  • RPC rate limits: don't make requests in useEffect without debounce, use wagmi cache
  • Suspense: TanStack Query under wagmi supports Suspense — use for skeletons instead of manual isLoading checks
  • Wallet detection: injected() connector finds all injected providers, no need for separate packages for each wallet