DEV Community

Lorenzo Sicilia
Lorenzo Sicilia

Posted on • Updated on • Originally published at consensys.net

How to fetch and nicely update data from Ethereum with React and SWR

Ethereum allows us to build Decentralized Applications (DApps). The main difference between a typical application and a DApp is that you don't need to deploy a backend. At least as long as you take advantage of the other smart contracts deployed in the Ethereum MainNet.

Because of that, the Frontend plays a major role. It is in charge of marshalling and unmarshalling the data from the smart contracts, handling the interactions with the wallet (hardware or software) and, as usual, managing the UX. Not only that, by design, a DApp uses JSON-RPC calls and it can open a socket connection to receive updates.

As you can see there are a few things to orchestrate but don't worry, the ecosystem has matured quite a lot in the last few months.

Prerequisites

During this tutorial, I will assume you already have the the following:

A wallet connect to a Geth node to connect

The simplest is installing MetaMask so that you can use Infura infrastructure out of the box.

Some Ether in your account

When you are developing with Ethereum I strongly advise you to switch to a test net and use fake Ethers. If you need funds for testing purpose you can use a faucet e.g. https://faucet.rinkeby.io/

Basic understand of React

I will guide you step by step but I will assume you know how React works (including hooks). If something seems unfamiliar consult the React documentation.

A working React playground

I wrote this tutorial with Typescript but just few things are typed so with minimal changes you can use it as it is in javascript as well. I used Parcel.js but feel free to use Create React App too or other web application bundler.

Connect to Ethereum Mainet

Once you have Metamask ready we are going to use web3-react to handle the interaction with the network. It will give you a quite handy hook useWeb3React which contains many useful utilities for playing with Ethereum.

yarn add @web3-react/core @web3-react/injected-connector

Then you need a Provider. A Provider abstracts a connection to the Ethereum blockchain, for issuing queries and sending signed state-changing transactions.
We will use Web3Provider from Ether.js.

It seems already a few libraries, but when interacting with Etherum you need to translate Javascript data types to Solidity ones. And, you are also required to sign the transactions when you want to execute an action. Ether.js elegantly provide these functionalities.

yarn add @ethersproject/providers

notice: the above Ether.js package is the v5 currently in beta

After that we are ready to jot down a minimal hello world to check if we have everything we need:

import React from 'react'
import { Web3ReactProvider } from '@web3-react/core'
import { Web3Provider } from '@ethersproject/providers'
import { useWeb3React } from '@web3-react/core'
import { InjectedConnector } from '@web3-react/injected-connector'

export const injectedConnector = new InjectedConnector({
  supportedChainIds: [
    1, // Mainet
    3, // Ropsten
    4, // Rinkeby
    5, // Goerli
    42, // Kovan
  ],
})

function getLibrary(provider: any): Web3Provider {
  const library = new Web3Provider(provider)
  library.pollingInterval = 12000
  return library
}

export const Wallet = () => {
  const { chainId, account, activate, active } = useWeb3React<Web3Provider>()

  const onClick = () => {
    activate(injectedConnector)
  }

  return (
    <div>
      <div>ChainId: {chainId}</div>
      <div>Account: {account}</div>
      {active ? (
        <div> </div>
      ) : (
        <button type="button" onClick={onClick}>
          Connect
        </button>
      )}
    </div>
  )
}

export const App = () => {
  return (
    <Web3ReactProvider getLibrary={getLibrary}>
      <Wallet />
    </Web3ReactProvider>
  )
}

If you did your homework you should have something like this:
step-1-helloworld
Here what we did so far: GIT - step-1

How to fetch data from the MainNet

I will use SWR to manage the data fetching.

This is what I want to achieve.

const { data: balance } = useSWR(["getBalance", account, "latest"])

Quite cool :)

Let's unveil the trick! SWR means Stale-While-Revalidate, an HTTP cache invalidation strategy popularized by RFC 5861.

SWR first returns the data from cache (stale), then sends the fetch request (revalidate), and finally comes with the up-to-date data again.
SWR accepts a key and behind the scenes will manage to resolve

To do that SWR allows passing a fetcher capable of resolving the key by returning a promise. The hello world of SWR is based on REST API requests with a fetcher based on fetch API or Axios.

What is brilliant about SWR is that the only requirement to create a fetcher is it must return a promise.

So here is my first implementation of a fetcher for Ethereum:

const fetcher = (library) => (...args) => {
  const [method, ...params] = args
  console.log(method, params)
  return library[method](...params)
}

As you can see, it is a partially applied function. In that way, I can inject the library ( my Web3Provider) when I configure the fetcher. Later, every time a key changes, the function can be resolved by returning the required promise.

Now I can create my <Balance/> component

