A guide to redux-multicall: building a real-time dApp — Part C

Adekunle Michael Ajayi
10 min readDec 24, 2023

--

Photo by Toluwase S. Owononi

This is the third and last of the three articles where we dive into building real-time dApps using redux-multicall.

In Part A, we focused on understanding redux-multicall and why you would want to use it.

In Part B, we walked through setting up redux-multicall for use in our application.

The final part, which is this, is focused on how to actually use redux-multicall to fetch data in our dApps. So let’s get started.

In redux-multicall, calls are made through hooks. We have about 6 hooks, each of which are used in different scenerio depending on the kind of aggregation you want to perfom.

In Part A, we mentioned that two of the hooks (useMultiChainMultiContractSingleData and useMultiChainSingleContractSingleData) are used for fetching data concurrently on mutilple chain while the remaining four are used on a single chain, in this aritcle, our focus will be on the other four. If you’re building a multichain, using the other two is also pretty straightforward. You can take a look at the integration test example here

To make this as straightforward as possible, we shall continue with the setup we did in Part B. So if you didn’t follow, you can clone the setup repository here.

After cloning, install dependencies and start the application. The screen should still be blank but check the console to be sure that there are no errors.

What we will be building

We’ll try to build a simple application that shows the first 1000 pools in unswap v2. clicking any of the pools should take you to a page dedicated to the pool. On that page, you will be able to see all the info of that particular pool.

To begin, if you take a closer look at the hooks in the multicall objects that we got from the createMulticall function from Part B of this article, you’ll notice that most of the hooks (All except the ones used in multichain dApp) share the same first two argument, i.e, chainId and latestBlock. In order to avoid passing these two arguments repeatedly every time we use this hooks, we are going to create a file named hooks.ts inside the src/muticall directory. and paste the following.

import { useLatestBlock } from "@/hooks/useLatestBlock";
import multicall from ".";
import { provider } from "@/constants/provider";

type TupleSplit<
T,
N extends number,
O extends readonly any[] = readonly []
> = O["length"] extends N
? [O, T]
: T extends readonly [infer F, ...infer R]
? TupleSplit<readonly [...R], N, readonly [...O, F]>
: [O, T];

export type SkipFirst<T extends readonly any[], N extends number> = TupleSplit<
T,
N
>[1];

type SkipFirstTwoParams<T extends (...args: any) => any> = SkipFirst<
Parameters<T>,
2
>;

export function useMultipleContractSingleData(
...args: SkipFirstTwoParams<
typeof multicall.hooks.useMultipleContractSingleData
>
) {
const latestBlock = useLatestBlock(provider);
return multicall.hooks.useMultipleContractSingleData(
1,
latestBlock,
...args
);
}

export function useSingleCallResult(
...args: SkipFirstTwoParams<typeof multicall.hooks.useSingleCallResult>
) {
const latestBlock = useLatestBlock(provider);
return multicall.hooks.useSingleCallResult(1, latestBlock, ...args);
}

export function useSingleContractMultipleData(
...args: SkipFirstTwoParams<
typeof multicall.hooks.useSingleContractMultipleData
>
) {
const latestBlock = useLatestBlock(provider);
return multicall.hooks.useSingleContractMultipleData(
1,
latestBlock,
...args
);
}

export function useSingleContractWithCallData(
...args: SkipFirstTwoParams<
typeof multicall.hooks.useSingleContractWithCallData
>
) {
const latestBlock = useLatestBlock(provider);
return multicall.hooks.useSingleContractWithCallData(
1,
latestBlock,
...args
);
}

In the file above, we basically just re-exported the four hooks we are interested in with the chainid and latestBlock pre-applied so that we don’t have to pass them in all the time.

The TupleSplit, SkipFirst, and SkipFirstTwoParams type were copied from stackoverflow

Next is to get and render all the pools from the uniswap v2 factory. We’ll need an instance of the factory contract, so let’s add the address to the constants file, and also import its abi.

