React hooks patterns with Web3 React integration
Introduction
React Hooks have revolutionized how we write React components by enabling stateful logic without classes. When combined with Web3React, a popular library for Ethereum dApp development, hooks provide an elegant way to integrate blockchain functionality into React applications. This post explores effective patterns for using React hooks with Web3React, covering common use cases, best practices, and potential pitfalls.
Web3React simplifies Ethereum provider management, wallet connections, and state synchronization across components. By leveraging hooks like useWeb3React
, useEffect
, and custom hooks, developers can create clean, maintainable dApps that handle blockchain interactions efficiently.
Setting Up Web3React with Custom Hooks
The foundation of any Web3React integration begins with proper setup. Creating custom hooks around Web3React's functionality helps encapsulate logic and makes it reusable across components.
First, let's set up the Web3ReactProvider with supported connectors:
import { Web3ReactProvider } from '@web3-react/core' import { MetaMask } from '@web3-react/metamask' import { WalletConnect } from '@web3-react/walletconnect' function getLibrary(provider) { return new Web3Provider(provider) } const connectors = [ new MetaMask(), new WalletConnect({ rpc: { 1: 'https://mainnet.infura.io/v3/YOUR_INFURA_KEY' } }) ] function App() { return ( <Web3ReactProvider getLibrary={getLibrary}> <YourAppComponent /> </Web3ReactProvider> ) }
Now, let's create a custom hook to handle connection logic:
import { useWeb3React } from '@web3-react/core' import { useEffect } from 'react' export function useWallet() { const { activate, deactivate, active, account, library, chainId } = useWeb3React() const connect = async (connector) => { try { await activate(connector) } catch (error) { console.error('Connection error:', error) } } const disconnect = () => { deactivate() } useEffect(() => { const provider = window.localStorage.getItem('provider') if (provider) { connect(connectors.find(c => c.constructor.name === provider)) } }, []) return { connect, disconnect, active, account, library, chainId } }
This custom hook provides a clean interface for components to interact with the wallet while handling persistence and error cases.
Managing Blockchain State with useEffect
React's useEffect
hook is particularly useful for reacting to changes in Web3React state. Here's how to properly manage blockchain data fetching and updates:
import { useState, useEffect } from 'react' import { useWeb3React } from '@web3-react/core' export function useBalance() { const { account, library, chainId } = useWeb3React() const [balance, setBalance] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) useEffect(() => { if (!account || !library) { setBalance(null) return } const fetchBalance = async () => { setLoading(true) try { const balance = await library.getBalance(account) setBalance(balance.toString()) setError(null) } catch (err) { setError('Failed to fetch balance') console.error(err) } finally { setLoading(false) } } fetchBalance() // Set up listener for new blocks const provider = library.provider if (provider.on) { const handleBlock = () => fetchBalance() provider.on('block', handleBlock) return () => { provider.removeListener('block', handleBlock) } } }, [account, library, chainId]) return { balance, loading, error } }
This pattern demonstrates several important concepts:
- Dependency tracking with
useEffect
- Cleanup of event listeners
- Loading and error states
- Automatic refresh on new blocks
Handling Contract Interactions
Interacting with smart contracts is a common requirement in dApps. Here's a pattern for creating reusable contract interaction hooks:
import { useState, useEffect, useCallback } from 'react' import { useWeb3React } from '@web3-react/core' import { Contract } from 'ethers' export function useContract(contractAddress, abi) { const { library, chainId } = useWeb3React() const [contract, setContract] = useState(null) useEffect(() => { if (!library || !contractAddress) { setContract(null) return } try { const signer = library.getSigner() const contractInstance = new Contract(contractAddress, abi, signer) setContract(contractInstance) } catch (error) { console.error('Failed to create contract instance:', error) setContract(null) } }, [library, chainId, contractAddress, abi]) const call = useCallback( async (methodName, ...args) => { if (!contract) { throw new Error('Contract not initialized') } return contract[methodName](...args) }, [contract] ) return { contract, call } }
Now let's create a specific hook for an ERC20 token:
import { useContract } from './useContract' import ERC20_ABI from './erc20-abi.json' export function useToken(tokenAddress) { const { call, contract } = useContract(tokenAddress, ERC20_ABI) const { account } = useWeb3React() const getBalance = useCallback(async () => { if (!account) return null return call('balanceOf', account) }, [call, account]) const approve = useCallback( async (spender, amount) => { return call('approve', spender, amount) }, [call] ) return { getBalance, approve, contract } }
This layered approach allows for both general contract interactions and token-specific functionality while maintaining clean separation of concerns.
Optimizing Performance with useMemo
Web3 applications often deal with complex data transformations. The useMemo
hook can help optimize these operations:
import { useMemo } from 'react' import { useWeb3React } from '@web3-react/core' import { formatEther } from 'ethers/lib/utils' export function useFormattedBalance() { const { balance } = useBalance() const formattedBalance = useMemo(() => { if (!balance) return '0.0' return parseFloat(formatEther(balance)).toFixed(4) }, [balance]) return formattedBalance }
Another common optimization is memoizing contract instances:
import { useMemo } from 'react' import { useWeb3React } from '@web3-react/core' import { Contract } from 'ethers' export function useMemoizedContract(address, abi) { const { library, chainId } = useWeb3React() return useMemo(() => { if (!address || !library) return null try { return new Contract(address, abi, library.getSigner()) } catch { return null } }, [address, abi, library, chainId]) }
Conclusion
React hooks provide powerful patterns for integrating Web3React into your dApp development workflow. By creating custom hooks for wallet management, contract interactions, and state management, you can build applications that are:
- More maintainable through separation of concerns
- More performant with proper memoization
- More reliable with comprehensive error handling
- More responsive with appropriate event listeners
The patterns shown in this post form a solid foundation, but they can be extended to handle more complex scenarios like multi-chain support, transaction monitoring, and gas price optimization. Remember that blockchain interactions are inherently asynchronous and error-prone, so always design your hooks with these characteristics in mind.
As the Web3 ecosystem evolves, these React hook patterns will continue to serve as flexible building blocks for your decentralized applications, allowing you to adapt to new requirements while maintaining clean, component-based architecture.