viem + wagmi: где заканчивается viem и начинается wagmi
Разведёшь viem и wagmi: один и тот же balanceOf через publicClient.readContract и через useReadContract.
Два слоя одного стека
viem — TypeScript SDK для Ethereum: клиенты, readContract, writeContract, multicall, утилиты. Работает в Node, скриптах, бэкенде, React — где угодно.
wagmi — React-хуки и провайдеры поверх viem: кэш, рефетч, статус загрузки, коннект кошелька. Внутри хуков те же действия viem.
Не «viem или wagmi». В React-dApp обычно оба: wagmi для UI-слоя, viem — типы, ABI, chains и иногда прямые вызовы вне React.
Одна задача: balanceOf USDC
viem (скрипт / любой runtime)
import { createPublicClient, http, formatUnits } from "viem";
import { mainnet } from "viem/chains";
const publicClient = createPublicClient({
chain: mainnet,
transport: http("https://ethereum.publicnode.com"),
});
const usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const holder = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
const erc20Abi = [
{
type: "function",
name: "balanceOf",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ type: "uint256" }],
},
{
type: "function",
name: "decimals",
stateMutability: "view",
inputs: [],
outputs: [{ type: "uint8" }],
},
] as const;
const [rawBalance, decimals] = await Promise.all([
publicClient.readContract({
address: usdcAddress,
abi: erc20Abi,
functionName: "balanceOf",
args: [holder],
}),
publicClient.readContract({
address: usdcAddress,
abi: erc20Abi,
functionName: "decimals",
}),
]);
console.log(formatUnits(rawBalance, decimals));wagmi (React-компонент)
Те же address / abi / functionName / args. Добавляются кэш и состояние запроса.
npm install wagmi viem @tanstack/react-query// config.ts
import { createConfig, http } from "wagmi";
import { mainnet } from "wagmi/chains";
export const config = createConfig({
chains: [mainnet],
transports: {
[mainnet.id]: http("https://ethereum.publicnode.com"),
},
});// main.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider } from "wagmi";
import { config } from "./config";
import { UsdcBalance } from "./UsdcBalance";
const queryClient = new QueryClient();
export function App() {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<UsdcBalance />
</QueryClientProvider>
</WagmiProvider>
);
}// UsdcBalance.tsx
import { useReadContract } from "wagmi";
import { formatUnits } from "viem";
const usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const holder = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
const erc20Abi = [
{
type: "function",
name: "balanceOf",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ type: "uint256" }],
},
{
type: "function",
name: "decimals",
stateMutability: "view",
inputs: [],
outputs: [{ type: "uint8" }],
},
] as const;
export function UsdcBalance() {
const balance = useReadContract({
address: usdcAddress,
abi: erc20Abi,
functionName: "balanceOf",
args: [holder],
});
const decimals = useReadContract({
address: usdcAddress,
abi: erc20Abi,
functionName: "decimals",
});
if (balance.isLoading || decimals.isLoading) {
return <p>Загрузка…</p>;
}
if (balance.error || decimals.error || balance.data == null || decimals.data == null) {
return <p>Не удалось прочитать баланс</p>;
}
return <p>{formatUnits(balance.data, decimals.data)} USDC</p>;
}useReadContract под капотом вызывает тот же readContract viem. Wagmi добавляет: isLoading / isError / data, рефетч, дедуп запросов через TanStack Query.
Как это работает
| Слой | Отвечает за |
|---|---|
| viem | RPC, ABI, кодирование calldata, клиенты, типы |
| wagmi | React-хуки, коннект кошелька, кэш, ре-рендер UI |
| TanStack Query | кэш и статусы запросов, которые использует wagmi |
Типичные соответствия:
| viem | wagmi |
|---|---|
publicClient.readContract | useReadContract |
publicClient.multicall | useReadContracts |
walletClient.writeContract | useWriteContract |
createWalletClient + requestAddresses | useConnect / useAccount |
waitForTransactionReceipt | useWaitForTransactionReceipt |
createConfig + http() — это конфиг транспортов/сетей для wagmi. По сути те же chain + transport, что ты задавал у createPublicClient.
Импорты mainnet можно брать из wagmi/chains или viem/chains — это одни и те же chain-объекты экосистемы wevm.
Если ты знаешь ethers.js
| Старый React-стек | Современный |
|---|---|
ethers + свой useEffect + ручной стейт | wagmi-хуки |
BrowserProvider в компоненте | useConnect / connectors |
вызов contract.balanceOf в useEffect | useReadContract |
| нет встроенного кэша | TanStack Query через wagmi |
RainbowKit и похожие UI-kit'ы для кошельков тоже сидят на wagmi → viem.
Частые ошибки
«Нужно выбрать: учу viem или wagmi» → для React-вакансий нужны оба. Без viem не поймёшь, что делают хуки; без wagmi будешь изобретать кэш и коннект кошелька.
Вызов createPublicClient в теле каждого компонента → для UI бери хуки wagmi. Прямой viem-клиент — в скриптах, API-роутах, тестах или редких местах вне React-дерева.
Забыли QueryClientProvider → wagmi v2 без TanStack Query не заведёт хуки чтения. Оба провайдера обязательны.
Что дальше
Раздел viem на этом закрыт. Имеет смысл вернуться к практике:
- Чтение данных из контракта: readContract — база, которую оборачивает
useReadContract - Запись в контракт: writeContract и simulateContract — то, что станет
useWriteContract+ симуляция - Мультивызовы: multicall — аналог
useReadContractsна голом viem