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 on the Ethereum mainnet.
Because of that, the frontend plays a major role. It is in charge of marshaling and unmarshaling 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 to connect to a Geth node
The simplest approach 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 testnet and use test Ether. If you need funds for testing purpose you can use a faucet e.g. https://faucet.rinkeby.io/
Basic understanding 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 a 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 Mainnet
Once you have MetaMask ready, we are going to use web3-react to handle the interaction with the network. It will give you quite a 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 to already have a few libraries, but when interacting with Ethereum 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:
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.
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
.
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
}
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 Ethereum wallets that are exchanging some ETH.
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 docs.
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
Be aware of the dragon. When you setup the fetcher, you passed a clojure as callback to the
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.
Here's 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's everything 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 👻)
And finally, here is the full GIT repo: (https://github.com/aboutlo/swr-eth-tutorial).
Get More Ethereum Tutorials Straight to Your Inbox
Subscribe to our newsletter for the latest Ethereum developer courses, tools, pro tips, and more.