An Interest In:
Web News this Week
- April 23, 2024
- April 22, 2024
- April 21, 2024
- April 20, 2024
- April 19, 2024
- April 18, 2024
- April 17, 2024
Tutorial: build DApp with Web3-React and SWR
In the tutorial "build DAPP with hardhat, React and Ethers.js", we connect and interact with the blockchain using ethers.js
directly. It is OK, but there are too many works need to be done by ourselves.
We would rather use frameworks to help us with three jobs:
1.maintain context and connect with blockchain.
2.connect to different kinds of blockchain providers.
3.manage query to blockchain more efficiently.
Web3-React, a connecting framework for React and Ethereum, can help us with job 1 & 2. Web3-React is an open source framework developed Uniswap engineering Lead Noah Zinsmeister. (We will focus on job 1.)
SWR can help us read blockchains more efficiently. SWR (stale-while-revalidate) is a library of react hooks for data fetching. I learn SWR from the Consensys tutorial by Lorenzo Sicilia How to Fetch and Update Data From Ethereum with React and SWR.
I am still trying to find a framework to help us with Event, The Graph (sub-graph) is one of the good choices. The Graph is widely used by DeFi applications. In Nader Dabit's guide "The Complete Guide to Full Stack Web3 Development", he gives us great advice on how to use sub-graph.
Special thanks to Lorenzo Sicilia and his tutorial, I adapted the flow and some code snippets from him only changed to using Web3-React
.
You can find the code repos for this tutorial:
Hardhat project: https://github.com/fjun99/chain-tutorial-hardhat-starter
Webapp project: https://github.com/fjun99/web3app-tutrial-using-web3react
Let's begin to build with our DApp using Web3-React.
Task 1: Prepare webapp project and smartcontract
First half of Task 1 is the same as the ones in "Tutorial: build DApp with Hardhat, React and ethers.js". Please refer to that tutorial.
Tutorial: build DApp with Hardhat, React and ethers.js
https://dev.to/yakult/a-tutorial-build-dapp-with-hardhat-react-and-ethersjs-1gmi
Task 1: setup development environment
Task 1.1 Install Hardhat and init a Hardhat project
Task 1.2 Development Circle in Hardhat
Task 1.3 MetaMask Switch Local testnet
Task 1.4 Create webapp with Next.js and Chakra UI
Task 1.5 Edit webapp - header, layout, _app.tsx, index.tsx
We choose to download the webapp code from github repo.
In your 'hhproject/' directory:
git clone https://github.com/fjun99/webapp-tutorial-scaffold.git webappcd webappyarn installyarn dev
We have a project directory to build.
- hhproject - chain (working dir for hardhat) - contracts - test - scripts - webapp (working dir for NextJS app) - src - pages - components
We also need to prepare an ERC20 token ClassToken for DApp to interact with. This is the second half of Task 1.
This job can be done same as Task 3 of "Tutorial: build DApp with Hardhat, React and ethers.js"
Task 3: Build ERC20 smart contract using OpenZeppelin
Task 3.1 Write ERC20 smart contract
Task 3.2 Compile smart contract
Task 3.3 Add unit test script
Task 3.4 Add deploy script
Task 3.5 Run stand-alone testnet again and deploy to it
Task 3.6 Interact with ClassToken in hardhat console
Task 3.7 Add token to MetaMask
Now we have ClassToken deployed to local testnet: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Task 2: Add Web3-React to our webapp - Connect button
Task 2.1 Understanding Web3-React
From my point of view, Web3-React is a web3 blockchain connecting framework which provides three features we need:
Web3ReactProvder, a react context we can access throughout our web app.
useWeb3React, handy react hook to interact with blockchain.
Connectors of several kinds of blockchain providers, such as MetaMask (browser extension), RPC connector(Alchemy and Infura), QR code connector(WalletConnect), Hardware connector (Ledger/Trezor).
Currently Web3-React has stable V6 and beta V8. We will use V6 in our tutorial.
Task 2.2 install Web3-React
, Ethers.js
and add Web3ReactProvder
STEP 1: install dependencies
In the webapp
directory, run:
yarn add @web3-react/coreyarn add @web3-react/injected-connectoryarn add ethersyarn add swr
We will use swr
later.
STEP 2: edit pages/_app.tsx
:
// src/pages/_app.tsximport { ChakraProvider } from '@chakra-ui/react'import type { AppProps } from 'next/app'import { Layout } from 'components/layout'import { Web3ReactProvider } from '@web3-react/core'import { Web3Provider } from '@ethersproject/providers'function getLibrary(provider: any): Web3Provider { const library = new Web3Provider(provider) return library}function MyApp({ Component, pageProps }: AppProps) { return ( <Web3ReactProvider getLibrary={getLibrary}> <ChakraProvider> <Layout> <Component {...pageProps} /> </Layout> </ChakraProvider> </Web3ReactProvider> )}export default MyApp
Explanations:
We add a react context provider
Web3ReactProvider
in_app.tsx
.Blockchain provider (library) is an ethers.js
Web3Provider
which we can add connector and activate later using hooks.
Task 2.3 add an empty ConnectMetamask component
The relationship between connector, provider and signer in ethers.js
is illustrated in the graph.
In this sub-task we will add an empty ConnectMetamask component.
- STEP 1: Add
src/components/ConnectMetamask.tsx
:
import { useEffect } from 'react'import { useWeb3React } from '@web3-react/core'import { Web3Provider } from '@ethersproject/providers'import { Box, Button, Text} from '@chakra-ui/react'import { injected } from 'utils/connectors'import { UserRejectedRequestError } from '@web3-react/injected-connector'import { formatAddress } from 'utils/helpers'const ConnectMetamask = () => { const { chainId, account, activate,deactivate, setError, active,library ,connector} = useWeb3React<Web3Provider>() const onClickConnect = () => { activate(injected,(error) => { if (error instanceof UserRejectedRequestError) { // ignore user rejected error console.log("user refused") } else { setError(error) } }, false) } const onClickDisconnect = () => { deactivate() } useEffect(() => { console.log(chainId, account, active,library,connector) }) return ( <div> {active && typeof account === 'string' ? ( <Box> <Button type="button" w='100%' onClick={onClickDisconnect}> Account: {formatAddress(account,4)} </Button> <Text fontSize="sm" w='100%' my='2' align='center'>ChainID: {chainId} connected</Text> </Box> ) : ( <Box> <Button type="button" w='100%' onClick={onClickConnect}> Connect MetaMask </Button> <Text fontSize="sm" w='100%' my='2' align='center'> not connected </Text> </Box> )} </div> ) }export default ConnectMetamask
STEP 2: define a injected
connector in uitls/connectors.tsx
:
import { InjectedConnector } from "@web3-react/injected-connector";export const injected = new InjectedConnector({ supportedChainIds: [ 1, 3, 4, 5, 10, 42, 31337, 42161 ]})
STEP 3: add a helper in utils/helpers.tsx
export function formatAddress(value: string, length: number = 4) { return `${value.substring(0, length + 2)}...${value.substring(value.length - length)}`}
STEP 4: add ConnectMetamask
component to index.tsx
import ConnectMetamask from 'components/ConnectMetamask'... <ConnectMetamask />
STEP 5: run web app by running yarn dev
Explanation of what we do here:
We get hooks from
useWeb3React
: chainId, account, activate,deactivate, setError, active,library ,connectorWhen a user clicks connect, we call
activate(injected)
.inject
isInjectedConnector
(mostly it means window.ethereum injected by MetaMask) that we can configure.When user click disconnect, we call
decativate()
.The library is the ethers.js Web3Provider we can use.
Specifically, the library is an ethers.js
provider which can be used to connect and read blockchain. If we want to send transaction to blockchain (write), we will need to get ethers.js signer by call provider.getSiger()
.
Task 3: Read from blockchain - ETHBalance
We will use Web3-React to read from smart contract.
Task 3.1 Add ETHbalance.tsx
(first attemp)
Add a component to get the ETH balance of current account. Add components/ETHBalance.tsx
import { useState, useEffect } from 'react'import { useWeb3React } from '@web3-react/core'import { Web3Provider } from '@ethersproject/providers'import { Text} from '@chakra-ui/react'import { formatEther } from "@ethersproject/units"const ETHBalance = () => { const [ethBalance, setEthBalance] = useState<number | undefined>(undefined) const {account, active, library,chainId} = useWeb3React<Web3Provider>() const provider = library useEffect(() => { if(active && account){ provider?.getBalance(account).then((result)=>{ setEthBalance(Number(formatEther(result))) }) } }) return ( <div> {active ? ( <Text fontSize="md" w='100%' my='2' align='left'> ETH in account: {ethBalance?.toFixed(3)} {chainId===31337? 'Test':' '} ETH </Text> ) : ( <Text fontSize="md" w='100%' my='2' align='left'>ETH in account:</Text> )} </div> ) }export default ETHBalance
Edit pages/index.tsx
to display ETHBalance:
<Box mb={0} p={4} w='100%' borderWidth="1px" borderRadius="lg"> <Heading my={4} fontSize='xl'>ETH Balance</Heading> <ETHBalance /> </Box>
The problem with this is how to constantly sync the results (ETH balance) with blockchain. Lorenzo Sicilia suggests to use SWR
with events listening to get data more efficiently. The SWR project homepage says:
SWR is a strategy to first return the data from cache (stale), then send the fetch request (revalidate), and finally come with the up-to-date data.
With SWR, components will get a stream of data updates constantly and automatically. The UI will always be fast and reactive.
Task 3.2 Add ETHBalanceSWR.tsx
(second attemp)
Add components/ETHBalanceSWR.tsx
import { useState, useEffect } from 'react'import { useWeb3React } from '@web3-react/core'import { Web3Provider } from '@ethersproject/providers'import { Text} from '@chakra-ui/react'import { formatEther } from "@ethersproject/units"import useSWR from 'swr'const fetcher = (library:any) => (...args:any) => { const [method, ...params] = args return library[method](...params)}const ETHBalanceSWR = () => { const { account, active, library,chainId} = useWeb3React<Web3Provider>() const { data: balance,mutate } = useSWR(['getBalance', account, 'latest'], { fetcher: fetcher(library), }) console.log("ETHBalanceSWR",balance) useEffect(() => { if(!library) return // listen for changes on an Ethereum address console.log(`listening for blocks...`) library.on('block', () => { console.log('update balance...') mutate(undefined, true) }) // remove listener when the component is unmounted return () => { library.removeAllListeners('block') } // trigger the effect only on component mount // ** changed to library prepared }, [library]) return ( <div> {active && balance ? ( <Text fontSize="md" w='100%' my='2' align='left'> ETH in account: {parseFloat(formatEther(balance)).toFixed(3)} {chainId===31337? 'Test':' '} ETH </Text> ) : ( <Text fontSize="md" w='100%' my='2' align='left'>ETH in account:</Text> )} </div> ) }export default ETHBalanceSWR
Add ETHBalanceSWR
component to index.tsx
<Box mb={0} p={4} w='100%' borderWidth="1px" borderRadius="lg"> <Heading my={4} fontSize='xl'>ETH Balance <b>using SWR</b></Heading> <ETHBalanceSWR /> </Box>
Explanations:
- We use SWR to fetch data, which calls
provider.getBalance( address [ , blockTag = latest ] )
(ethers docs link). Thelibrary
is a web3 provider.
const { data: balance,mutate } = useSWR(['getBalance', account, 'latest'], { fetcher: fetcher(library), })
- The fetcher is constructed as:
const fetcher = (library:any) => (...args:any) => { const [method, ...params] = args return library[method](...params)}
- We get
mutate
of SWR to change its internal cache in the client. We mutate balance toundefined
in every block, so SWR will query and update for us.
library.on('block', () => { console.log('update balance...') mutate(undefined, true) })
- When library(provider) changes and we have a provider, the side effect (
useEffect()
) will add a listener to blockchain new block event. Block events are emitted on every block change.
Let's play with the webapp:
Send test ETH from Hardhat local testnet Account#0(
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
) to Account#1(0x70997970C51812dc3A010C7d01b50e0d17dc79C8
).Check that the ETH balance of current account (Account#0) changes accordingly.
More explanations about SWR can be found at:
Task 4: Read / Listen - Interact with smart contract
In this task, we will read data using SWR from smart contract. We use smart contract event listening to get updates.
Task 4.1 Add ERC20ABI.tsx
Add abi/ERC20ABI.tsx
for standard ERC20.
export const ERC20ABI = [ // Read-Only Functions "function balanceOf(address owner) view returns (uint256)", "function totalSupply() view returns (uint256)", "function decimals() view returns (uint8)", "function symbol() view returns (string)", // Authenticated Functions "function transfer(address to, uint amount) returns (bool)", // Events "event Transfer(address indexed from, address indexed to, uint amount)"];
Add components/ReadERC20.tsx
import React, { useEffect,useState } from 'react';import { useWeb3React } from '@web3-react/core'import { Web3Provider } from '@ethersproject/providers'import {Contract} from "@ethersproject/contracts";import { formatEther}from "@ethersproject/units"import { Text} from '@chakra-ui/react'import useSWR from 'swr'import {ERC20ABI as abi} from "abi/ERC20ABI"interface Props { addressContract: string}const fetcher = (library: Web3Provider | undefined, abi: any) => (...args:any) => { if (!library) return const [arg1, arg2, ...params] = args const address = arg1 const method = arg2 const contract = new Contract(address, abi, library) return contract[method](...params) }export default function ReadERC20(props:Props){ const addressContract = props.addressContract const [symbol,setSymbol]= useState<string>("") const [totalSupply,setTotalSupply]=useState<string>() const { account, active, library} = useWeb3React<Web3Provider>() const { data: balance, mutate } = useSWR([addressContract, 'balanceOf', account], { fetcher: fetcher(library, abi), })useEffect( () => { if(!(active && account && library)) return const erc20:Contract = new Contract(addressContract, abi, library); library.getCode(addressContract).then((result:string)=>{ //check whether it is a contract if(result === '0x') return erc20.symbol().then((result:string)=>{ setSymbol(result) }).catch('error', console.error) erc20.totalSupply().then((result:string)=>{ setTotalSupply(formatEther(result)) }).catch('error', console.error); })//called only when changed to active},[active])useEffect(() => { if(!(active && account && library)) return const erc20:Contract = new Contract(addressContract, abi, library) // listen for changes on an Ethereum address console.log(`listening for Transfer...`) const fromMe = erc20.filters.Transfer(account, null) erc20.on(fromMe, (from, to, amount, event) => { console.log('Transfer|sent', { from, to, amount, event }) mutate(undefined, true) }) const toMe = erc20.filters.Transfer(null, account) erc20.on(toMe, (from, to, amount, event) => { console.log('Transfer|received', { from, to, amount, event }) mutate(undefined, true) }) // remove listener when the component is unmounted return () => { erc20.removeAllListeners(toMe) erc20.removeAllListeners(fromMe) } // trigger the effect only on component mount }, [active,account])return ( <div> <Text >ERC20 Contract: {addressContract}</Text> <Text>token totalSupply:{totalSupply} {symbol}</Text> <Text my={4}>ClassToken in current account:{balance ? parseFloat(formatEther(balance)).toFixed(1) : " " } {symbol}</Text> </div> )}
Add ReadERC20
to index.tsx
:
const addressContract='0x5fbdb2315678afecb367f032d93f642f64180aa3'... <Box my={4} p={4} w='100%' borderWidth="1px" borderRadius="lg"> <Heading my={4} fontSize='xl'>ClassToken: ERC20 Smart Contract</Heading> <ReadERC20 addressContract={addressContract} /> </Box>
Some explanations:
- We query data from blockchain and smart contract by calling
contract.balanceOf()
.
const { data: balance, mutate } = useSWR([addressContract, 'balanceOf', account], { fetcher: fetcher(library, ERC20ABI), })
- The fetcher is constructed as:
const fetcher = (library: Web3Provider | undefined, abi: any) => (...args:any) => { if (!library) return const [arg1, arg2, ...params] = args const address = arg1 const method = arg2 const contract = new Contract(address, abi, library) return contract[method](...params) }
When ethereum network connection is changed to
active
, querysymbol()
andtotalSupply
. Since these two are non-changable constants, we only query them once.Add listener when change to
active
oraccount
change. Two listeners are added: events transfer ERC20 token toaccount
and fromaccount
.
// listen for changes on an Ethereum address console.log(`listening for Transfer...`) const fromMe = erc20.filters.Transfer(account, null) erc20.on(fromMe, (from, to, amount, event) => { console.log('Transfer|sent', { from, to, amount, event }) mutate(undefined, true) }) const toMe = erc20.filters.Transfer(null, account) erc20.on(toMe, (from, to, amount, event) => { console.log('Transfer|received', { from, to, amount, event }) mutate(undefined, true) })
Result:
Task 5: Write - Interact with smart contract
In this task, we will add TransferERC20.tsx
.
Edit components/TransferERC20.tsx
import React, { useState } from 'react';import { useWeb3React } from '@web3-react/core'import { Web3Provider } from '@ethersproject/providers'import {Contract} from "@ethersproject/contracts";import { parseEther}from "@ethersproject/units"import { Button, Input , NumberInput, NumberInputField, FormControl, FormLabel } from '@chakra-ui/react'import {ERC20ABI} from "abi/ERC20ABI"interface Props { addressContract: string}export default function TransferERC20(props:Props){ const addressContract = props.addressContract const [toAddress, setToAddress]=useState<string>("") const [amount,setAmount]=useState<string>('100') const { account, active, library} = useWeb3React<Web3Provider>() async function transfer(event:React.FormEvent) { event.preventDefault() if(!(active && account && library)) return // new contract instance with **signer** const erc20 = new Contract(addressContract, ERC20ABI, library.getSigner()); erc20.transfer(toAddress,parseEther(amount)).catch('error', console.error) } const handleChange = (value:string) => setAmount(value) return ( <div> <form onSubmit={transfer}> <FormControl> <FormLabel htmlFor='amount'>Amount: </FormLabel> <NumberInput defaultValue={amount} min={10} max={1000} onChange={handleChange}> <NumberInputField /> </NumberInput> <FormLabel htmlFor='toaddress'>To address: </FormLabel> <Input id="toaddress" type="text" required onChange={(e) => setToAddress(e.target.value)} my={3}/> <Button type="submit" isDisabled={!account}>Transfer</Button> </FormControl> </form> </div> )}
Add TransferERC20
in index.tsx
<Box my={4} p={4} w='100%' borderWidth="1px" borderRadius="lg"> <Heading my={4} fontSize='xl'>Transfer ClassToken ERC20 token</Heading> <TransferERC20 addressContract={addressContract} /> </Box>
You can find that the web app is structured well and simply by using Web3-React
. Web3-React gives us context provider and hooks we can use easily.
From now on, you can begin to write your own DAPPs.
Tutorial list:
1. A Concise Hardhat Tutorial(3 parts)
https://dev.to/yakult/a-concise-hardhat-tutorial-part-1-7eo
2. Understanding Blockchain with ethers.js
(5 parts)
https://dev.to/yakult/01-understanding-blockchain-with-ethersjs-4-tasks-of-basics-and-transfer-5d17
3. Tutorial : build your first DAPP with Remix and Etherscan (7 Tasks)
https://dev.to/yakult/tutorial-build-your-first-dapp-with-remix-and-etherscan-52kf
4. Tutorial: build DApp with Hardhat, React and ethers.js (6 Tasks)
https://dev.to/yakult/a-tutorial-build-dapp-with-hardhat-react-and-ethersjs-1gmi
5. Tutorial: build DAPP with Web3-React and SWR
https://dev.to/yakult/tutorial-build-dapp-with-web3-react-and-swr-1fb0
6. Tutorial: write upgradeable smart contract (proxy) using OpenZeppelin(7 Tasks)
If you find this tutorial helpful, follow me at Twitter @fjun99
Original Link: https://dev.to/yakult/tutorial-build-dapp-with-web3-react-and-swr-1fb0
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To