Writing
Web3 Frontend State Management for Wallet Data
A practical guide to separating wallet state, app state, and server cache state in Web3 dashboards. Covers wagmi, Zustand, TanStack Query, and architecture patterns that scale.
Web3 applications handle several types of data at the same time. Wallet connections, blockchain reads, API responses, filters, forms, and UI preferences all need to be managed correctly.
The key is to keep different types of state separate. When wallet state, application state, and server state are mixed together, Web3 applications can suffer from stale data, unnecessary re-renders, and difficult-to-test components.
Three Types of State in Web3 Apps
Web3 frontends generally work with three distinct categories of state.
Wallet State
Wallet state includes:
-
Connection status
-
Wallet address
-
Chain ID
-
Account information
-
Signing capabilities
For this type of state, wagmi should be the primary source of truth.
Application State
Application state covers UI-specific information such as:
-
Dropdowns
-
Filters
-
Modals
-
Form inputs
-
Tabs
-
Sidebar state
-
User preferences
React state or lightweight state-management libraries such as Zustand can handle these requirements.
Server and Cache State
Server state includes information retrieved from:
-
Blockchain RPC calls
-
On-chain contract reads
-
APIs
-
Indexers
-
External data providers
TanStack Query is well suited for managing this type of state because it provides caching, background refetching, deduplication, and loading states.
Why State Separation Matters
Blurring these boundaries can lead to:
-
Stale blockchain data
-
Unnecessary re-renders
-
Duplicate requests
-
Difficult testing
-
Complex components
-
Hard-to-maintain global stores
Keeping each type of state in the right place makes the application easier to understand and scale.
Managing Wallet Connection State
wagmi should be the source of truth for wallet state.
Avoid duplicating values such as the wallet address or chain ID inside a global Zustand or Redux store.
Configure wagmi
A typical wagmi configuration can support multiple chains and wallet connectors.
// lib/wagmi-config.ts
import { createConfig, http } from 'wagmi';
import { mainnet, base } from 'wagmi/chains';
import {
coinbaseWallet,
injected,
walletConnect,
} from 'wagmi/connectors';
export const config = createConfig({
chains: [mainnet, base],
connectors: [
injected(),
coinbaseWallet({
appName: 'My Dashboard',
}),
walletConnect({
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID!,
}),
],
transports: {
[mainnet.id]: http(process.env.NEXT_PUBLIC_ALCHEMY_URL),
[base.id]: http(),
},
ssr: true,
});
Keep Wallet Context Isolated
Wrap only the parts of the application that require wallet context.
Public pages that do not need wallet information should avoid importing wagmi hooks unnecessarily.
When to Use Each State Management Tool
Different state tools solve different problems. Choosing the smallest tool that fits the requirement keeps the codebase simpler.
React useState
Use useState for local UI state such as:
-
Dropdown visibility
-
Tab selection
-
Form inputs
-
Toggle states
Keep this state close to the component that uses it.
React Context
React Context is rarely necessary for wallet state because wagmi already provides wallet context.
It can still be useful for application-wide UI concerns such as:
-
Theme settings
-
Feature flags
-
Global UI configuration
Zustand
Zustand works well for lightweight application state such as:
-
Dashboard filters
-
Sidebar state
-
Notification queues
-
User preferences
// stores/dashboard-store.ts
import { create } from 'zustand';
interface DashboardStore {
timeRange: '24h' | '7d' | '30d' | 'all';
selectedTokens: string[];
setTimeRange: (range: DashboardStore['timeRange']) => void;
toggleToken: (address: string) => void;
}
export const useDashboardStore = create<DashboardStore>((set) => ({
timeRange: '7d',
selectedTokens: [],
setTimeRange: (timeRange) => set({ timeRange }),
toggleToken: (address) =>
set((state) => ({
selectedTokens: state.selectedTokens.includes(address)
? state.selectedTokens.filter((t) => t !== address)
: [...state.selectedTokens, address],
})),
}));
Redux
Redux can make sense for large teams managing complex cross-feature application state.
For most Web3 dashboards, however, it may add unnecessary complexity unless the project already uses Redux.
TanStack Query
TanStack Query should handle server and on-chain data.
It provides useful features such as:
-
Caching
-
Request deduplication
-
Background refetching
-
Loading states
-
Error handling
-
Query invalidation
For production Web3 dashboards, keeping server state in a dedicated query layer makes the architecture easier to maintain.
How to Avoid Unnecessary Re-Renders
Performance problems can appear when wallet state changes cause large sections of the application to re-render.
Separate Connected and Disconnected Views
Split connected and disconnected experiences into separate component trees.
// components/dashboard/DashboardShell.tsx
'use client';
import { useAccount } from 'wagmi';
import { PublicDashboard } from './PublicDashboard';
import { ConnectedDashboard } from './ConnectedDashboard';
export function DashboardShell() {
const { isConnected } = useAccount();
return isConnected ? (
<ConnectedDashboard />
) : (
<PublicDashboard />
);
}
This keeps wallet-dependent rendering isolated from public dashboard content.
Use React Query Select
Use React Query's select option when a component only needs a small portion of a larger response.
const { data: totalValue } = useQuery({
queryKey: ['portfolio', address],
queryFn: () => fetchPortfolio(address),
select: (data) =>
data.positions.reduce(
(sum, position) => sum + position.usdValue,
0
),
enabled: !!address,
});
Memoize Expensive Calculations
Use useMemo for expensive transformations, especially when preparing large datasets for charts or tables.
Avoid memoizing everything. Use it where calculations are genuinely expensive or where stable references are important.
Maintainable Web3 Architecture as Features Grow
As a Web3 application grows, organizing the project by feature can make the codebase easier to maintain.
Organize by Feature
Instead of putting every component, hook, and type into separate global folders, let each feature own its related files.
src/
features/
wallet/
components/
ConnectButton.tsx
NetworkGuard.tsx
portfolio/
hooks/
useTokenBalances.ts
usePortfolioValue.ts
components/
PortfolioSummary.tsx
transactions/
hooks/
useTransactionHistory.ts
components/
TransactionTable.tsx
lib/
wagmi-config.ts
api-client.ts
app/
dashboard/
page.tsx
Keep Data and Presentation Separate
The data layer should not depend on the presentation layer.
Hooks should return typed data, while components should focus on rendering that data.
This separation makes individual features easier to test, replace, and extend.
Practical Web3 State Architecture Example
A portfolio hook may need to combine blockchain balances with external token prices.
// features/portfolio/hooks/usePortfolioValue.ts
import { useReadContracts } from 'wagmi';
import { useQuery } from '@tanstack/react-query';
import { erc20Abi } from 'viem';
export function usePortfolioValue(
address: `0x${string}` | undefined,
tokens: Token[]
) {
const { data: balances } = useReadContracts({
contracts: tokens.map((token) => ({
address: token.address,
abi: erc20Abi,
functionName: 'balanceOf',
args: address ? [address] : undefined,
})),
query: {
enabled: !!address,
staleTime: 30_000,
},
});
const { data: prices } = useQuery({
queryKey: [
'prices',
tokens.map((token) => token.coingeckoId),
],
queryFn: () =>
fetchPrices(tokens.map((token) => token.coingeckoId)),
staleTime: 60_000,
});
if (!balances || !prices) {
return {
totalUsd: 0,
isLoading: true,
};
}
const totalUsd = balances.reduce((sum, balance, index) => {
const amount =
Number(balance.result) /
10 ** tokens[index].decimals;
return (
sum +
amount *
(prices[tokens[index].coingeckoId] ?? 0)
);
}, 0);
return {
totalUsd,
isLoading: false,
};
}
Why This Pattern Works
This approach keeps different responsibilities separated:
-
wagmi handles blockchain reads
-
TanStack Query handles cached API data
-
The custom hook combines the required data
-
Components only consume the final typed result
Common Web3 State Management Mistakes
Several state-management decisions can create problems as a Web3 application grows.
Storing On-Chain Data in Zustand
Avoid storing frequently changing blockchain data in Zustand when TanStack Query can manage it.
Otherwise, you lose useful features such as caching, deduplication, invalidation, and background refetching.
Calling Wallet Hooks Everywhere
Calling useBalance or similar hooks independently across many child components can create unnecessary data dependencies.
Where appropriate, create shared data hooks that provide the information required by related components.
Using useEffect for Data Fetching
Avoid using useEffect as the primary solution for server or blockchain data fetching.
Dedicated data-fetching tools provide better handling for caching, loading, errors, refetching, and request deduplication.
Storing Sensitive Information in Client State
Never put wallet private keys or sensitive credentials into client-side state.
RPC credentials should also be handled carefully and should not be exposed unnecessarily in browser code.
Creating a Monolithic Web3 Context
Avoid creating one large Web3Context that contains wallet information, balances, transactions, prices, filters, and UI state.
A change in one part of the context can cause unrelated components to re-render.
FAQ About Web3 State Management
Can I Use SWR Instead of React Query?
Yes. SWR can manage server-side data, but wagmi v2 integrates closely with TanStack Query.
Using one primary server-state library can reduce unnecessary complexity.
Should Wallet State Live in Zustand?
Generally, no.
wagmi already provides reactive wallet state, including connection information, account changes, and reconnection behavior.
How Do I Test Components Without a Wallet?
You can mock wagmi hooks or use wagmi's mock connectors and viem testing utilities.
This allows components to be tested without requiring a real wallet connection.
When Should I Lift State Up?
Lift state when multiple components need the same information.
For shared server data, prefer a common query key and reusable hook rather than moving the data into a global application store.
How Do I Handle Multi-Chain State?
Include chainId as part of relevant query keys and data-fetching logic.
Never silently assume that the application is always running on mainnet.
Final Thoughts
Good Web3 state management starts with clear boundaries.
Use wagmi for wallet state, React or Zustand for application state, and TanStack Query for server and on-chain data.
Keeping these responsibilities separate reduces unnecessary re-renders, prevents stale data, improves testing, and creates an architecture that can scale as the Web3 application becomes more complex.