React hooks patterns with Web3 React integration

Frontend Lead
December 11, 2024
Updated on February 1, 2025
0 MIN READ
#backend#next-js#typescript#react#hooks

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:

  1. Dependency tracking with useEffect
  2. Cleanup of event listeners
  3. Loading and error states
  4. 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:

  1. More maintainable through separation of concerns
  2. More performant with proper memoization
  3. More reliable with comprehensive error handling
  4. 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.

Share this article