A guide to redux-multicall: building a real-time dApp — Part A
This is the first of three articles where we delve into building real-time dApps using redux-multicall. The articles are structured as follows:
- Part A: This one, Focused on understanding redux-multicall and why you need it.
- Part B: Focuses on setting up redux-multicall for use in your dApp.
- Part C: The final part is focused on how to use redux-multicall for data fetching in your dApps.
Disclaimer: Reader’s Assumed Knowledge Level
Before delving into this guide, it is assumed that readers have a fundamental understanding of decentralized applications (dApps), Ethereum, and web development concepts. Familiarity with technologies such as React, Next.js, Redux, and smart contracts will be beneficial for a comprehensive grasp of the content.
This guide is tailored for web3 frontend developers with an intermediate skill level, aiming to build real-time dApps using redux-multicall. If you are new to the mentioned technologies or concepts, I recommend gaining some foundational knowledge through relevant documentation and tutorials.
Proceeding with the guide without the presumed knowledge may result in challenges in comprehending the discussed topics. Feel free to refer to additional resources to strengthen your understanding of the prerequisites mentioned.
Introduction
Over the years, we’ve employed various methods to fetch data from smart contracts when building decentralized applications. Initially, instantiating the contract with ethers or web3.js and calling the public or external function sufficed.
As our data-fetching needs grew, we began to see a problem with this approach. Say for example, you want to get all the uniswap pools a particular user have invested in. Using the aproach stated above will look similar to this.
import { ethers } from 'ethers';
import { factoryAddress } from './constants';
import factoryAbi from './abi/factory.json';
import pairAbi from './abi/pairAbi.json';
import provider from './constant/network';
async function getPairsAndBalances(userAddress: string) {
try {
// creating an instance of the uniswap V2 factory
const factoryContract = new ethers.Contract(factoryAddress, factoryAbi, provider);
// getting the total length of pools in the factory
const allPairsLength = await factoryContract.allPairsLength();
const pairAddresses: string[] = [];
// loop from 0 to the pair length and get the address of the pairs
for (let i = 0; i < allPairsLength.toNumber(); i++) {
const pairAddress = await factoryContract.allPairs(i);
pairAddresses.push(pairAddress);
}
const balances: {pairAddress: string, balance: string}[] = [];
// loop through all the pairs, and get the users balance in each of them
for (const pairAddress of pairAddresses) {
const pairContract = new ethers.Contract(pairAddress, pairAbi, provider);
const balance = await pairContract.balanceOf(userAddress);
balances.push({ pairAddress, balance: balance.toString() });
}
return balances;
} catch (error) {
console.error('Error:', error);
return [];
}
}
Before you start to think too much, as at the time of writing this article, uniswap V2 factory has about 300,000 pairs. In the code above, We have two loops, the first to get all the pair addresses, and the second to get the user balance of each of the pairs. That is about 600,000 calls to the blockchain for everytime this function is called. This is not a realistic approach, because it is not only resource-intensive but also prone to RPC server rate limit issues and potential call failures.
Also, as you may have known, using promise.all will not help either, except you want to crash your user’s web brower.
Multicall to the rescue
To address this, the concept of multicall emerged — a smart contract designed to aggregate multiple function calls into a single call, significantly reducing the number of interactions with the blockchain.
Revisiting our previous example with multicall, the code streamlined to just two calls — fetching pair addresses and user balances simultaneously.
import { ethers } from 'ethers';
import {factoryAddress, multicall_address} from './constants';
import factoryAbi from './abi/factory.json';
import multicallAbi from './abi/multicall.json';
import pairAbi from './abi/pairAbi.json';
import provider from './constant/network';
async function getPairsAndBalancesMulticall(userAddress: string) {
try {
const factoryContract = new ethers.Contract(factoryAddress, factoryAbi, provider);
const multicall = new ethers.Contract(multicall_address, multicallAbi, provider);
const allPairsLength = await factoryContract.allPairsLength();
const pairAddressesCallData: {target:string, callData: string}[]
for (let i = 0; i < allPairsLength.toNumber(); i++) {
const call = {
target: factoryAddress,
callData: factoryContract.interface.encodeFunctionData('allPairs', [i]),
};
pairAddressesCallData.push(call);
}
const pairAddressesCallResult = await multicall.aggregate(pairAddressesCallData);
const pairAddresses = pairAddressesCallResult.returnData.map(data => {
const pairAddress = ethers.utils.defaultAbiCoder.decode(['address'], data)[0];
return pairAddress;
});
const balancesCallData = pairAddresses.map(pairAddress => {
const pairContract = new ethers.Contract(pairAddress, pairAbi, provider);
const callData = pairContract.interface.encodeFunctionData('balanceOf', [userAddress]);
return { target: pairAddress, callData };
});
const result = await multicall.aggregate(balancesCallData);
const balances = result.returnData.map((data, index) => {
const balance = ethers.utils.defaultAbiCoder.decode(['uint256'], data)[0];
return { pairAddress: pairAddresses[index], balance: balance.toString() };
});
return balances;
} catch (error) {
console.error('Error:', error);
return [];
}
}
In contrast to the traditional approach, this significantly reduced the number of calls.
In case you don’t know what multicall contracts are, makerdao multicall2 is one of the most popularly used multicall contracts
The problem with the traditional multicall
While multicall was a game-changer, it introduced some issues:
- Repetitive code for every aggregation: Each call aggregation required repetitive encoding and decoding of function calls.
- Possible gas Issues for large calls: Generally,
view
andpure
functions do not cost gas when called externally, but they are limited by block gas limit. This hinders the batching of large number of calls in a single invocation. As a matter of fact, the above multicall example code will not work except we chuck the calls into smaller multiple calls as they will be too much to be aggregated into a single call. If you’re still confused as to why we need to worry about block gas limit in a read-only function calls, I recommend reading this. - It is a one-off call: Using the traditional multicall, once the call is made and the result is returned, if the data get’s updated on the smart contract, until we make the call again, our UI will still be displaying the stale data.
Redux-Multicall
According to the Uniswap team, creators of redux-multicall, it is:
A React + Redux library for fetching, batching, and caching chain state via the MultiCall contract.
Redux-multicall eliminates repetitive code, handles gas issues, and ensures data freshness, supporting multichain setups.
UniswapInterfaceMulticall
Redux-multicall is an npm library. It is worth noting that it expects that a special kind of multicall contract be passed to it in order to perform aggregation.
The uniswap team made some modification to the makerdao multicall2 contract for use in redux-multicall and they called it UniswapInterfaceMulticall
. you can find it here.
Inside redux-multicall
From the name “redux-multicall”, it is intuitive to know that it doesn’t just use multicall contract, it also uses redux under the hood and requires that you have redux set up in your application.
When you create an instance of redux-multicall (which you will do in the next part of this article), you get an object containing:
- actions
- reducerPath
- reducer
- hooks
- updater
actions
These are redux actions that are used internally by redux-multicall to add listeners, remove listeners, update call results, etc.
You will never need to use them directly as they are only used internally by redux-multicall.
reducerPath
This is just a simple string, which is the name that will be given to the redux-multicall state in your application’s redux store.
reducer
The redux-multicall reducer where all your calls and results are managed. The reducer and reducerPath are all you need to bind your redux-multicall state to your application state.
hooks
Hooks are what you use to fetch data from smart contract(s). redux-multicall gives us 6 types of hooks, any of which can be used depending on your data fetching need.
- useSingleCallResult: used for making single call to a single function of a smart contract. Example usage of this is if you want to get the balance of a user from an erc20 contract.
- useSingleContractMultipleData: formats many calls to a function on a single contract, with the function name and inputs specified. Example of this is if you want to get the balance of multiple users from an erc20 contract (notice the difference between this and useSingleCallResult).
- useMultipleContractSingleData: formats the same calls to multiple similar contracts. Example usage of this is if you want to get the balance of a user from multiple erc20 contract (notice the difference between useSingleContractMultipleData).
- useSingleContractWithCallData: formats many calls to any number of functions on a single contract, with only the calldata specified. Example usage of this is if you want to get name, symbol, decimal, balance, allowance etc from a single erc20 contract all in one call. This hook require a little bit of more work as it can only be used with callData i.e, you have to explicitly do contract.interface.encodeFunctionData(…) for all the functions you want to call.
- useMultiChainMultiContractSingleData: same as useMultipleContractSingleData but on multiple chain. This is only needed when you’re building a concurent multichain dApp. It requires that you have an updater for every chain (more on updater later). Example usage of this is if you want to get the balance of a user from multiple erc20 contract both on ethereum mainnet and binance smart chain.
- useMultiChainSingleContractSingleData: Similar to useSingleCallResult but instead of one contract on one chain, this is for querying similar contracts on multiple chains. Example usage of this is if you want to get the balance of a user from an erc20 contract both on ethereum mainnet and binance smart chain.
updater
The updater is a smart component you place at the root of your application. It does not render any DOM element, It only coordinate data fetching according to the configuration you provided. It accepts a couple of argument (props).
chainId
— the chainId of the target network.latestBlockNumber
— an ever changing value that will always be the latest block number of the target chain. This helps redux-multicall to know when data are stale and try to revalidate them. This can either be a value gotten from a block number context, or a simple useLatestBlock hookcontract
— the instance of UniswapInterfaceMulticall contract on the target chain.listenerOptions
— a configuration object with a single property:blocksPerFetch
. This tells redux-multicall how often you want your data to be revalidated. if you want it to always fetch the data again after every new block, then1
should be assigned. You have to be carefull withblocksPerFetch
on very fast chains such as optimism, arbitrum, etc, as this may cause your app to misbehave. the Uniswap team uses15
for such chains.
If you’re building a dApp that fetches data from multiple chains concurently (not app that requires user to switch chains before using the dApp on another chains), You will need to render multiple updater at the root of your appilcation, each of which must have their own different pros based on the chain they are for.
Conclusion
Redux-Multicall, a powerful tool created by the Uniswap team, alleviates the burden of maintaining dApp state consistency with smart contract state. In use for about two years in the Uniswap interface codebase, it’s also adopted by major projects in the space.
In Part B, where we’ll delve into how to set up redux-multicall for your dApp.
Questions or Clarifications? Connect on X!
If you have any questions, need clarifications, or just want to discuss, feel free to reach out to me on X at https://x.com/0xAdek. I look forward to assist and engage in conversations related to the guide.
Happy coding!