Inside src/constants/index.ts Add this at the end of the file

export const uniswapV2FactoryAddress =
"0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f";

The file should now look like this

Next, create a json file named uniswapV2FactoryAbi.json inside src/abi directory and paste in the abi of the uniswapV2Factory contract which can be found in the Contract Abi section here

Now, to fetch the first 1000 pools, create a file called usePools.ts inside src/hooks directory and paste in the following code

import { provider } from "@/constants/provider";
import { Contract } from "@ethersproject/contracts";
import V2factoryAbi from "../abis/uniswapV2FactoryAbi.json";
import { useSingleContractMultipleData } from "@/multicall/hooks";
import { uniswapV2FactoryAddress } from "@/constants";
import { useMemo } from "react";

export const usePools = (): { loaded: boolean; poolAddresses: string[] } => {
const V2FactoryContract = new Contract(
uniswapV2FactoryAddress,
V2factoryAbi,
provider
);

const poolsIndexArray = Array.from({ length: 1000 }, (_, index) => [index]);

const poolsCall = useSingleContractMultipleData(
V2FactoryContract,
"allPairs",
poolsIndexArray
);

const loaded = useMemo(
() => poolsCall.every((call) => !call?.loading),
[poolsCall]
);

const poolAddresses = useMemo(
() => poolsCall.map((call) => call?.result?.[0]),
[poolsCall]
);
return { loaded, poolAddresses: loaded ? poolAddresses : [] };
};

The usePools hook above demonstrates a typical use of useMultipleContractSingleData hook.

useMultipleContractSingleData works for when you want to call the same function in a contract repeatedly passing in different parameters.

It first creates an instance of the uniswap V2 factory. to get the first 1000 pools from the factory, we have to call the allPairs function passing in 0, 1, …., 999. This is why we create an array of 1000 arrays, where each inner array contains a number from 0 – 999 (i.e [[0], [1], [2], …,[999]]). we put each number in its own array because redux-multicall hooks expects our parameters to be in an array.

The useSingleContractMultipleData hook takes in the contract instance, the function to be called and the arrays of parameters to be passed. In this case, redux-multicall will call the allPairs function of V2FactoryContract 1000 times, passing in 0, 1, 2, …, 999.

The bolean variable, loaded, checks to see if all the data has been fetched (i.e, none is still loading) and eventually, a tuple is returned containing loaded and poolAddresses.

Now, to render our pools, lets head to src/page/index.tsx and replace its content with the following:

import { usePools } from "@/hooks/usePools";
import { Inter } from "next/font/google";
import Link from "next/link";

const inter = Inter({ subsets: ["latin"] });

export default function Home() {
const { loaded, poolAddresses } = usePools();

return (
<main className={`min-h-screen p-24 ${inter.className}`}>
<h1 className="text-2xl font-bold">Uniswap V2 Pools</h1>
<div className="mt-4">
{loaded ? (
poolAddresses.map((pool, index) => (
<div className="py-1" key={index}>
<Link
className="hover:text-blue-500"
href={`/pool/${pool}`}
>
{pool}
</Link>
</div>
))
) : (
<div>
<span>loading...</span>
</div>
)}
</div>
</main>
);
}

This makes the index page render loading if the data is still loading, otherwise, it renders the 1000 pool address, each as a link to its own page. we do not have the page yet.

What we want is that when any of the pool is clicked, it should take us to a page where we see info about the pool.

For this we will be needing two more abis. the pool contract abi and an erc20 abi.

Inside the src/abi directory, create a file namedpool.json and paste in the abi in the Contract abi section of this page

For the erc20, inside the same src/abi directory, create another file named erc20.json, and paste in the abi here. It is a standard erc20 abi. any erc20 abi will work fine too.

Now, create a file named usePool.ts inside the src/hook directory and paste the following.

