Web3 Integration
This page covers patterns for integrating blockchain functionality with Redux and RTK Query, including handling wallet connections, transactions, and on-chain events.
Core Principle: Keep Web3 Objects Outside Redux
Never store these in Redux:
window.ethereumProvider instances (
ethers.Provider,Web3Provider)Signer instances
WebSocket connections
Contract instances
AbortControllerinstances
These are non-serializable and violate Redux principles.
Recommended Architecture
Web3 Context (Outside Redux)
// src/contexts/Web3Context.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { ethers } from 'ethers';
interface Web3ContextValue {
provider: ethers.providers.Web3Provider | null;
signer: ethers.Signer | null;
account: string | null;
chainId: number | null;
connect: () => Promise<void>;
disconnect: () => void;
}
const Web3Context = createContext<Web3ContextValue | undefined>(undefined);
export function Web3Provider({ children }: { children: ReactNode }) {
const [provider, setProvider] = useState<ethers.providers.Web3Provider | null>(null);
const [signer, setSigner] = useState<ethers.Signer | null>(null);
const [account, setAccount] = useState<string | null>(null);
const [chainId, setChainId] = useState<number | null>(null);
const connect = async () => {
if (!window.ethereum) {
throw new Error('MetaMask not installed');
}
const web3Provider = new ethers.providers.Web3Provider(window.ethereum);
const accounts = await web3Provider.send('eth_requestAccounts', []);
const network = await web3Provider.getNetwork();
const signer = web3Provider.getSigner();
setProvider(web3Provider);
setSigner(signer);
setAccount(accounts[0]);
setChainId(network.chainId);
};
const disconnect = () => {
setProvider(null);
setSigner(null);
setAccount(null);
setChainId(null);
};
// Listen to account changes
useEffect(() => {
if (!window.ethereum) return;
const handleAccountsChanged = (accounts: string[]) => {
if (accounts.length === 0) {
disconnect();
} else {
setAccount(accounts[0]);
}
};
const handleChainChanged = (chainIdHex: string) => {
const newChainId = parseInt(chainIdHex, 16);
setChainId(newChainId);
};
window.ethereum.on('accountsChanged', handleAccountsChanged);
window.ethereum.on('chainChanged', handleChainChanged);
return () => {
window.ethereum?.removeListener('accountsChanged', handleAccountsChanged);
window.ethereum?.removeListener('chainChanged', handleChainChanged);
};
}, []);
return (
<Web3Context.Provider
value={{ provider, signer, account, chainId, connect, disconnect }}
>
{children}
</Web3Context.Provider>
);
}
export function useWeb3() {
const context = useContext(Web3Context);
if (!context) {
throw new Error('useWeb3 must be used within Web3Provider');
}
return context;
}Redux Slice for Web3 State
Store only serializable Web3 state in Redux:
Syncing Context with Redux
Connect the Web3 context to Redux:
Transaction Lifecycle
Sending Transactions
Using in Components
Listening to On-Chain Events
RTK Query with Web3 Data
Create endpoints that use blockchain data:
Cache Invalidation Strategies
On Network Change
On Account Change
After Transaction Confirmation
Best Practices
1. Separate Concerns
2. Handle Transaction States
3. Optimistic Updates with Rollback
4. Event Listener Cleanup
Next Steps
Review Testing & Performance for optimization
See Component Patterns for usage examples
Understand RTK Query for cache management
Last updated