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 8700cc7
Show file tree
Hide file tree
Showing 7 changed files with 205 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";
73 changes: 38 additions & 35 deletions packages/react/src/stores/query.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -22,13 +21,13 @@ const instructionPayloadAtomFamily = atomFamily(
// eslint-disable-next-line @typescript-eslint/ban-types
{}>
>;
}) => {
}): Atom<unknown> | WritableAtom<Promise<unknown>, [], 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<unknown>;
return query(api, param.instruction, { signal });
});
case "observable":
return atomWithObservable((get) =>
Expand All @@ -43,41 +42,45 @@ 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 },
});
});
});

// 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),
Expand Down

0 comments on commit 8700cc7

Please sign in to comment.