import {
useMultipleContractSingleData,
useSingleCallResult,
} from "@/multicall/hooks";
import { Contract } from "@ethersproject/contracts";
import poolAbi from "../abis/pool.json";
import erc20Abi from "../abis/erc20.json";
import { provider } from "@/constants/provider";
import { useMemo } from "react";
import { Interface } from "@ethersproject/abi";
import { formatEther, formatUnits } from "@ethersproject/units";

export const usePool = (address: string | undefined) => {
const poolContract = useMemo(
() => (address ? new Contract(address, poolAbi, provider) : null),
[address]
);
const token0Call = useSingleCallResult(poolContract, "token0");
const token1Call = useSingleCallResult(poolContract, "token1");
const totalSupplyCall = useSingleCallResult(poolContract, "totalSupply");
const reservesCall = useSingleCallResult(poolContract, "getReserves");

const tokensAddresses = useMemo(
() => [token0Call?.result?.[0], token1Call?.result?.[0]],
[token0Call?.result, token1Call?.result]
);
const ERC20_INTERFACE = new Interface(erc20Abi);

const tokensNamesCalls = useMultipleContractSingleData(
tokensAddresses,
ERC20_INTERFACE,
"name"
);
const tokensSymbolsCalls = useMultipleContractSingleData(
tokensAddresses,
ERC20_INTERFACE,
"symbol"
);
const tokensDecimalsCalls = useMultipleContractSingleData(
tokensAddresses,
ERC20_INTERFACE,
"decimals"
);

return useMemo(
() =>
token0Call.loading ||
token1Call.loading ||
totalSupplyCall.loading ||
reservesCall.loading ||
tokensNamesCalls.some((call) => call.loading) ||
tokensSymbolsCalls.some((call) => call.loading) ||
tokensDecimalsCalls.some((call) => call.loading)
? { loading: true, data: null }
: {
loading: false,
data: {
token0Name: tokensNamesCalls[0].result?.[0],
token1Name: tokensNamesCalls[1].result?.[0],
token0Symbol: tokensSymbolsCalls[0].result?.[0],
token1Symbol: tokensSymbolsCalls[1].result?.[0],
token0Decimals: tokensDecimalsCalls[0].result?.[0],
token1Decimals: tokensDecimalsCalls[1].result?.[0],
reserve0: reservesCall?.result
? formatUnits(
reservesCall?.result?._reserve0,
tokensDecimalsCalls[0].result?.[0]
)
: 0,
reserve1: reservesCall?.result
? formatUnits(
reservesCall?.result?._reserve1,
tokensDecimalsCalls[1].result?.[0]
)
: 0,
totalSupply: totalSupplyCall.result
? formatEther(totalSupplyCall.result?.[0])
: 0,
},
},
[
reservesCall.loading,
reservesCall?.result,
token0Call.loading,
token1Call.loading,
tokensDecimalsCalls,
tokensNamesCalls,
tokensSymbolsCalls,
totalSupplyCall.loading,
totalSupplyCall.result,
]
);
};

In this file, we’re using @ethersproject/units which we haven’t installed, do well to install it.

A couple of things are going on in this hook, but what you need to pay close attention to is the use of the useSingleCallResult and useMultipleContractSingleData hooks and their differences. We have seen useSingleContractMultipleData in action in the usePools hook.

The useSingleCallResult takes in an instance of the contract whose function you want to call. Next, it takes in the name of the function you want to call in form of a string. Optionally, if the function requires parameters to be passed, it takes in the parameters in an array. Also optionally, it takes in an option object in which you can manually specify listenerOption and gas.

An interesting thing to know is that the calls made with useMultipleContractSingleData hook here, can be done slightly differently with useSingleContractWithCallData. With useSingleContractWithCallData, the name, symbol, and decimal for a token contract can be gotten in one call. This means that instead of using three useMultipleContractSingleData, two useMultipleContractSingleData will also get the job done.

Likewise, the four useSingleCallResult used for calling token0, token1, totalSupply, and getReserves from the factory contract can be replaced with just one useSingleContractWithCallData.We didn’t use it in this guide to avoid too much complexities as it requires encoding every call into calldata.

