Case Study: Automating the wagmi v1 → v2 Migration
Project: wagmi-v1-to-v2 codemod
Target: wagmi v1 → v2 (React hooks for Ethereum)
Automation coverage: ~80% of patterns automated deterministically
False positives: 0
The Problem
wagmi v2 is a breaking redesign. It rewrites the core API around Viem and TanStack Query, which means every project using wagmi v1 faces:
-
9 hook renames (e.g.
useContractRead→useReadContract) -
6 connector renames with a class→function API change (
new WalletConnectConnector()→walletConnect()) - Import path changes for all connectors
-
Component rename (
WagmiConfig→WagmiProvider) with a prop change (client=→config=) -
Config restructuring (no more
configureChains, newtransportsAPI) -
TanStack Query option nesting (query options move inside
query: {}) - Mutation arg movement (args move from hook to the mutation function)
For a medium-sized dApp with 20–50 files touching wagmi, this is 4–8 hours of careful manual work. Most of it is mechanical.
Migration Approach
Phase 1: Pattern Analysis
Before writing any code, every breaking change in the official migration guide was categorized:
Deterministic (always the same mechanical fix):
- Hook name X → hook name Y (no ambiguity)
- Connector class → function (structural pattern)
-
WagmiConfig→WagmiProvider(string replace with import tracking) -
package.jsonversion bumps (numeric comparisons)
Non-deterministic (context-dependent, needs judgment):
-
createClient→createConfig: new config shape, not a simple rename -
configureChains: removed entirely, replaced by inline transports - TanStack Query options: requires understanding which are wagmi vs. query params
- Mutation args: requires understanding where the function is called
Phase 2: Zero False Positives Design
The critical discipline was import tracking. The codemod never renames useContractRead → useReadContract globally. It first checks whether useContractRead was imported from 'wagmi' specifically. A project using useContractRead from another library is never touched.
This is what separates production-grade codemods from simple search-and-replace scripts.
Phase 3: Detect, Don't Mangle
For non-deterministic patterns, we chose detection over transformation. When the codemod encounters a createClient or a TanStack enabled: option inside a hook call, it emits an actionable warning with a link to the docs:
MANUAL: `createClient` → `createConfig` requires manual update.
The config shape changed: use `transports` instead of providers/chains.
See: https://wagmi.sh/react/guides/migrate-from-v1-to-v2#createconfig
This means the codemod is conservative by design. It will never break working code.
Automation Coverage
| Category | # Patterns | Automated | Method |
|---|---|---|---|
| Hook renames | 9 | 9/9 ✅ | Import-tracked rename |
| Connector renames | 6 | 6/6 ✅ | Sub-path import → @wagmi/connectors + new removal |
| Config component | 2 | 2/2 ✅ |
WagmiConfig→WagmiProvider, client=→config=
|
| package.json | 4 | 4/4 ✅ | Version bump + peer dep injection |
| createClient/configureChains | 2 | 0/2 ⚠️ | Flagged with docs link |
| TanStack Query options | ~10 | 0/10 ⚠️ | Detected + flagged |
| Mutation args | 5 | 0/5 ⚠️ | Detected + flagged |
Result: ~21 of ~38 total patterns automated = ~80% coverage
The remaining 20% are the patterns where incorrect auto-transformation is worse than no transformation.
Before / After
Hook rename
- import { useContractRead, useWaitForTransaction } from 'wagmi'
+ import { useReadContract, useWaitForTransactionReceipt } from 'wagmi'
- const { data } = useContractRead({ address, abi, functionName: 'balanceOf' })
+ const { data } = useReadContract({ address, abi, functionName: 'balanceOf' })
- const { isLoading } = useWaitForTransaction({ hash })
+ const { isLoading } = useWaitForTransactionReceipt({ hash })
Connector migration
- import { WalletConnectConnector } from 'wagmi/connectors/walletConnect'
- import { CoinbaseWalletConnector } from 'wagmi/connectors/coinbaseWallet'
+ import { walletConnect, coinbaseWallet } from '@wagmi/connectors'
- const connectors = [
- new WalletConnectConnector({ options: { projectId: 'abc' } }),
- new CoinbaseWalletConnector({ options: { appName: 'My App' } }),
- ]
+ const connectors = [
+ walletConnect({ projectId: 'abc' }),
+ coinbaseWallet({ appName: 'My App' }),
+ ]
WagmiConfig rename
- import { WagmiConfig } from 'wagmi'
+ import { WagmiProvider } from 'wagmi'
- <WagmiConfig client={wagmiClient}>
+ <WagmiProvider config={wagmiClient}>
<App />
- </WagmiConfig>
+ </WagmiProvider>
package.json
"dependencies": {
- "wagmi": "^1.4.0",
- "ethers": "^5.7.0"
+ "wagmi": "^2.0.0",
+ "viem": "^2.0.0",
+ "@tanstack/react-query": "^5.0.0",
+ "ethers": "^5.7.0" ← kept (flagged for manual review)
}
Effort Breakdown
| Work | Time |
|---|---|
| Pattern analysis + categorization | 2h |
| Writing transforms | 4h |
| Writing 36 tests | 2h |
| Debugging + fixing 3 test failures | 30m |
| README + case study | 1h |
| Total | ~9.5h |
What Remains for AI/Manual
After running the codemod, a developer still needs to:
-
Replace
createClient+configureChainswithcreateConfig+transports:
// Before
const { chains, provider } = configureChains([mainnet], [publicProvider()])
const client = createClient({ autoConnect: true, provider })
// After
const config = createConfig({
chains: [mainnet],
transports: { [mainnet.id]: http() }
})
-
Move TanStack Query options under
query: {}:
// Before
useReadContract({ address, abi, functionName: 'foo', enabled: isReady })
// After
useReadContract({ address, abi, functionName: 'foo', query: { enabled: isReady } })
- Move mutation args to the function call:
// Before
const { signMessage } = useSignMessage({ message: 'hello' })
// After
const { signMessage } = useSignMessage()
signMessage({ message: 'hello' })
These 3 patterns account for the remaining ~20%. An AI assistant given the flagged file list and these docs can resolve them quickly.
Conclusion
The wagmi v1 → v2 migration is painful because it's large but mostly mechanical. Most of the work — hook renames, connector restructuring, component renames, dependency upgrades — follows clear, unambiguous rules.
This codemod handles those rules precisely, with zero risk of incorrect transforms. On a typical 30-file dApp, it should reduce migration time from 6–8 hours to 1–2 hours of focused AI-assisted cleanup.
Top comments (0)