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.readStorage("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>
);
}

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
.getConstant("Babe", "ExpectedBlockTime")
.getConstant("Babe", "EpochDuration")
.readStorage("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 callApis & readStorages.

const [rewards, metadatum] = useLazyLoadQuery((builder) =>
builder
.callApis("NominationPoolsApi", "pending_rewards", [
[ADDRESS_1],
[ADDRESS_2],
[ADDRESS_3],
])
.readStorages("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.

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

const poolMetadatum = useLazyLoadQuery((builder) =>
builder.readStorages(
"NominationPools",
"Metadata",
pools.map(({ keyArgs: [poolId] }) => [poolId] as const),
),
);

return (
<section>
<h1>Pool names</h1>
<ul>
{poolMetadatum.map((metadata, index) => (
<li key={index}>{metadata.asText()}</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.callApi("NominationPoolsApi", "pending_rewards", [
account.address,
]),
);

// Or

const conditionalFunction = useLazyLoadQuery(
account === undefined
? undefined
: (builder) =>
builder.callApi("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

Certain query, like runtime API calls & reading of storage entries doesn't create any subscriptions. In order to get the latest data, they must be manually refreshed with the useLazyLoadQueryWithRefresh hook.

import { useTransition } from "react";

function QueryWithRefresh() {
const [isPending, startTransition] = useTransition();
const [pendingRewards, refreshPendingRewards] = useLazyLoadQueryWithRefresh(
(builder) =>
builder.callApi("NominationPoolsApi", "pending_rewards", [
ACCOUNT_ADDRESS,
]),
);

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

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

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

return (
<div>
{/* ... */}
<button
// Only the 2nd account rewards will be refreshed
onClick={() => startTransition(() => refreshAccount2Rewards())}
disabled={isPending}
>
Refresh
</button>
</div>
);
}

Retry failed query

Error from queries can be caught and 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={(details) => {
if (details.reason === "imperative-api") {
const [error] = details.args;
resetQueryError(error);
}
}}
>
{/* ... */}
</ErrorBoundary>
);
}