Async Patterns in React (PolkadotJS)

Updated in May 2024

On bitlauncher.ai I started using Wagmi, and I like its design enough that I now reach for useQuery more often than the useAsync helpers in the PolkadotJS examples below.

TanStack for async operations

In React, I use TanStack Query (formerly React Query) for asynchronous operations and server state, because it handles caching, request deduplication, and persistence.

Wagmi is built on TanStack Query for those same primitives. It ships over 20 hooks for wallets, ENS, contracts, transactions, signing, and more, plus connectors for injected providers, WalletConnect, MetaMask, and Coinbase Wallet. Coinbase, Stripe, Shopify, Uniswap, Optimism, ENS, and Sushi use it in production.

The Parity Capi Multisig App also uses TanStack internally.

The rest of this post is the older PolkadotJS pattern: keep core logic in vanilla TypeScript and use useAsync / useAsyncFn at the React edge.

A services directory holds plain functions that talk to external APIs and storage: HTTP requests, WebSocket connections, web storage, and API queries. The examples that follow consume a vanilla PolkadotJS service from React components.

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;
}

To avoid reconnecting to the WebSocket on every service call, you can keep a reference to the connected API client in the global window scope, for instance window.myappscope.api.

Async calls when a component mounts

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>
  );
}

The useAsync and useAsyncFn hooks come from react-use, and they handle errors internally, so each call state carries { error, loading, data } from the async operation and the component needs no try/catch blocks or manual loading flags. More examples are in the GitHub repository.

Related writing