diff --git a/apps/docs/docs/getting-started/query.md b/apps/docs/docs/getting-started/query.md
index 6448ad75..a646fe28 100644
--- a/apps/docs/docs/getting-started/query.md
+++ b/apps/docs/docs/getting-started/query.md
@@ -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 (
+
+
{pendingRewards.toLocaleString()}
+
+
+ );
+};
+```
diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx
index a3ffad50..df831df8 100644
--- a/apps/example/src/App.tsx
+++ b/apps/example/src/App.tsx
@@ -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 (
+
+ Pending rewards
+ Please connect accounts to see pending rewards
+
+ );
+ }
+
+ return (
+
+ Pending rewards
+
+
+ {pendingRewards.map((rewards, index) => (
+ -
+ {accounts.at(index)?.address}: {rewards.toLocaleString()} planck
+
+ ))}
+
+
+ );
+};
const Query = () => {
const block = useBlock();
@@ -69,7 +109,7 @@ const Query = () => {
Total issuance
- {totalIssuance.toString()} planck
+ {totalIssuance.toLocaleString()} planck
Bonding duration
@@ -77,11 +117,11 @@ const Query = () => {
Total value staked
- {totalStaked?.toString()} planck
+ {totalStaked?.toLocaleString()} planck
Total value locked in nomination Pools
- {totalValueLocked.toString()} planck
+ {totalValueLocked.toLocaleString()} planck
First 4 pools
@@ -89,6 +129,7 @@ const Query = () => {
{x.asText()}
))}
+
);
};
diff --git a/apps/example/src/index.css b/apps/example/src/index.css
new file mode 100644
index 00000000..928218de
--- /dev/null
+++ b/apps/example/src/index.css
@@ -0,0 +1,14 @@
+:root {
+ font-family:
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ Roboto,
+ Oxygen,
+ Ubuntu,
+ Cantarell,
+ "Open Sans",
+ "Helvetica Neue",
+ sans-serif;
+}
diff --git a/apps/example/src/index.tsx b/apps/example/src/index.tsx
index e4ba30a4..f69e4358 100644
--- a/apps/example/src/index.tsx
+++ b/apps/example/src/index.tsx
@@ -1,4 +1,5 @@
import App from "./App.js";
+import "./index.css";
import React from "react";
import ReactDOM from "react-dom/client";
diff --git a/packages/react/src/hooks/useQuery.ts b/packages/react/src/hooks/useQuery.ts
index 2536e762..d3ee560e 100644
--- a/packages/react/src/hooks/useQuery.ts
+++ b/packages/react/src/hooks/useQuery.ts
@@ -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";
@@ -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>,
@@ -33,14 +37,17 @@ export const useQuery = <
>(
builder: TQuery,
options?: ChainHookOptions,
-): TQuery extends false
- ? undefined
- : FalsyGuard<
- ReturnType>,
- FlatHead<
- InferQueryPayload>, Falsy>>
- >
- > => {
+): [
+ data: TQuery extends false
+ ? undefined
+ : FalsyGuard<
+ ReturnType>,
+ FlatHead<
+ InferQueryPayload>, Falsy>>
+ >
+ >,
+ refresh: () => void,
+] => {
const contextChainId = useContext(ChainIdContext);
const chainId = options?.chainId ?? contextChainId;
@@ -58,8 +65,7 @@ export const useQuery = <
[query],
);
- // @ts-expect-error complex type
- return flatHead(
+ const data = flatHead(
useAtomValue(
useMemo(
() =>
@@ -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[], TDescriptor> | Falsy)
+ | Falsy,
+ TDescriptor extends TChainId extends void
+ ? ReDotDescriptor
+ : Chains[Exclude],
+ TChainId extends ChainId | void = void,
+>(
+ builder: TQuery,
+ options?: ChainHookOptions,
+): TQuery extends false
+ ? undefined
+ : FalsyGuard<
+ ReturnType>,
+ FlatHead<
+ InferQueryPayload>, Falsy>>
+ >
+ > => {
+ const [data] = useQueryWithRefresh(builder, options);
+
+ return data;
};
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 36beb60b..791296fa 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -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";
diff --git a/packages/react/src/stores/query.ts b/packages/react/src/stores/query.ts
index 3b24f29e..1ebe7c84 100644
--- a/packages/react/src/stores/query.ts
+++ b/packages/react/src/stores/query.ts
@@ -1,16 +1,15 @@
import { stringify } from "../utils.js";
import { typedApiAtomFamily } from "./client.js";
import {
- type MultiInstruction,
- type Query,
- QueryError,
QueryInstruction,
preflight,
query,
+ type MultiInstruction,
+ type Query,
} from "@reactive-dot/core";
import type { ChainId } from "@reactive-dot/types";
-import { atom } from "jotai";
-import { atomFamily, atomWithObservable } from "jotai/utils";
+import { type Atom, type WritableAtom, atom } from "jotai";
+import { atomFamily, atomWithObservable, atomWithRefresh } from "jotai/utils";
import { from, switchMap, type Observable } from "rxjs";
const instructionPayloadAtomFamily = atomFamily(
@@ -22,13 +21,13 @@ const instructionPayloadAtomFamily = atomFamily(
// eslint-disable-next-line @typescript-eslint/ban-types
{}>
>;
- }) => {
+ }): Atom | WritableAtom, [], void> => {
switch (preflight(param.instruction)) {
case "promise":
- return atom(async (get, { signal }) => {
+ return atomWithRefresh(async (get, { signal }) => {
const api = await get(typedApiAtomFamily(param.chainId));
- return query(api, param.instruction, { signal }) as Promise;
+ return query(api, param.instruction, { signal });
});
case "observable":
return atomWithObservable((get) =>
@@ -43,41 +42,61 @@ const instructionPayloadAtomFamily = atomFamily(
(a, b) => stringify(a) === stringify(b),
);
+export const getQueryInstructionPayloadAtoms = (
+ chainId: ChainId,
+ query: Query,
+) =>
+ query.instructions.map((instruction) => {
+ if (!("multi" in instruction)) {
+ return instructionPayloadAtomFamily({
+ chainId: chainId,
+ instruction,
+ });
+ }
+
+ return (instruction.args as unknown[]).map((args) => {
+ const { multi, ...rest } = instruction;
+
+ return instructionPayloadAtomFamily({
+ chainId: chainId,
+ instruction: { ...rest, args },
+ });
+ });
+ });
+
+atomFamily((param: { chainId: ChainId; query: Query }) =>
+ atom((get) => {
+ const atoms = getQueryInstructionPayloadAtoms(param.chainId, param.query);
+
+ return Promise.all(
+ atoms.map((atomOrAtoms) => {
+ if (Array.isArray(atomOrAtoms)) {
+ return Promise.all(atomOrAtoms.map(get));
+ }
+
+ return get(atomOrAtoms);
+ }),
+ );
+ }),
+);
+
// TODO: should be memoized within render function instead
// https://github.com/pmndrs/jotai/discussions/1553
export const queryPayloadAtomFamily = atomFamily(
(param: { chainId: ChainId; query: Query }) =>
- atom((get) =>
- Promise.all(
- param.query.instructions.map((instruction) => {
- if (param.chainId === undefined) {
- throw new QueryError("No chain Id provided");
- }
+ atom((get) => {
+ const atoms = getQueryInstructionPayloadAtoms(param.chainId, param.query);
- if (!("multi" in instruction)) {
- return get(
- instructionPayloadAtomFamily({
- chainId: param.chainId,
- instruction,
- }),
- );
+ return Promise.all(
+ atoms.map((atomOrAtoms) => {
+ if (Array.isArray(atomOrAtoms)) {
+ return Promise.all(atomOrAtoms.map(get));
}
- const { multi: _, ...query } = instruction;
-
- return Promise.all(
- instruction.args.map((args: unknown[]) =>
- get(
- instructionPayloadAtomFamily({
- chainId: param.chainId,
- instruction: { ...query, args },
- }),
- ),
- ),
- );
+ return get(atomOrAtoms);
}),
- ),
- ),
+ );
+ }),
(a, b) =>
a.chainId === b.chainId &&
stringify(a.query.instructions) === stringify(b.query.instructions),