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

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка фронтенду dApp на React
Середній
~1-2 тижні
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1306
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1218
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    920
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1147
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    610
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    885

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

Головна ошибка при розробці фронтенда dApp — переносити архітектурні паттерни з звичайних веб-додатків. У dApp немає «користувача» в класичному сенсі, немає сесій, немає авторизації через сервер. Є гаманець, є підписи, є трансакції які можуть зависнути на годину. Це принципово змінює як влаштовано state management та UX.

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

wagmi v2 — це не просто обёртка над viem. Це opinionated шар для 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 → включена в блок → 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 дій (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 провайдери, не потрібні окремі пакети для кожного гаманця