export const Balance = () => {
  const { account, library } = useWeb3React<Web3Provider>()
  const { data: balance } = useSWR(['getBalance', account, 'latest'], {
    fetcher: fetcher(library),
  })
  if(!balance) {
    return <div>...</div>
  }
  return <div>Balance: {balance.toString()}</div>
}

The balance object returned is a BigNumber.

step-2-balance

As you can see, the number is not formated and extremely large. This is because Solidity uses Integer up to 256 bits.
To display the number in a human readable format, the solution is using one of the aforementioned utilities from Ether.js utilities: formatEther(balance)

yarn install @ethersproject/units

Now that I can rework my <Balance/> component to handle and format the BitInt in a human readable form:

export const Balance = () => {
  const { account, library } = useWeb3React<Web3Provider>()
  const { data: balance } = useSWR(['getBalance', account, 'latest'], {
    fetcher: fetcher(library),
  })
  if(!balance) {
    return <div>...</div>
  }
  return <div>Ξ {parseFloat(formatEther(balance)).toPrecision(4)}</div>
}

step-2-balance-formated

this what we did so far: GIT step-2

How to update the data in real-time

SWR exposes a mutate function to update its internal cache.

  const { data: balance, mutate } = useSWR(['getBalance', account, 'latest'], {
    fetcher: fetcher(library),
  })

const onClick = () => {
  mutate(new BigNumber(10), false)
}

The mutate function is automatically bound to the key (e.g. ['getBalance', account, 'latest'] from which it has been generated. It accepts two parameters. The new data and if a validation should be triggered. If it should, SWR will automatically use the fetcher to update the cache 💥

As anticipated, Solidity events give a tiny abstraction on top of the EVM’s logging functionality. Applications can subscribe and listen to these events through the RPC interface of an Ethereum client.

Ether.js has a simple API to subscribe to an event:

const { account, library } = useWeb3React<Web3Provider>()
library.on("blockNumber", (blockNumber) => {
    console.log({blockNumber})
})

Now let's combine both approaches in the new <Balance/> component

export const Balance = () => {
  const { account, library } = useWeb3React<Web3Provider>()
  const { data: balance, mutate } = useSWR(['getBalance', account, 'latest'], {
    fetcher: fetcher(library),
  })

  useEffect(() => {
    // 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
  }, [])

  if (!balance) {
    return <div>...</div>
  }
  return <div>Ξ {parseFloat(formatEther(balance)).toPrecision(4)}</div>
}

Initially, SWR will fetch the account balance, and then every time it receives a block event it will use mutate to trigger a re-fetch.

notice: We used mutate(undefined, true) because we can't retrieve from the current event the actual balance we just trigger a re-fetch of the balance.

Below is quick demo with two wallets that are exchanging some Ether.

step-3-demo

Here what we did so far: GIT step-3

How to interact with a smart contract

So far we illustrated the basics of using SWR and how to make a basic call via a Web3Provider. Let's now discover how to interact with a smart contract.

Ether.js handles smart contract interaction using the Contract Application Binary Interface (ABI) ABI generated by the Solidity Compiler.

The Contract Application Binary Interface (ABI) is the standard way to interact with contracts in the Ethereum ecosystem, both from outside the blockchain and for contract-to-contract interaction.

For example, given the below simple smart contract:

pragma solidity ^0.5.0;

contract Test {
  constructor() public { b = hex"12345678901234567890123456789012"; }
  event Event(uint indexed a, bytes32 b);
  event Event2(uint indexed a, bytes32 b);
  function foo(uint a) public { emit Event(a, b); }
  bytes32 b;
}

this is the ABI generated

[
  {
    "type": "event",
    "inputs": [
      { "name": "a", "type": "uint256", "indexed": true },
      { "name": "b", "type": "bytes32", "indexed": false }
    ],
    "name": "Event"
  },
  {
    "type": "event",
    "inputs": [
      { "name": "a", "type": "uint256", "indexed": true },
      { "name": "b", "type": "bytes32", "indexed": false }
    ],
    "name": "Event2"
  },
  {
    "type": "function",
    "inputs": [{ "name": "a", "type": "uint256" }],
    "name": "foo",
    "outputs": []
  }
]

To use the ABIs, we can simply copy them directly into your code and import them where it is required. In this demo, we will use a standard ERC20 ABI because we want to retrieve the balances of two tokens: DAI and MKR.

Next step is creating the <TokenBalance/> component

export const TokenBalance = ({ symbol, address, decimals }) => {
  const { account, library } = useWeb3React<Web3Provider>()
  const { data: balance, mutate } = useSWR([address, 'balanceOf', account], {
    fetcher: fetcher(library, ERC20ABI),
  })

  useEffect(() => {
    // listen for changes on an Ethereum address
    console.log(`listening for Transfer...`)
    const contract = new Contract(address, ERC20ABI, library.getSigner())
    const fromMe = contract.filters.Transfer(account, null)
    library.on(fromMe, (from, to, amount, event) => {
      console.log('Transfer|sent', { from, to, amount, event })
      mutate(undefined, true)
    })
    const toMe = contract.filters.Transfer(null, account)
    library.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 () => {
      library.removeAllListeners(toMe)
      library.removeAllListeners(fromMe)
    }
    // trigger the effect only on component mount
  }, [])

  if (!balance) {
    return <div>...</div>
  }
  return (
    <div>
      {parseFloat(formatUnits(balance, decimals)).toPrecision(4)} {symbol}
    </div>
  )
}

Let's zoom in. There are two main differences:

Key definition

The key, used by useSWR([address, 'balanceOf', account])), needs to start with an Ethereum address rather than a method. Because of that, the fetcher can recognize what we want to achieve and use the ABI.

