Async Patterns in React (PolkadotJS)

September 5, 2023 (1y ago)

Updated in May 2024

Leveraging TanStack for Async Operations

In my recent work on bitLauncher.ai, I've started using Wagmi, and I really appreciate its design and feature set. This shift has led me to use the useQuery hook more frequently than the useAsync helpers I used previously in the PolkadotJS examples.

When working with React, I now recommend using TanStack (formerly known as React Query) for managing asynchronous operations and state. TanStack Query offers powerful caching, request deduplication, and persistence, which are essential for a seamless developer experience.

Wagmi leverages TanStack Query for caching, request deduplication, and persistence. It provides over 20 hooks for working with wallets, ENS, contracts, transactions, signing, and more. Additionally, Wagmi includes built-in wallet connectors for injected providers, WalletConnect, MetaMask, and Coinbase Wallet. Major projects like Coinbase, Stripe, Shopify, Uniswap, Optimism, ENS, and Sushi use Wagmi, demonstrating its robustness and reliability.

The new Parity Capi Multisig App Parity Capi Multisig App also uses TanStack internally.


In this post, we'll explore patterns that help achieve a clean separation between core logic and presentation in your projects. By keeping your core logic in Vanilla TypeScript and utilizing the useAsync and useAsyncFn hooks for asynchronous operations, you can build a solid foundation for your applications.

Within a services directory, you'll store plain JavaScript functions that handle interactions with external APIs and storage systems. These functions are designed to manage HTTP requests, WebSocket communications, web storage, and API queries effectively.

In the examples that follow, I'll show how to consume the PolkadotJS vanilla service from React components using this approach.

import { ApiPromise, WsProvider } from '@polkadot/api';
import { executePromisesInSeries } from '~/lib/async';
 
const wsProvider = new WsProvider('wss://rpc.polkadot.io');
 
export async function getApi(){
  return ApiPromise.create({ provider: wsProvider });
}
 
export async function getChain(){
  const api = await getApi();
  return api.rpc.system.chain();
}
 
export async function getLatestHeader() {
  const api = await getApi();
  return api.rpc.chain.getHeader();
}
 
export async function getGenisisHash(){
  const api = await getApi();
  return api.genesisHash.toHex();
}
 
export async function someVanillaSeries(){
  const tasks = [
    getChain, 
    getGenisisHash, 
    getChain
  ];
  return executePromisesInSeries(tasks);
}
 
export async function someVanillaParallel(){
  const result = await Promise.all([getGenisisHash(), getChain()]);
  return await result;
}
 
export async function someVanillaConditional(){
  const chain = await getChain();
  const genesisHash = chain ? await getGenisisHash() : null;
  return genesisHash;
}

If you'd like to avoid reconnecting to the WebSocket with each service call, you have the option to utilize the global window scope to maintain a reference to your connected API client. For instance, you can use window.myappscope.api.

Async Calls on Component Mounted

import { useAsync } from "~/hooks/use-async-fn";
import { someVanillaConditional, someVanillaParallel, someVanillaSeries } from "~/services/chain";
 
export function AsyncOnComponentMounted(){
  const conditionalCallState = useAsync(someVanillaConditional);
  const parallelCallState = useAsync(someVanillaParallel);
  const seriesCallState = useAsync(someVanillaSeries);
  
  return (
    <div>
      <h1>Async on Component Mounted Conditionally</h1>
      <pre>{JSON.stringify(conditionalCallState)}</pre>
 
      <h1>Async on Component Mounted in Parallel</h1>
      <pre>{JSON.stringify(parallelCallState, null, 2)}</pre>
 
      <h1>Async on Component Mounted in Series</h1>
      <pre>{JSON.stringify(seriesCallState, null, 2)}</pre>
    </div>
  );
}

Async Calls on Browser Events

import { useAsyncFn } from "~/hooks/use-async-fn";
import { someVanillaConditional, someVanillaParallel, someVanillaSeries } from "~/services/chain";
 
export function AsyncOnEvent(){
  const [conditionalCallState, callConditionalFn] = useAsyncFn(someVanillaConditional);
  const [parallelCallState, callParallelFn] = useAsyncFn(someVanillaParallel);
  const [seriesCallState, callSeriesFn] = useAsyncFn(someVanillaSeries);
  
  return (
    <div>
      <h1>Async on Event Conditionally</h1>
      <button onClick={callConditionalFn}>Get data conditionally</button>
      <pre>{JSON.stringify(conditionalCallState)}</pre>
 
      <h1>Async on Event in Parallel</h1>
      <button onClick={callParallelFn}>Get data in parallel</button>
      <pre>{JSON.stringify(parallelCallState, null, 2)}</pre>
 
       <h1>Async on Event in Series</h1>
      <button onClick={callSeriesFn}>Get data in series</button>
      <pre>{JSON.stringify(seriesCallState, null, 2)}</pre>
    </div>
  );
}

I've incorporated the useAsync and useAsyncFn functions from a fantastic library called react-use. These functions are designed to handle errors internally, simplifying your code. Each call state variable encompasses { error, loading, data } from your asynchronous operation. As a result, you can eliminate the need for cumbersome try/catch statements and manual loading variable updates.

For more examples and code snippets, check out the GitHub repository.