BR Academy
0%
Web3 Front-End Foundations·Lesson 1 of 25·35 min

Project Setup with Vite, wagmi, viem

The concept

Every dApp needs three things before a single wallet button exists: a fast front-end build (Vite), a wallet/chain-interaction layer (wagmi), and a low-level Ethereum client underneath it (viem). wagmi is a set of React hooks; viem is the typed library that actually talks to RPC nodes and wallets underneath those hooks. You configure wagmi once, at the top of your app, with a list of chains you support (for this course: Ethereum mainnet and the Sepolia testnet), a transport (how to reach an RPC node for each chain), and connectors (how a wallet attaches). Secrets — your RPC URLs, especially paid provider keys — go in environment variables, never hardcoded, because the client bundle is public and anyone can read it.

Vite matters more than it looks like it should. Create-React-App-era tooling bundles your entire dependency graph before serving a single page, and a wagmi + viem + React stack pulls in enough transitive dependencies that cold starts become painful. Vite instead serves native ES modules during development and only bundles for production, so the dev server boots in under a second and hot reloads are near instant — important because you will restart this server dozens of times per session while wiring wallet state. @tanstack/react-query sits underneath wagmi's hooks as its caching and refetching engine; you rarely call it directly, but every useReadContract and useBalance hook you write later is a thin wrapper around a query-cache entry, which is why wagmi requires a QueryClientProvider at the root even though you never see "query" in your own code.

The three layers also map to three different failure modes you'll meet repeatedly: a Vite error means your build or import graph is broken; a wagmi error (like "hook called outside provider") means your React tree is wired wrong; a viem error (like a bad RPC URL or malformed ABI) means the chain-communication layer itself is misconfigured. Learning to tell these apart from the stack trace alone will save you real debugging time in every later module.

Package manager choice matters less for correctness than consistency: whichever of npm, pnpm, or yarn you pick, commit the lockfile so every teammate and CI run installs byte-identical dependency trees — a wagmi/viem version mismatch between two machines is a common source of "works on my machine" wallet bugs that don't look anything like a wallet bug in the stack trace. TypeScript's strict mode, already on by default in Vite's react-ts template, is worth keeping enabled for the entire course: viem's types describe exact ABI argument and return shapes, and loosening strictness quietly reopens the same class of untyped mistakes this stack exists to catch at compile time rather than at runtime, in production, with a user's wallet open.

Worked example

Scaffold the app, then install the web3 layer:

npm create vite@latest dapp -- --template react-ts
cd dapp
npm install wagmi viem @tanstack/react-query

Create src/wagmi.config.ts:

import { http, createConfig } from 'wagmi'
import { mainnet, sepolia } from 'wagmi/chains'

export const config = createConfig({
  chains: [mainnet, sepolia],
  transports: {
    [mainnet.id]: http(import.meta.env.VITE_MAINNET_RPC_URL),
    [sepolia.id]: http(import.meta.env.VITE_SEPOLIA_RPC_URL),
  },
})

Note the VITE_ prefix — Vite only exposes environment variables to client code when they start with that prefix, as a deliberate guardrail against accidentally shipping unprefixed server secrets into the browser bundle. Add both values to a local .env file that stays out of version control.

Wrap the app in src/main.tsx:

import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { config } from './wagmi.config'

const queryClient = new QueryClient()

createRoot(document.getElementById('root')!).render(
  <WagmiProvider config={config}>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </WagmiProvider>,
)

A minimal App.tsx proves the tree works before any wallet logic exists:

import { useAccount } from 'wagmi'

export default function App() {
  const { isConnected } = useAccount()
  return <p>{isConnected ? 'Connected' : 'Not connected'}</p>
}

Running the app should print "Not connected" with no console errors — that's your proof the provider tree is wired correctly. If you instead see a blank screen, check the browser console first: a missing QueryClientProvider or a config imported before it's exported will throw before React ever paints.

Try it

Add a third chain (Polygon or Base) to wagmi.config.ts with its own RPC transport and env variable. Confirm the app still boots, then log chainId from useChainId() to the console on load. Finally, deliberately remove WagmiProvider from main.tsx, re-render, and read the error wagmi throws — this "hook called outside provider" error recurs constantly in later modules, so learn to recognize it now rather than debugging it under pressure during the minting-page work in Module 6. As a stretch step, open your browser's network tab, reload, and identify the single RPC request Vite's dev server makes on load versus the request your own useChainId() call triggers — distinguishing framework traffic from your own chain calls is a habit that pays off when you start debugging rate limits in Module 3.

Recap

  • wagmi gives you React hooks; viem does the actual RPC/wallet talking underneath, with react-query caching results in between.
  • Chains, transports, and connectors are configured once in createConfig and provided at the app root via WagmiProvider + QueryClientProvider.
  • RPC URLs live in VITE_-prefixed environment variables because client bundles are public and unprefixed vars are intentionally hidden from the browser.

Resources