Skip to content

Commit

Permalink
feat: query refreshing
Browse files Browse the repository at this point in the history
  • Loading branch information
tien committed Jun 12, 2024
1 parent 07c3765 commit 5f1d293
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 55 deletions.
27 changes: 27 additions & 0 deletions apps/docs/docs/getting-started/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,30 @@ const Query = () => {
);
};
```

## 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 [`useQueryWithRefresh`](/api/react/function/useQueryWithRefresh) hook.

```tsx
import { useTransition } from "react";

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

return (
<div>
<p>{pendingRewards.toLocaleString()}</p>
<button
onClick={() => startTransition(() => refreshPendingRewards())}
disabled={isPending}
>
Refresh
</button>
</div>
);
};
```
49 changes: 45 additions & 4 deletions apps/example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,51 @@ import {
useConnectedWallets,
useMutation,
useQuery,
useQueryWithRefresh,
useWallets,
} from "@reactive-dot/react";
import { formatDistance } from "date-fns";
import { Binary } from "polkadot-api";
import { Suspense, useState } from "react";
import { Suspense, useState, useTransition } from "react";

const PendingPoolRewards = () => {
const accounts = useAccounts();

const [isPending, startTransition] = useTransition();
const [pendingRewards, refreshPendingRewards] = useQueryWithRefresh(
(builder) =>
builder.callApis(
"NominationPoolsApi",
"pending_rewards",
accounts.map((account) => [account.address] as const),
),
);

if (accounts.length === 0) {
return (
<article>
<h4>Pending rewards</h4>
<p>Please connect accounts to see pending rewards</p>
</article>
);
}

return (
<article>
<h4>Pending rewards</h4>
<button onClick={() => startTransition(() => refreshPendingRewards())}>
{isPending ? "Refreshing..." : "Refresh"}
</button>
<ul>
{pendingRewards.map((rewards, index) => (
<li key={index}>
{accounts.at(index)?.address}: {rewards.toLocaleString()} planck
</li>
))}
</ul>
</article>
);
};

const Query = () => {
const block = useBlock();
Expand Down Expand Up @@ -69,26 +109,27 @@ const Query = () => {
</article>
<article>
<h4>Total issuance</h4>
<p>{totalIssuance.toString()} planck</p>
<p>{totalIssuance.toLocaleString()} planck</p>
</article>
<article>
<h4>Bonding duration</h4>
<p>{formatDistance(0, bondingDurationMs)}</p>
</article>
<article>
<h4>Total value staked</h4>
<p>{totalStaked?.toString()} planck</p>
<p>{totalStaked?.toLocaleString()} planck</p>
</article>
<article>
<h4>Total value locked in nomination Pools</h4>
<p>{totalValueLocked.toString()} planck</p>
<p>{totalValueLocked.toLocaleString()} planck</p>
</article>
<article>
<h4>First 4 pools</h4>
{poolMetadatum.map((x, index) => (
<p key={index}>{x.asText()}</p>
))}
</article>
<PendingPoolRewards />
</section>
);
};
Expand Down
14 changes: 14 additions & 0 deletions apps/example/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
:root {
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
}
1 change: 1 addition & 0 deletions apps/example/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import App from "./App.js";
import "./index.css";
import React from "react";
import ReactDOM from "react-dom/client";

Expand Down
94 changes: 79 additions & 15 deletions packages/react/src/hooks/useQuery.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ChainIdContext } from "../context.js";
import { queryPayloadAtomFamily } from "../stores/query.js";
import {
getQueryInstructionPayloadAtoms,
queryPayloadAtomFamily,
} from "../stores/query.js";
import type { Falsy, FalsyGuard, FlatHead } from "../types.js";
import { flatHead, stringify } from "../utils.js";
import type { ChainHookOptions } from "./types.js";
Expand All @@ -11,16 +14,17 @@ import {
} from "@reactive-dot/core";
import type { ChainId, Chains, ReDotDescriptor } from "@reactive-dot/types";
import { atom, useAtomValue } from "jotai";
import { useContext, useMemo } from "react";
import { useAtomCallback } from "jotai/utils";
import { useCallback, useContext, useMemo } from "react";

/**
* Hook for querying data from chain, and returning the response.
* Hook for querying data from chain, returning the response & a refresher function.
*
* @param builder - The function to create the query
* @param options - Additional options
* @returns The data response
* @returns The data response & a function to refresh it
*/
export const useQuery = <
export const useQueryWithRefresh = <
TQuery extends
| ((
builder: Query<[], TDescriptor>,
Expand All @@ -33,14 +37,17 @@ export const useQuery = <
>(
builder: TQuery,
options?: ChainHookOptions,
): TQuery extends false
? undefined
: FalsyGuard<
ReturnType<Exclude<TQuery, Falsy>>,
FlatHead<
InferQueryPayload<Exclude<ReturnType<Exclude<TQuery, Falsy>>, Falsy>>
>
> => {
): [
data: TQuery extends false
? undefined
: FalsyGuard<
ReturnType<Exclude<TQuery, Falsy>>,
FlatHead<
InferQueryPayload<Exclude<ReturnType<Exclude<TQuery, Falsy>>, Falsy>>
>
>,
refresh: () => void,
] => {
const contextChainId = useContext(ChainIdContext);
const chainId = options?.chainId ?? contextChainId;

Expand All @@ -58,8 +65,7 @@ export const useQuery = <
[query],
);

// @ts-expect-error complex type
return flatHead(
const data = flatHead(
useAtomValue(
useMemo(
() =>
Expand All @@ -74,4 +80,62 @@ export const useQuery = <
),
),
);

const refresh = useAtomCallback(
useCallback(
(_, set) => {
if (!query) {
return;
}

const atoms = getQueryInstructionPayloadAtoms(chainId, query).flat();

for (const atom of atoms) {
if ("write" in atom) {
set(atom);
}
}
},
[chainId, query],
),
);

return [
// @ts-expect-error complex type
data,
refresh,
];
};

/**
* Hook for querying data from chain, and returning the response.
*
* @param builder - The function to create the query
* @param options - Additional options
* @returns The data response
*/
export const useQuery = <
TQuery extends
| ((
builder: Query<[], TDescriptor>,
) => Query<QueryInstruction<TDescriptor>[], TDescriptor> | Falsy)
| Falsy,
TDescriptor extends TChainId extends void
? ReDotDescriptor
: Chains[Exclude<TChainId, void>],
TChainId extends ChainId | void = void,
>(
builder: TQuery,
options?: ChainHookOptions,
): TQuery extends false
? undefined
: FalsyGuard<
ReturnType<Exclude<TQuery, Falsy>>,
FlatHead<
InferQueryPayload<Exclude<ReturnType<Exclude<TQuery, Falsy>>, Falsy>>
>
> => {
const [data] = useQueryWithRefresh(builder, options);

return data;
};
2 changes: 1 addition & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ export { useAccounts } from "./hooks/useAccounts.js";
export { useBlock } from "./hooks/useBlock.js";
export { useClient } from "./hooks/useClient.js";
export { useMutation } from "./hooks/useMutation.js";
export { useQuery } from "./hooks/useQuery.js";
export { useQuery, useQueryWithRefresh } from "./hooks/useQuery.js";
export { useTypedApi } from "./hooks/useTypedApi.js";
export { useConnectedWallets, useWallets } from "./hooks/useWallets.js";
Loading

0 comments on commit 5f1d293

Please sign in to comment.