Let's refactor the fetcher accordingly:

const fetcher = (library: Web3Provider, abi?: any) => (...args) => {
  const [arg1, arg2, ...params] = args
  // it's a contract
  if (isAddress(arg1)) {
    const address = arg1
    const method = arg2
    const contract = new Contract(address, abi, library.getSigner())
    return contract[method](...params)
  }
  // it's a eth call
  const method = arg1
  return library[method](arg2, ...params)
}

Now we have a general-purpose fetcher capable of interacting with the JSON-RPC calls of Ethereum. 🙌

Log filters

The other aspect in <TokenBalance/> is how to listen for the ERC20 events. Ether.js provides a handy way to configure a filter based on the topics and name of the event. More info about what is a topic can be found in the Solidity Doc

const contract = new Contract(address, ERC20ABI, library.getSigner())
const fromMe = contract.filters.Transfer(account, null)

Once you have built a contract instance with the ABI, then you can pass the filter to the library instance.

Warning:

You could be tempted to use the amount in the ERC20 event directly to increase or decrease the balance.
Be aware of the dragon. When you setup the fetcher, you passed a closjure as callback to the on function, which contained the balance value at the time.

This could be fixed using a useRef but for the sake of somplicity let's revalidate the cache to ensure the balances are fresh: mutate(undefined, true)

We now have all of the pieces required, the last bit is a bit of glue.

I configured a few constants in order to have a nice way to map my TokenBalance component to a list of tokens depending on the network where we are working:

export const Networks = {
  MainNet: 1,
  Rinkeby: 4,
  Ropsten: 3,
  Kovan: 42,
}

export interface IERC20 {
  symbol: string
  address: string
  decimals: number
  name: string
}

export const TOKENS_BY_NETWORK: {
  [key: number]: IERC20[]
} = {
  [Networks.Rinkeby]: [
    {
      address: "0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa",
      symbol: "DAI",
      name: "Dai",
      decimals: 18,
    },
    {
      address: "0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85",
      symbol: "MKR",
      name: "Maker",
      decimals: 18,
    },
  ],
}

Once we have the constants it's easy to map the configured tokens to my <TokenList/> component:

export const TokenList = ({ chainId }) => {
  return (
    <>
      {TOKENS_BY_NETWORK[chainId].map((token) => (
        <TokenBalance key={token.address} {...token} />
      ))}
    </>
  )
}

All set! Now we have an Ethereum Wallet that loads Ether and Token balances. And If the user sends or receives funds the wallet UI is updated.

step-4

Here what we did so far: GIT step-4

Refactoring

Let's move every component in a separated file and make the fetcher a globally available using SWRConfig provider.

<SWRConfig value={{ fetcher: fetcher(library, ERC20ABI) }}>
    <EthBalance />
    <TokenList chainId={chainId} />
<SWRConfig/>

With SWRConfig we can configure some options as always available, so that we can have a more convenient usage of SWR.

const {data: balance, mutate} = useSWR([address, 'balanceOf', account])

Here after the refactoring: GIT step-5

Wrap Up

SWR and Ether.js are two nice libraries to work with if you want to streamline your data fetching strategy with Ethereum Dapp.

Key Advantages

  • Declarative approach
  • Data always fresh via web sockets or SWR options
  • Avoid reinventing the wheel for state management with custom React context

If you use multiple smart contracts in your DApp and you liked this tutorial I generalised the web3 fetcher into a small util: swr-eth (Stars are appreciated 👻)

Feel free to use it and let me know what you think about this approach in the comments below

And finally, here is the full GIT repo: (https://github.com/aboutlo/swr-eth-tutorial)

Oldest comments (1)

Collapse
 
get_your_slice profile image
Slice🍕

hello, can I ask if this SWR hook works also in Next.js where data must be ready at build-time?