Разработка фронтенда dApp на React

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка фронтенда dApp на React
Средняя
~1-2 недели
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

Разработка фронтенда dApp на React

Главная ошибка при разработке dApp фронтенда — переносить архитектурные паттерны из обычных веб-приложений. В dApp нет «пользователя» в классическом смысле, нет сессий, нет авторизации через сервер. Есть кошелёк, есть подписи, есть транзакции которые могут подвиснуть на час. Это принципиально меняет как устроено state management и UX.

Стек: почему wagmi + viem, а не ethers.js напрямую

wagmi v2 — это не просто обёртка над viem. Это опinionated слой для React с:

  • Автоматическим управлением connection state (connected/disconnecting/reconnecting)
  • Cache invalidation после транзакций (через TanStack Query под капотом)
  • SSR-совместимостью из коробки
  • Type-safe ABI encoding через viem

viem заменил ethers.js v5 как де-факто стандарт для low-level операций. Лучший TypeScript support, tree-shakeable, значительно меньше bundle size.

// wagmi v2 конфигурация
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),
  },
});

Управление состоянием транзакций

Транзакция в EVM — это не HTTP запрос. Она проходит стадии: pending в mempool → included в блок → confirmed (N confirmations). Пользователь должен понимать, что происходит на каждом этапе.

Паттерн для tracking транзакций:

import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";

function MintButton() {
  const { writeContract, data: hash, isPending } = useWriteContract();
  
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
    hash,
    confirmations: 2, // ждём 2 блока
  });

  // Три состояния UI:
  // isPending = транзакция отправлена, ждём подписи в кошельке
  // isConfirming = подписана, ждём включения в блок
  // isSuccess = подтверждена
  
  return (
    <button disabled={isPending || isConfirming}>
      {isPending ? "Подписываем..." : isConfirming ? "Ждём блок..." : "Mint"}
    </button>
  );
}

Читаем данные с контракта: useReadContract и multicall

Для чтения нескольких значений используем useReadContracts с multicall — это батчит запросы в один RPC call:

import { useReadContracts } from "wagmi";

const { data } = useReadContracts({
  contracts: [
    { ...tokenContract, functionName: "balanceOf", args: [userAddress] },
    { ...tokenContract, functionName: "totalSupply" },
    { ...stakingContract, functionName: "pendingRewards", args: [userAddress] },
  ],
  // Автоматически использует multicall3 если доступен
});

Важно: wagmi использует TanStack Query для кеширования. По умолчанию данные считаются свежими 4 секунды. Для DeFi dashboards с fast-changing data снижаем staleTime до 0 и включаем refetchInterval.

Обработка ошибок: типичные случаи

User rejected — пользователь нажал Cancel в MetaMask. error.code === 4001. Просто закрываем модалку, не показываем error toast.

Insufficient fundserror.code === -32000. Показываем понятное сообщение с суммой, которой не хватает.

Revert с reason string — контракт выбросил require("Insufficient allowance"). Парсим через viem:

import { ContractFunctionRevertedError } from "viem";

if (error instanceof ContractFunctionRevertedError) {
  const reason = error.data?.errorName ?? error.shortMessage;
  // Показываем reason пользователю
}

Stuck transaction — транзакция в pending > 5 минут. Нужен UI для speed up (replace с тем же nonce и +10% gas) или cancel (отправить 0 ETH себе с тем же nonce). wagmi не предоставляет это из коробки, пишем через viem sendTransaction с явным nonce.

Wallet UX: распространённые ошибки

Не проверяем chainId — пользователь подключён к mainnet, а dApp работает на Arbitrum. Без явной проверки и switchChain call пользователь увидит cryptic error. Используем хук useChainId и компонент-гард:

import { useChainId, useSwitchChain } from "wagmi";

function ChainGuard({ requiredChainId, children }) {
  const chainId = useChainId();
  const { switchChain } = useSwitchChain();
  
  if (chainId !== requiredChainId) {
    return (
      <button onClick={() => switchChain({ chainId: requiredChainId })}>
        Переключить сеть
      </button>
    );
  }
  return children;
}

Hydration mismatch при SSR — wallet state на сервере неизвестен, на клиенте — подключён. Next.js выбросит hydration error. Решение: оборачиваем wallet-dependent компоненты в dynamic import с ssr: false, или используем mounted state.

EIP-712 подписи (typed data)

Для off-chain actions (permit, gasless mint, snapshot voting) используем typed data signatures вместо обычных транзакций:

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") },
});

Структура проекта

src/
  abi/           # JSON ABI файлы контрактов
  config/        # wagmi config, chain configs
  contracts/     # typed contract instances (viem getContract)
  hooks/         # кастомные хуки для каждого контракта
    useStaking.ts
    useTokenBalance.ts
  components/    # UI компоненты
  lib/           # утилиты (formatUnits wrappers, address shortener)

Генерация типов из ABI через wagmi CLI (wagmi generate) — избавляет от ручного написания типов и автоматически генерирует хуки.

Performance: что важно в dApp

  • Bundle size: viem + wagmi ≈ 60KB gzipped. Избегаем дублирования (не тащим ethers.js если уже есть viem)
  • RPC rate limits: не делаем запросы в useEffect без debounce, используем wagmi кеш
  • Suspense: TanStack Query под wagmi поддерживает Suspense — используем для скелетонов вместо ручных isLoading checks
  • Wallet detection: injected() коннектор находит все injected providers, не нужны отдельные пакеты для каждого кошелька