Wagmi Development
Full-stack patterns for adding Wagmi features. This skill covers Viem-based actions only (not Wagmi config actions).
Layer Overview
- Core Action (
packages/core/src/actions/) - Base functionality wrapping Viem
- Query Options (
packages/core/src/query/) - TanStack Query integration
- Framework Bindings - React (
packages/react/src/hooks/), Vue (packages/vue/src/composables/)
1. Core Action
Structure
ts
1import {
2 type MyActionErrorType as viem_MyActionErrorType,
3 type MyActionParameters as viem_MyActionParameters,
4 type MyActionReturnType as viem_MyActionReturnType,
5 myAction as viem_myAction,
6} from 'viem/actions'
7
8import type { Config } from '../createConfig.js'
9import type { ChainIdParameter, ConnectorParameter } from '../types/properties.js'
10import type { Compute } from '../types/utils.js'
11import { getAction } from '../utils/getAction.js'
12
13export type MyActionParameters<config extends Config = Config> = Compute<
14 ChainIdParameter<config> & viem_MyActionParameters
15>
16
17export type MyActionReturnType = viem_MyActionReturnType
18
19export type MyActionErrorType = viem_MyActionErrorType
20
21/** https://wagmi.sh/core/api/actions/myAction */
22export async function myAction<config extends Config>(
23 config: config,
24 parameters: MyActionParameters<config>,
25): Promise<MyActionReturnType> {
26 const { chainId, ...rest } = parameters
27 const client = config.getClient({ chainId })
28 const action = getAction(client, viem_myAction, 'myAction')
29 return action(rest)
30}
Key Rules
- Viem imports: Prefix with
viem_ (e.g., viem_getBalance)
- Client access:
- Read-only:
config.getClient({ chainId })
- Wallet:
await getConnectorClient(config, { chainId, connector, account })
- Mixed: Use
getConnectorClient for account, getClient for action (see estimateGas.ts)
- Parameters: Add
ChainIdParameter<config> always. Add ConnectorParameter for wallet actions.
- Type params: Mirror Viem's type params for inference. Use
const modifier for literal inference (abi, args).
- Spread: Omit wagmi-specific props (
chainId, connector) when calling Viem action.
Testing
Runtime tests (action.test.ts):
ts
1import { abi, address, config } from '@wagmi/test'
2import { expect, test } from 'vitest'
3import { myAction } from './myAction.js'
4
5test('default', async () => {
6 await expect(myAction(config, { /* required params */ })).resolves.toMatchInlineSnapshot(`...`)
7})
8
9test('parameters: chainId', async () => { /* test chainId param */ })
10
11test('behavior: error case', async () => { /* test error handling */ })
Type tests (action.test-d.ts) - only if action has type inference:
ts
1import { config } from '@wagmi/test'
2import { expectTypeOf, test } from 'vitest'
3import { myAction } from './myAction.js'
4
5test('default', async () => {
6 const result = await myAction(config, { /* params */ })
7 expectTypeOf(result).toEqualTypeOf<ExpectedType>()
8})
Type benchmarks (action.bench-d.ts) - only if action has type inference:
ts
1import { attest } from '@ark/attest'
2import { test } from 'vitest'
3import type { MyActionParameters } from './myAction.js'
4
5test('default', () => {
6 type Result = MyActionParameters<typeof abi.erc20, 'balanceOf'>
7 const res = {} as Result
8 attest.instantiations([12345, 'instantiations'])
9 attest(res.args).type.toString.snap(`readonly [account: \`0x\${string}\`]`)
10})
Wallet action tests: Connect before, disconnect after:
ts
1test('default', async () => {
2 await connect(config, { connector })
3 await expect(myAction(config, { /* params */ })).resolves.toMatchInlineSnapshot(`...`)
4 await disconnect(config, { connector })
5})
2. Query Options
Query (read-only) or Mutation (wallet) options for TanStack Query.
Query Structure
ts
1import {
2 type MyActionErrorType,
3 type MyActionParameters,
4 type MyActionReturnType,
5 myAction,
6} from '../actions/myAction.js'
7import type { Config } from '../createConfig.js'
8import type { ScopeKeyParameter } from '../types/properties.js'
9import type { QueryOptions, QueryParameter } from '../types/query.js'
10import type { Compute, ExactPartial } from '../types/utils.js'
11import { filterQueryOptions, structuralSharing } from './utils.js'
12
13export type MyActionOptions<
14 config extends Config,
15 selectData = MyActionData,
16> = Compute<ExactPartial<MyActionParameters<config>> & ScopeKeyParameter> &
17 QueryParameter<MyActionQueryFnData, MyActionErrorType, selectData, MyActionQueryKey<config>>
18
19export function myActionQueryOptions<
20 config extends Config,
21 selectData = MyActionData,
22>(
23 config: config,
24 options: MyActionOptions<config, selectData> = {},
25): MyActionQueryOptions<config, selectData> {
26 return {
27 ...options.query,
28 enabled: Boolean(options.requiredParam && (options.query?.enabled ?? true)),
29 queryFn: async (context) => {
30 const [, { scopeKey: _, ...parameters }] = context.queryKey
31 if (!parameters.requiredParam) throw new Error('requiredParam is required')
32 const result = await myAction(config, {
33 ...(parameters as MyActionParameters),
34 requiredParam: parameters.requiredParam,
35 })
36 return result ?? null
37 },
38 queryKey: myActionQueryKey(options),
39 structuralSharing, // include when returning complex objects/arrays
40 }
41}
42
43export type MyActionQueryFnData = Compute<MyActionReturnType>
44export type MyActionData = MyActionQueryFnData
45
46export function myActionQueryKey<config extends Config>(
47 options: Compute<ExactPartial<MyActionParameters<config>> & ScopeKeyParameter> = {},
48) {
49 return ['myAction', filterQueryOptions(options)] as const
50}
51
52export type MyActionQueryKey<config extends Config> = ReturnType<typeof myActionQueryKey<config>>
53
54export type MyActionQueryOptions<
55 config extends Config,
56 selectData = MyActionData,
57> = QueryOptions<MyActionQueryFnData, MyActionErrorType, selectData, MyActionQueryKey<config>>
Mutation Structure
ts
1import type { MutationOptions, MutationParameter } from '../types/query.js'
2
3export type MyActionOptions<config extends Config, context = unknown> = MutationParameter<
4 MyActionData,
5 MyActionErrorType,
6 MyActionVariables<config>,
7 context
8>
9
10export function myActionMutationOptions<config extends Config, context>(
11 config: config,
12 options: MyActionOptions<config, context> = {},
13): MyActionMutationOptions<config> {
14 return {
15 ...options.mutation,
16 mutationFn: async (variables) => {
17 return myAction(config, variables)
18 },
19 mutationKey: ['myAction'],
20 }
21}
22
23export type MyActionMutationOptions<config extends Config> = MutationOptions<
24 MyActionData,
25 MyActionErrorType,
26 MyActionVariables<config>
27>
Key Rules
- ExactPartial vs UnionExactPartial: Use
ExactPartial for simple types, UnionExactPartial for complex unions (contract actions)
- enabled: Based on required params being truthy
- structuralSharing: Include when action returns objects/arrays
- filterQueryOptions: Filters common non-serializable props. Skip props like
onReplaced manually in query key.
- Query key: Always
['actionName', filterQueryOptions(options)]
Testing
ts
1import { config } from '@wagmi/test'
2import { expect, test } from 'vitest'
3import { myActionQueryOptions } from './myAction.js'
4
5test('default', () => {
6 expect(myActionQueryOptions(config, {})).toMatchInlineSnapshot(`
7 {
8 "enabled": false,
9 "queryFn": [Function],
10 "queryKey": ["myAction", {}],
11 }
12 `)
13})
14
15test('enabled', () => {
16 expect(myActionQueryOptions(config, { requiredParam: 'value' }).enabled).toBe(true)
17})
18
19test('queryFn: calls query fn', async () => {
20 const options = myActionQueryOptions(config, { requiredParam: 'value' })
21 const result = await options.queryFn({ queryKey: options.queryKey } as any)
22 expect(result).toMatchInlineSnapshot(`...`)
23})
3. Framework Bindings
React Query Hook
ts
1'use client'
2import type { Config, MyActionErrorType, ResolvedRegister } from '@wagmi/core'
3import type { Compute } from '@wagmi/core/internal'
4import {
5 type MyActionData,
6 type MyActionOptions,
7 myActionQueryOptions,
8} from '@wagmi/core/query'
9import type { ConfigParameter } from '../types/properties.js'
10import { type UseQueryReturnType, useQuery } from '../utils/query.js'
11import { useChainId } from './useChainId.js'
12import { useConfig } from './useConfig.js'
13
14export type UseMyActionParameters<
15 config extends Config = Config,
16 selectData = MyActionData,
17> = Compute<MyActionOptions<config, selectData> & ConfigParameter<config>>
18
19export type UseMyActionReturnType<selectData = MyActionData> =
20 UseQueryReturnType<selectData, MyActionErrorType>
21
22/** https://wagmi.sh/react/api/hooks/useMyAction */
23export function useMyAction<
24 config extends Config = ResolvedRegister['config'],
25 selectData = MyActionData,
26>(
27 parameters: UseMyActionParameters<config, selectData> = {},
28): UseMyActionReturnType<selectData> {
29 const config = useConfig(parameters)
30 const chainId = useChainId({ config })
31 const options = myActionQueryOptions(config, {
32 ...parameters,
33 chainId: parameters.chainId ?? chainId,
34 query: parameters.query,
35 })
36 return useQuery(options)
37}
React Mutation Hook
ts
1'use client'
2import { useMutation } from '@tanstack/react-query'
3import type { Config, ResolvedRegister, MyActionErrorType } from '@wagmi/core'
4import {
5 type MyActionData,
6 type MyActionMutate,
7 type MyActionMutateAsync,
8 type MyActionOptions,
9 type MyActionVariables,
10 myActionMutationOptions,
11} from '@wagmi/core/query'
12import type { ConfigParameter } from '../types/properties.js'
13import type { UseMutationReturnType } from '../utils/query.js'
14import { useConfig } from './useConfig.js'
15
16export type UseMyActionParameters<config extends Config = Config, context = unknown> =
17 MyActionOptions<config, context> & ConfigParameter<config>
18
19export type UseMyActionReturnType<config extends Config = Config, context = unknown> =
20 UseMutationReturnType<
21 MyActionData,
22 MyActionErrorType,
23 MyActionVariables<config>,
24 context,
25 MyActionMutate<config, context>,
26 MyActionMutateAsync<config, context>
27 >
28
29/** https://wagmi.sh/react/api/hooks/useMyAction */
30export function useMyAction<
31 config extends Config = ResolvedRegister['config'],
32 context = unknown,
33>(
34 parameters: UseMyActionParameters<config, context> = {},
35): UseMyActionReturnType<config, context> {
36 const config = useConfig(parameters)
37 const options = myActionMutationOptions(config, parameters)
38 const mutation = useMutation(options)
39 type Return = UseMyActionReturnType<config, context>
40 return {
41 ...mutation,
42 mutate: mutation.mutate as Return['mutate'],
43 mutateAsync: mutation.mutateAsync as Return['mutateAsync'],
44 }
45}
Vue Composable (Query)
ts
1import type { Config, MyActionErrorType, ResolvedRegister } from '@wagmi/core'
2import type { Compute } from '@wagmi/core/internal'
3import {
4 type MyActionData,
5 type MyActionOptions,
6 myActionQueryOptions,
7} from '@wagmi/core/query'
8import { computed } from 'vue'
9import type { ConfigParameter } from '../types/properties.js'
10import type { DeepMaybeRef } from '../types/ref.js'
11import { deepUnref } from '../utils/cloneDeep.js'
12import { type UseQueryReturnType, useQuery } from '../utils/query.js'
13import { useChainId } from './useChainId.js'
14import { useConfig } from './useConfig.js'
15
16export type UseMyActionParameters<
17 config extends Config = Config,
18 selectData = MyActionData,
19> = Compute<DeepMaybeRef<MyActionOptions<config, selectData> & ConfigParameter<config>>>
20
21export type UseMyActionReturnType<selectData = MyActionData> =
22 UseQueryReturnType<selectData, MyActionErrorType>
23
24/** https://wagmi.sh/vue/api/composables/useMyAction */
25export function useMyAction<
26 config extends Config = ResolvedRegister['config'],
27 selectData = MyActionData,
28>(
29 parameters: UseMyActionParameters<config, selectData> = {},
30): UseMyActionReturnType<selectData> {
31 const params = computed(() => deepUnref(parameters))
32 const config = useConfig(params)
33 const chainId = useChainId({ config })
34 const options = computed(() =>
35 myActionQueryOptions(config as any, {
36 ...params.value,
37 chainId: params.value.chainId ?? chainId.value,
38 query: params.value.query,
39 }),
40 )
41 return useQuery(options as any) as any
42}
Framework Rules
| Rule | React | Vue |
|---|
| Top directive | 'use client' | None |
| Parameters wrapper | Compute<...> | Compute<DeepMaybeRef<...>> |
| Reactivity | Direct | computed() + deepUnref() |
| Doc URL | wagmi.sh/react/api/hooks/ | wagmi.sh/vue/api/composables/ |
Shared rules:
ResolvedRegister['config']: Use in function signature only, not type defs
- No
enabled/structuralSharing in hooks: Handled by queryOptions
Testing
Query hook test-d.ts:
ts
1import { abi } from '@wagmi/test'
2import { expectTypeOf, test } from 'vitest'
3import { useMyAction } from './useMyAction.js'
4
5test('select data', () => {
6 const result = useMyAction({
7 /* params */
8 query: {
9 select(data) {
10 expectTypeOf(data).toEqualTypeOf<ExpectedDataType>()
11 return data
12 },
13 },
14 })
15 expectTypeOf(result.data).toEqualTypeOf<ExpectedDataType>()
16})
Mutation hook test-d.ts:
ts
1import { expectTypeOf, test } from 'vitest'
2import { useMyAction } from './useMyAction.js'
3
4test('context', () => {
5 const { mutate } = useMyAction({
6 mutation: {
7 onMutate(variables) {
8 expectTypeOf(variables).toMatchTypeOf<{ /* expected shape */ }>()
9 return { foo: 'bar' }
10 },
11 onError(error, variables, context) { /* test types */ },
12 onSuccess(data, variables, context) { /* test types */ },
13 onSettled(data, error, variables, context) { /* test types */ },
14 },
15 })
16
17 mutate({ /* params */ }, {
18 onSuccess(data, variables, context) { /* test inference */ },
19 })
20})
Exports
Add to exports/index.ts in respective package:
ts
1// packages/core/src/exports/index.ts
2export {
3 type MyActionParameters,
4 type MyActionReturnType,
5 type MyActionErrorType,
6 myAction,
7} from '../actions/myAction.js'
8
9// packages/core/src/exports/query.ts
10export {
11 type MyActionData,
12 type MyActionOptions,
13 type MyActionQueryFnData,
14 type MyActionQueryKey,
15 type MyActionQueryOptions,
16 myActionQueryKey,
17 myActionQueryOptions,
18} from '../query/myAction.js'
19
20// packages/react/src/exports/index.ts
21export {
22 type UseMyActionParameters,
23 type UseMyActionReturnType,
24 useMyAction,
25} from '../hooks/useMyAction.js'
Verification
bash
1# Format
2pnpm format
3
4# Type check (all or filtered)
5pnpm check:types
6pnpm --filter @wagmi/core check:types
7pnpm --filter wagmi check:types
8
9# Test (all or filtered)
10pnpm test
11pnpm test --project core
12pnpm test --project react
13
14# Update test snapshots
15pnpm vitest -u
16
17# Type benchmarks
18pnpm bench:types
19
20# Viem version mismatch in test snapshots
21pnpm version:update:viem
22
23# Build (all or filtered)
24pnpm run clean && pnpm build
25pnpm --filter @wagmi/core build