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 import { type MyActionErrorType as viem_MyActionErrorType, type MyActionParameters as viem_MyActionParameters, type MyActionReturnType as viem_MyActionReturnType, myAction as viem_myAction, } from 'viem/actions'
import type { Config } from '../createConfig.js' import type { ChainIdParameter, ConnectorParameter } from '../types/properties.js' import type { Compute } from '../types/utils.js' import { getAction } from '../utils/getAction.js'
export type MyActionParameters
export type MyActionReturnType = viem_MyActionReturnType
export type MyActionErrorType = viem_MyActionErrorType
/* https://wagmi.sh/core/api/actions/myAction /
export async function myAction
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
Runtime tests (action.test.ts):
import { abi, address, config } from '@wagmi/test' import { expect, test } from 'vitest' import { myAction } from './myAction.js'
test('default', async () => {
await expect(myAction(config, { / required params / })).resolves.toMatchInlineSnapshot(...)
})
test('parameters: chainId', async () => { / test chainId param / })
test('behavior: error case', async () => { / test error handling / })
Type tests (action.test-d.ts) - only if action has type inference:
import { config } from '@wagmi/test' import { expectTypeOf, test } from 'vitest' import { myAction } from './myAction.js'
test('default', async () => {
const result = await myAction(config, { / params / })
expectTypeOf(result).toEqualTypeOf
Type benchmarks (action.bench-d.ts) - only if action has type inference:
import { attest } from '@ark/attest' import { test } from 'vitest' import type { MyActionParameters } from './myAction.js'
test('default', () => {
type Result = MyActionParametersreadonly [account: \0x\${string}`]`)
})
Wallet action tests: Connect before, disconnect after:
test('default', async () => {
await connect(config, { connector })
await expect(myAction(config, { / params / })).resolves.toMatchInlineSnapshot(...)
await disconnect(config, { connector })
})
- Query Options
Query (read-only) or Mutation (wallet) options for TanStack Query.
Query Structure import { type MyActionErrorType, type MyActionParameters, type MyActionReturnType, myAction, } from '../actions/myAction.js' import type { Config } from '../createConfig.js' import type { ScopeKeyParameter } from '../types/properties.js' import type { QueryOptions, QueryParameter } from '../types/query.js' import type { Compute, ExactPartial } from '../types/utils.js' import { filterQueryOptions, structuralSharing } from './utils.js'
export type MyActionOptions< config extends Config, selectData = MyActionData,
= Compute
> & ScopeKeyParameter> & QueryParameter >
export function myActionQueryOptions< config extends Config, selectData = MyActionData,
( config: config, options: MyActionOptions
= {}, ): MyActionQueryOptions { return { ...options.query, enabled: Boolean(options.requiredParam && (options.query?.enabled ?? true)), queryFn: async (context) => { const [, { scopeKey: _, ...parameters }] = context.queryKey if (!parameters.requiredParam) throw new Error('requiredParam is required') const result = await myAction(config, { ...(parameters as MyActionParameters), requiredParam: parameters.requiredParam, }) return result ?? null }, queryKey: myActionQueryKey(options), structuralSharing, // include when returning complex objects/arrays } }
export type MyActionQueryFnData = Compute
export function myActionQueryKey
export type MyActionQueryKey
export type MyActionQueryOptions< config extends Config, selectData = MyActionData,
= QueryOptions
>
Mutation Structure import type { MutationOptions, MutationParameter } from '../types/query.js'
export type MyActionOptions
export function myActionMutationOptions
export type MyActionMutationOptions
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 import { config } from '@wagmi/test' import { expect, test } from 'vitest' import { myActionQueryOptions } from './myAction.js'
test('default', () => {
expect(myActionQueryOptions(config, {})).toMatchInlineSnapshot({
"enabled": false,
"queryFn": [Function],
"queryKey": ["myAction", {}],
})
})
test('enabled', () => { expect(myActionQueryOptions(config, { requiredParam: 'value' }).enabled).toBe(true) })
test('queryFn: calls query fn', async () => {
const options = myActionQueryOptions(config, { requiredParam: 'value' })
const result = await options.queryFn({ queryKey: options.queryKey } as any)
expect(result).toMatchInlineSnapshot(...)
})
- Framework Bindings React Query Hook 'use client' import type { Config, MyActionErrorType, ResolvedRegister } from '@wagmi/core' import type { Compute } from '@wagmi/core/internal' import { type MyActionData, type MyActionOptions, myActionQueryOptions, } from '@wagmi/core/query' import type { ConfigParameter } from '../types/properties.js' import { type UseQueryReturnType, useQuery } from '../utils/query.js' import { useChainId } from './useChainId.js' import { useConfig } from './useConfig.js'
export type UseMyActionParameters< config extends Config = Config, selectData = MyActionData,
= Compute
& ConfigParameter\ >
export type UseMyActionReturnType
/* https://wagmi.sh/react/api/hooks/useMyAction / export function useMyAction< config extends Config = ResolvedRegister['config'], selectData = MyActionData,
( parameters: UseMyActionParameters
= {}, ): UseMyActionReturnType { const config = useConfig(parameters) const chainId = useChainId({ config }) const options = myActionQueryOptions(config, { ...parameters, chainId: parameters.chainId ?? chainId, query: parameters.query, }) return useQuery(options) }
React Mutation Hook 'use client' import { useMutation } from '@tanstack/react-query' import type { Config, ResolvedRegister, MyActionErrorType } from '@wagmi/core' import { type MyActionData, type MyActionMutate, type MyActionMutateAsync, type MyActionOptions, type MyActionVariables, myActionMutationOptions, } from '@wagmi/core/query' import type { ConfigParameter } from '../types/properties.js' import type { UseMutationReturnType } from '../utils/query.js' import { useConfig } from './useConfig.js'
export type UseMyActionParameters
export type UseMyActionReturnType
/* https://wagmi.sh/react/api/hooks/useMyAction / export function useMyAction< config extends Config = ResolvedRegister['config'], context = unknown,
( parameters: UseMyActionParameters
= {}, ): UseMyActionReturnType { const config = useConfig(parameters) const options = myActionMutationOptions(config, parameters) const mutation = useMutation(options) type Return = UseMyActionReturnType return { ...mutation, mutate: mutation.mutate as Return['mutate'], mutateAsync: mutation.mutateAsync as Return['mutateAsync'], } }
Vue Composable (Query) import type { Config, MyActionErrorType, ResolvedRegister } from '@wagmi/core' import type { Compute } from '@wagmi/core/internal' import { type MyActionData, type MyActionOptions, myActionQueryOptions, } from '@wagmi/core/query' import { computed } from 'vue' import type { ConfigParameter } from '../types/properties.js' import type { DeepMaybeRef } from '../types/ref.js' import { deepUnref } from '../utils/cloneDeep.js' import { type UseQueryReturnType, useQuery } from '../utils/query.js' import { useChainId } from './useChainId.js' import { useConfig } from './useConfig.js'
export type UseMyActionParameters< config extends Config = Config, selectData = MyActionData,
= Compute
& ConfigParameter\ >>
export type UseMyActionReturnType
/* https://wagmi.sh/vue/api/composables/useMyAction / export function useMyAction< config extends Config = ResolvedRegister['config'], selectData = MyActionData,
( parameters: UseMyActionParameters
= {}, ): UseMyActionReturnType { const params = computed(() => deepUnref(parameters)) const config = useConfig(params) const chainId = useChainId({ config }) const options = computed(() => myActionQueryOptions(config as any, { ...params.value, chainId: params.value.chainId ?? chainId.value, query: params.value.query, }), ) return useQuery(options as any) as any }
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:
import { abi } from '@wagmi/test' import { expectTypeOf, test } from 'vitest' import { useMyAction } from './useMyAction.js'
test('select data', () => {
const result = useMyAction({
/ params /
query: {
select(data) {
expectTypeOf(data).toEqualTypeOf
Mutation hook test-d.ts:
import { expectTypeOf, test } from 'vitest' import { useMyAction } from './useMyAction.js'
test('context', () => { const { mutate } = useMyAction({ mutation: { onMutate(variables) { expectTypeOf(variables).toMatchTypeOf<{ / expected shape / }>() return { foo: 'bar' } }, onError(error, variables, context) { / test types / }, onSuccess(data, variables, context) { / test types / }, onSettled(data, error, variables, context) { / test types / }, }, })
mutate({ / params / }, { onSuccess(data, variables, context) { / test inference / }, }) })
Exports
Add to exports/index.ts in respective package:
// packages/core/src/exports/index.ts export { type MyActionParameters, type MyActionReturnType, type MyActionErrorType, myAction, } from '../actions/myAction.js'
// packages/core/src/exports/query.ts export { type MyActionData, type MyActionOptions, type MyActionQueryFnData, type MyActionQueryKey, type MyActionQueryOptions, myActionQueryKey, myActionQueryOptions, } from '../query/myAction.js'
// packages/react/src/exports/index.ts export { type UseMyActionParameters, type UseMyActionReturnType, useMyAction, } from '../hooks/useMyAction.js'
Verification
Format
pnpm format
Type check (all or filtered)
pnpm check:types pnpm --filter @wagmi/core check:types pnpm --filter wagmi check:types
Test (all or filtered)
pnpm test pnpm test --project core pnpm test --project react
Update test snapshots
pnpm vitest -u
Type benchmarks
pnpm bench:types
Viem version mismatch in test snapshots
pnpm version:update:viem
Build (all or filtered)
pnpm run clean && pnpm build pnpm --filter @wagmi/core build