Web3 Tutorial: build DApp with Web3-React and SWR

yakult

fangjun

Posted on February 24, 2022

Web3 Tutorial: build DApp with Web3-React and SWR

In "Tutorial: Build DAPP with hardhat, React and Ethers.js", we connect to and interact with the blockchain using Ethers.js directly. It is ok, but there are tedious processes needed to be done by ourselves.

We would rather use handy frameworks to help us in three aspects:

  1. maintain context and connect with blockchain.

  2. connect to different kinds of blockchain providers.

  3. query blockchain more efficiently.

Web3-React, a connecting framework for React and Ethereum, can help us with job 1 & 2. (We will focus on job 1.) Web3-React is an open source framework developed by Uniswap engineering Lead Noah Zinsmeister. You can also try WAGMI: React Hooks for Ethereum.

SWR can help us to query blockchains efficiently. SWR (stale-while-revalidate) is a library of react hooks for data fetching. I learned how to use SWR with blockchain from Lorenzo Sicilia's tutorial How to Fetch and Update Data From Ethereum with React and SWR.

I am still trying to find an efficient way to deal with Event. The Graph (sub-graph) is one of the good choices. The Graph Protocol and sub-graph are widely used by DeFi applications. In Nader Dabit's tutorial "The Complete Guide to Full Stack Web3 Development", he gives us a clear guide on how to use sub-graph.

Special thanks to Lorenzo Sicilia and his tutorial. I adapted the SWR flow and some code snippets from him.

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 start building our DApp using Web3-React.


Task #1

Task 1: Prepare webapp project and smart contract

The 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 scaffold code from our github repo.

First, we make a hhproject/ directory for our project (hhproject/chain/ for hardhat project, hhproject/webapp/ for React/Node.js webapp):

mkdir hhproject && cd hhproject
Enter fullscreen mode Exit fullscreen mode

Project directory structure:

- hhproject
  - chain (working dir for hardhat)
    - contracts
    - test
    - scripts
  - webapp (working dir for NextJS app)
    - src
      - pages
      - components  
Enter fullscreen mode Exit fullscreen mode

Download an empty webapp scaffold:

git clone https://github.com/fjun99/webapp-tutorial-scaffold.git webapp
cd webapp
yarn install
yarn dev
Enter fullscreen mode Exit fullscreen mode

We also need to prepare an ERC20 token ClassToken for our webapp 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

Again, we choose to download the hardhat chain starter project from github repo.In your hhproject/ directory:

git clone git@github.com:fjun99/chain-tutorial-hardhat-starter.git chain
cd chain
yarn install
Enter fullscreen mode Exit fullscreen mode

Let's run "compile, test, deploy" circle of smart contract development.

In another terminal, run command line in hhproject/chain/ directory to start a stand-alone Hardhat Network (local testnet) :

yarn hardhat node
Enter fullscreen mode Exit fullscreen mode

Then compile, test and deploy smart contract:

yarn hardhat compile
yarn hardhat test test/ClassToken.test.ts
yarn hardhat run scripts/deploy_classtoken.ts --network localhost
// ClassToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
// ✨  Done in 4.04s.
Enter fullscreen mode Exit fullscreen mode

Now we have ClassToken deployed to local testnet: 0x5FbDB2315678afecb367f032d93F642f64180aa3


Task #2

Task 2: Add Web3-React to our webapp - Connect button

Task 2.1: Understanding Web3-React

Web3-react explained

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/core
yarn add @web3-react/injected-connector
yarn add ethers
yarn add swr
Enter fullscreen mode Exit fullscreen mode

We will use swr later.

STEP 2: edit pages/_app.tsx:

// src/pages/_app.tsx
import { 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
Enter fullscreen mode Exit fullscreen mode

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

connector, provider, signer

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

Enter fullscreen mode Exit fullscreen mode

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
    ]
})
Enter fullscreen mode Exit fullscreen mode

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)}`
}
Enter fullscreen mode Exit fullscreen mode

STEP 4: add ConnectMetamask component to index.tsx

import ConnectMetamask from 'components/ConnectMetamask'
...
      <ConnectMetamask />
Enter fullscreen mode Exit fullscreen mode

STEP 5: run web app by running yarn dev

connect wallet

Explanation of what do we do here:

  • We get hooks from useWeb3React: chainId, account, activate,deactivate, setError, active,library ,connector

  • When a user clicks connect, we call activate(injected). inject is InjectedConnector (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.getSigner().


Task #3

Task 3: Read from blockchain - ETHBalance

We will use Web3-React to read from smart contract.

Task 3.1: Add ETHbalance.tsx (first attempt)

Add a component to get the ETH balance of your 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

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 attempt)

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

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Explanations:

  • We use SWR to fetch data, which calls provider.getBalance( address [ , blockTag = latest ] ) (Ethers docs link). The library is a web3 provider.
    const { data: balance,mutate } = useSWR(['getBalance', account, 'latest'], {
      fetcher: fetcher(library),
    })
Enter fullscreen mode Exit fullscreen mode
  • The fetcher is constructed as:
const fetcher = (library:any) => (...args:any) => {
  const [method, ...params] = args
  return library[method](...params)
}
Enter fullscreen mode Exit fullscreen mode
  • We get mutate of SWR to change its internal cache in the client. We mutate balance to undefined in every block, so SWR will query and update for us.
      library.on('block', () => {
        console.log('update balance...')
        mutate(undefined, true)
      })
Enter fullscreen mode Exit fullscreen mode
  • 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 the current account (Account#0) changes accordingly.

More explanations about SWR can be found at:

  • Lorenzo Sicilia's blockchain tutorial: link

  • SWR documents: link


Image description

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)"
];

Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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),
  })
Enter fullscreen mode Exit fullscreen mode
  • 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)
  }
Enter fullscreen mode Exit fullscreen mode
  • When ethereum network connection is changed to active, query symbol() and totalSupply. Since these two are non-changable constants, we only query them once.

  • Add listener when change to active or account change. Two listeners are added: events transfer ERC20 token to account and from account.

    // 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)
    })
Enter fullscreen mode Exit fullscreen mode

Result:

display results


Task #5

Task 5: Write - Interact with smart contract

Task 5.1: Add a component for Transfer

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>
  )
}

Enter fullscreen mode Exit fullscreen mode

Task 5.2 Add transfer component to index.tsx

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>

Enter fullscreen mode Exit fullscreen mode

Let's go to http://localhost:3000/ in browse and play with our DApp:

Webapp


You can find that the webapp 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)

https://dev.to/yakult/tutorial-write-upgradeable-smart-contract-proxy-contract-with-openzeppelin-1916

7. Tutorial: Build an NFT marketplace DApp like Opensea(5 Tasks)

https://dev.to/yakult/tutorial-build-a-nft-marketplace-dapp-like-opensea-3ng9


If you find this tutorial helpful, follow me at Twitter @fjun99

💖 💪 🙅 🚩
yakult
fangjun

Posted on February 24, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related