Skip to main content

Query

The useLazyLoadQuery hook allow you to read any data from chain, while maintaining updates, concurrency, caching & deduplication behind the scene for you.

Async handling​

useLazyLoadQuery utilize React's Suspense API for data fetching & error handling.

function ActiveEra() {
const activeEra = useLazyLoadQuery((builder) => builder.storage("Staking", "ActiveEra"));

return <div>Active era: {activeEra}</div>;
}

function App() {
return (
<ErrorBoundary fallback="Error fetching active era!">
<Suspense fallback="Fetching active era...">
<ActiveEra />
</Suspense>
</ErrorBoundary>
);
}
tip

For advanced async handling, see this guide.

Fetching multiple data​

Fetching multiple data can be done by chaining queries together, useLazyLoadQuery (with TypeScript) will automatically infer that you want to fetch multiple data concurrently & will return an array of data instead.

function MultiQuery() {
const [expectedBlockTime, epochDuration, proposalCount] = useLazyLoadQuery((builder) =>
builder
.constant("Babe", "ExpectedBlockTime")
.constant("Babe", "EpochDuration")
.storage("Treasury", "ProposalCount"),
);

return (
<dl>
<dt>Expected block time</dt>
<dd>{expectedBlockTime}</dd>

<dt>Epoch duration</dt>
<dd>{epochDuration}</dd>

<dt>Proposal count</dt>
<dd>{proposalCount}</dd>
</dl>
);
}

Multiple queries of the same type can also be fetched using runtimeApis & storages.

const [rewards, metadatum] = useLazyLoadQuery((builder) =>
builder
.runtimeApis("NominationPoolsApi", "pending_rewards", [[ADDRESS_1], [ADDRESS_2], [ADDRESS_3]])
.storages("NominationPools", "Metadata", [[POOL_ID_1], [POOL_ID_2], [POOL_ID_3]]),
);

Dependent queries​

Result of a query can be used as variables in subsequent queries.

import { Binary } from "polkadot-api";

function Query() {
const pools = useLazyLoadQuery((builder) =>
builder.storageEntries("NominationPools", "BondedPools"),
);

const poolMetadatum = useLazyLoadQuery((builder) =>
builder.storages(
"NominationPools",
"Metadata",
pools.map(([[poolId], _]) => [poolId] as const),
),
);

return (
<section>
<h1>Pool names</h1>
<ul>
{poolMetadatum.map((metadata, index) => (
<li key={index}>{Binary.toText(metadata)}</li>
))}
</ul>
</section>
);
}

Conditional query​

Use a falsy value (undefined, null or false) to conditionally fetch data. If the query builder returns or itself is a falsy value, ReactiveDOT will not execute the query.

import { idle } from "@reactive-dot/core";

const conditionalReturn = useLazyLoadQuery((builder) =>
account === undefined
? undefined
: builder.runtimeApi("NominationPoolsApi", "pending_rewards", [account.address]),
);

// Or

const conditionalFunction = useLazyLoadQuery(
account === undefined
? undefined
: (builder) => builder.runtimeApi("NominationPoolsApi", "pending_rewards", [account.address]),
);

// Result will be `idle` if the query hasn't been executed
if (conditionalReturn === idle || conditionalFunction === idle) {
console.log("Queries are in idle state");
}

Refreshing queries​

Data from constant queries never changes, while storage and storage-entries queries automatically subscribe to updates. For all other query types, you must manually refresh them to get the latest data by updating the fetchKey option.

import { useLazyLoadQuery } from "@reactive-dot/react";
import { useState, useTransition } from "react";

function QueryWithRefresh() {
const [fetchCount, setFetchCount] = useState(0);
const [isPending, startTransition] = useTransition();

const pendingRewards = useLazyLoadQuery(
(builder) => builder.runtimeApi("NominationPoolsApi", "pending_rewards", [ACCOUNT_ADDRESS]),
{ fetchKey: fetchCount },
);

return (
<div>
<p>{pendingRewards.toLocaleString()}</p>
<button
onClick={() => startTransition(() => setFetchCount((count) => count + 1))}
disabled={isPending}
>
Refresh
</button>
</div>
);
}

The above will refresh all refreshable data in the query. If you want to target specific data to refresh, the useStore hook can be used instead.

import { useLazyLoadQuery, useStore } from "@reactive-dot/react";
import { useTransition } from "react";

function QueryWithRefresh() {
const store = useStore();
const [isPending, startTransition] = useTransition();
const [account1Rewards, account2Rewards] = useLazyLoadQuery((builder) =>
builder
.runtimeApi("NominationPoolsApi", "pending_rewards", [ACCOUNT_ADDRESS_1])
.runtimeApi("NominationPoolsApi", "pending_rewards", [ACCOUNT_ADDRESS_2]),
);

return (
<div>
{/* ... */}
<button
// Only the 2nd account rewards will be refreshed
onClick={() =>
startTransition(() =>
store.invalidateQuery((builder) =>
builder.runtimeApi("NominationPoolsApi", "pending_rewards", [ACCOUNT_ADDRESS_2]),
),
)
}
disabled={isPending}
>
Refresh
</button>
</div>
);
}

Retry failed query​

Error from queries can be reset using ErrorBoundary & useQueryErrorResetter hook.

import { useQueryErrorResetter } from "@reactive-dot/react";
import { ErrorBoundary, type FallbackProps } from "react-error-boundary";

function ErrorFallback(props: FallbackProps) {
return (
<article>
<header>Oops, something went wrong!</header>
<button onClick={() => props.resetErrorBoundary(props.error)}>Retry</button>
</article>
);
}

function AppErrorBoundary() {
const resetQueryError = useQueryErrorResetter();

return (
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => resetQueryError()}>
{/* ... */}
</ErrorBoundary>
);
}