But to show useSingleContractWithCallData in action, I have re-written our usePool hook using just 3 useSingleContractWithCallData hooks. You can find it in this github gist. If you paste it in the usePool.ts file, it still works the same way.

Now, back to our dApp, remember how we rendered our pools in the index.tsx page. clicking on any of the link takes you to /pool/<pooladdress>. This page does not exist yet, so let’s create it.

Inside the src/pages directory, create another directory called pool, and inside it, create a file with the name [address].tsx

This is how you create dynamic routes in next.js

Inside the [address].tsx file, paste the following code

import { usePool } from "@/hooks/usePool";
import { useRouter } from "next/router";
import React from "react";
import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });

const Pool = () => {
const router = useRouter();
const { address } = router.query;

const { loading, data } = usePool(address as string | undefined);
const {
reserve0,
reserve1,
token0Symbol,
token1Symbol,
token0Name,
token1Name,
totalSupply,
} = data || {};

return (
<main className={`min-h-screen p-24 ${inter.className}`}>
<div className="flex flex-col gap-2 mb-6">
<span className="font-bold text-xl">Address: </span>
<span>{loading ? "loading..." : address}</span>
</div>
<div className="flex flex-col gap-2 mb-6">
<span className="font-bold text-xl">Token0: </span>
<span>{loading ? "loading..." : token0Name}</span>
</div>
<div className="flex flex-col gap-2 mb-6">
<span className="font-bold text-xl">Token1: </span>
<span>{loading ? "loading..." : token1Name}</span>
</div>
<div className="flex flex-col gap-2 mb-6">
<span className="font-bold text-xl">Reserve0: </span>
<span>
{loading ? "loading..." : `${reserve0} ${token0Symbol}`}
</span>
</div>
<div className="flex flex-col gap-2 mb-6">
<span className="font-bold text-xl">Reserve1: </span>
<span>
{loading ? "loading..." : `${reserve1} ${token1Symbol}`}
</span>
</div>
<div className="flex flex-col gap-2 mb-6">
<span className="font-bold text-xl">Total Supply: </span>
<span>{loading ? "loading..." : totalSupply}</span>
</div>
</main>
);
};

export default Pool;

Here, we pass the dynamic path (i.e, the address of the pool) into the usePool hook, which then in turn return the info about the pool, and then finally, we render the info on the page.

This demonstrates how to use redux-multicall in your dApp. all the data fetched using redux-multicall will automatically be updated on every new block and you won’t have to worry about how to keep your dApp updated.

Conclusion

In this three-part series, we delved into the intricacies of building real-time decentralized applications (dApps) using redux-multicall. Part A provided a foundational understanding of redux-multicall and its significance. In Part B, we walked through the process of setting up redux-multicall for our application. Finally, in this concluding Part C, our focus was on the practical application of redux-multicall for fetching data in our dApps.

We explored the various hooks offered by redux-multicall, with a specific emphasis on four hooks tailored for use on a single chain. By creating a dedicated file for these hooks and pre-applying chainId and latestBlock, we streamlined the usage of these hooks, enhancing code readability and efficiency.

The ultimate goal was to build a simple application showcasing the first 1000 pools in Uniswap v2. Each pool is clickable, leading to a dedicated page displaying information about that specific pool. We demonstrated the use of hooks like useSingleCallResult, useSingleContractMultipleData, useMultipleContractSingleDatato fetch and render this data efficiently. we also understood the slight complexity introduced in the use of useSingleContractWithCallData and how it can reduce the number of calls, if used.

In summary, this series provides a comprehensive guide to leveraging redux-multicall for building real-time dApps, emphasizing best practices and efficient code organization. As a result, developers can ensure their applications stay updated seamlessly with each new block, providing a smooth and responsive user experience in the ever-evolving landscape of decentralized applications.

If you’re interested in exploring the code and trying it out for yourself, you can find the repository here.

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!

--

--

Adekunle Michael Ajayi
Adekunle Michael Ajayi

Responses (1)