Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: colocate atoms & hooks #287

Merged
merged 1 commit into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion packages/react/src/hooks/use-accounts.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { accountsAtom } from "../stores/accounts.js";
import { atomFamilyWithErrorCatcher } from "../utils/jotai.js";
import type { ChainHookOptions } from "./types.js";
import { internal_useChainId } from "./use-chain-id.js";
import { chainSpecDataAtom } from "./use-chain-spec-data.js";
import { useConfig } from "./use-config.js";
import { connectedWalletsAtom } from "./use-wallets.js";
import { getAccounts, type ChainId, type Config } from "@reactive-dot/core";
import { useAtomValue } from "jotai";
import { atomWithObservable } from "jotai/utils";

/**
* Hook for getting currently connected accounts.
Expand All @@ -18,3 +22,24 @@ export function useAccounts(options?: ChainHookOptions) {
}),
);
}

/**
* @internal
*/
export const accountsAtom = atomFamilyWithErrorCatcher(
(param: { config: Config; chainId: ChainId | undefined }, withErrorCatcher) =>
withErrorCatcher(atomWithObservable)((get) =>
getAccounts(
get(connectedWalletsAtom(param.config)),
param.chainId === undefined
? undefined
: get(
chainSpecDataAtom(
// @ts-expect-error `chainId` will never be undefined
param,
),
),
),
),
(a, b) => a.config === b.config && a.chainId === b.chainId,
);
33 changes: 32 additions & 1 deletion packages/react/src/hooks/use-block.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { bestBlockAtom, finalizedBlockAtom } from "../stores/block.js";
import { atomFamilyWithErrorCatcher } from "../utils/jotai.js";
import type { ChainHookOptions } from "./types.js";
import { internal_useChainId } from "./use-chain-id.js";
import { clientAtom } from "./use-client.js";
import { useConfig } from "./use-config.js";
import { type ChainId, type Config, getBlock } from "@reactive-dot/core";
import { useAtomValue } from "jotai";
import { atomWithObservable } from "jotai/utils";
import { from } from "rxjs";
import { switchMap } from "rxjs/operators";

/**
* Hook for fetching information about the latest block.
Expand All @@ -24,3 +29,29 @@ export function useBlock(
: bestBlockAtom({ config, chainId }),
);
}

/**
* @internal
*/
export const finalizedBlockAtom = atomFamilyWithErrorCatcher(
(param: { config: Config; chainId: ChainId }, withErrorCatcher) =>
withErrorCatcher(atomWithObservable)((get) =>
from(get(clientAtom(param))).pipe(
switchMap((client) => getBlock(client, { tag: "finalized" })),
),
),
(a, b) => a.config === b.config && a.chainId === b.chainId,
);

/**
* @internal
*/
export const bestBlockAtom = atomFamilyWithErrorCatcher(
(param: { config: Config; chainId: ChainId }, withErrorCatcher) =>
withErrorCatcher(atomWithObservable)((get) =>
from(get(clientAtom(param))).pipe(
switchMap((client) => getBlock(client, { tag: "best" })),
),
),
(a, b) => a.config === b.config && a.chainId === b.chainId,
);
19 changes: 17 additions & 2 deletions packages/react/src/hooks/use-chain-spec-data.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { chainSpecDataAtom } from "../stores/client.js";
import { atomFamilyWithErrorCatcher } from "../utils/jotai.js";
import type { ChainHookOptions } from "./types.js";
import { internal_useChainId } from "./use-chain-id.js";
import { clientAtom } from "./use-client.js";
import { useConfig } from "./use-config.js";
import { useAtomValue } from "jotai";
import type { Config, ChainId } from "@reactive-dot/core";
import { atom, useAtomValue } from "jotai";

/**
* Hook for fetching the [JSON-RPC spec](https://paritytech.github.io/json-rpc-interface-spec/api/chainSpec.html).
Expand All @@ -18,3 +20,16 @@ export function useChainSpecData(options?: ChainHookOptions) {
}),
);
}

/**
* @internal
*/
export const chainSpecDataAtom = atomFamilyWithErrorCatcher(
(param: { config: Config; chainId: ChainId }, withErrorCatcher) =>
withErrorCatcher(atom)(async (get) => {
const client = await get(clientAtom(param));

return client.getChainSpecData();
}),
(a, b) => a.config === b.config && a.chainId === b.chainId,
);
22 changes: 21 additions & 1 deletion packages/react/src/hooks/use-client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { clientAtom } from "../stores/client.js";
import { atomFamilyWithErrorCatcher } from "../utils/jotai.js";
import type { ChainHookOptions } from "./types.js";
import { internal_useChainId } from "./use-chain-id.js";
import { useConfig } from "./use-config.js";
import type { ChainId, Config } from "@reactive-dot/core";
import { getClient, ReactiveDotError } from "@reactive-dot/core";
import { useAtomValue } from "jotai";
import { atom } from "jotai";

/**
* Hook for getting Polkadot-API client instance.
Expand All @@ -18,3 +21,20 @@ export function useClient(options?: ChainHookOptions) {
}),
);
}

/**
* @internal
*/
export const clientAtom = atomFamilyWithErrorCatcher(
(param: { config: Config; chainId: ChainId }, withErrorCatcher) =>
withErrorCatcher(atom)(async () => {
const chainConfig = param.config.chains[param.chainId];

if (chainConfig === undefined) {
throw new ReactiveDotError(`No config provided for ${param.chainId}`);
}

return getClient(chainConfig);
}),
(a, b) => a.config === b.config && a.chainId === b.chainId,
);
2 changes: 1 addition & 1 deletion packages/react/src/hooks/use-mutation.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { SignerContext } from "../contexts/index.js";
import { MutationEventSubjectContext } from "../contexts/mutation.js";
import { typedApiAtom } from "../stores/client.js";
import type { ChainHookOptions } from "./types.js";
import { useAsyncAction } from "./use-async-action.js";
import { internal_useChainId } from "./use-chain-id.js";
import { useConfig } from "./use-config.js";
import { typedApiAtom } from "./use-typed-api.js";
import type { ChainId } from "@reactive-dot/core";
import { MutationError, pending } from "@reactive-dot/core";
import type { ChainDescriptorOf } from "@reactive-dot/core/internal.js";
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/hooks/use-query-loader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { queryPayloadAtom } from "../stores/query.js";
import type { ChainHookOptions } from "./types.js";
import { internal_useChainId } from "./use-chain-id.js";
import { useConfig } from "./use-config.js";
import { queryPayloadAtom } from "./use-query.js";
import { type ChainId, Query } from "@reactive-dot/core";
import type {
ChainDescriptorOf,
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/hooks/use-query-refresher.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getQueryInstructionPayloadAtoms } from "../stores/query.js";
import type { ChainHookOptions } from "./types.js";
import { internal_useChainId } from "./use-chain-id.js";
import { useConfig } from "./use-config.js";
import { getQueryInstructionPayloadAtoms } from "./use-query.js";
import { Query, type ChainId } from "@reactive-dot/core";
import type {
ChainDescriptorOf,
Expand Down
124 changes: 119 additions & 5 deletions packages/react/src/hooks/use-query.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import { queryPayloadAtom } from "../stores/query.js";
import { atomFamilyWithErrorCatcher } from "../utils/jotai.js";
import type { ChainHookOptions } from "./types.js";
import { internal_useChainId } from "./use-chain-id.js";
import { useConfig } from "./use-config.js";
import { useQueryRefresher } from "./use-query-refresher.js";
import { idle, Query, type ChainId } from "@reactive-dot/core";
import { typedApiAtom } from "./use-typed-api.js";
import {
type ChainId,
type Config,
idle,
preflight,
query,
Query,
} from "@reactive-dot/core";
import {
type ChainDescriptorOf,
flatHead,
stringify,
type Falsy,
type FalsyGuard,
flatHead,
type FlatHead,
type InferQueryPayload,
type MultiInstruction,
type QueryInstruction,
stringify,
} from "@reactive-dot/core/internal.js";
import { atom, useAtomValue } from "jotai";
import { type Atom, atom, useAtomValue, type WritableAtom } from "jotai";
import { atomWithObservable, atomWithRefresh } from "jotai/utils";
import { useMemo } from "react";
import { from, type Observable } from "rxjs";
import { switchMap } from "rxjs/operators";

/**
* Hook for querying data from chain, and returning the response.
Expand Down Expand Up @@ -105,3 +117,105 @@ export function useLazyLoadQueryWithRefresh<

return [data, refresh] as [data: typeof data, refresh: typeof refresh];
}

/**
* @internal
*/
export const instructionPayloadAtom = atomFamilyWithErrorCatcher(
(
param: {
config: Config;
chainId: ChainId;
instruction: Exclude<
QueryInstruction,
MultiInstruction<// @ts-expect-error need any empty object here
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
{}>
>;
},
withErrorCatcher,
): Atom<unknown> | WritableAtom<Promise<unknown>, [], void> => {
switch (preflight(param.instruction)) {
case "promise":
return withErrorCatcher(atomWithRefresh)(async (get, { signal }) => {
const api = await get(typedApiAtom(param));

return query(api, param.instruction, { signal });
});
case "observable":
return withErrorCatcher(atomWithObservable)((get) =>
from(get(typedApiAtom(param))).pipe(
switchMap(
(api) => query(api, param.instruction) as Observable<unknown>,
),
),
);
}
},
(a, b) =>
a.config === b.config &&
a.chainId === b.chainId &&
stringify(a.instruction) === stringify(b.instruction),
);

/**
* @internal
*/
export function getQueryInstructionPayloadAtoms(
config: Config,
chainId: ChainId,
query: Query,
) {
return query.instructions.map((instruction) => {
if (!("multi" in instruction)) {
return instructionPayloadAtom({
config,
chainId,
instruction,
});
}

return (instruction.args as unknown[]).map((args) => {
const { multi, ...rest } = instruction;

return instructionPayloadAtom({
config,
chainId,
instruction: { ...rest, args },
});
});
});
}

/**
* @internal
* TODO: should be memoized within render function instead
* https://github.com/pmndrs/jotai/discussions/1553
*/
export const queryPayloadAtom = atomFamilyWithErrorCatcher(
(
param: { config: Config; chainId: ChainId; query: Query },
withErrorCatcher,
): Atom<unknown> =>
withErrorCatcher(atom)((get) => {
const atoms = getQueryInstructionPayloadAtoms(
param.config,
param.chainId,
param.query,
);

return Promise.all(
atoms.map((atomOrAtoms) => {
if (Array.isArray(atomOrAtoms)) {
return Promise.all(atomOrAtoms.map(get));
}

return get(atomOrAtoms);
}),
);
}),
(a, b) =>
a.config === b.config &&
a.chainId === b.chainId &&
stringify(a.query.instructions) === stringify(b.query.instructions),
);
32 changes: 29 additions & 3 deletions packages/react/src/hooks/use-typed-api.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { typedApiAtom } from "../stores/client.js";
import { atomFamilyWithErrorCatcher } from "../utils/jotai.js";
import type { ChainHookOptions } from "./types.js";
import { internal_useChainId } from "./use-chain-id.js";
import { clientAtom } from "./use-client.js";
import { useConfig } from "./use-config.js";
import type { ChainId } from "@reactive-dot/core";
import {
ReactiveDotError,
type ChainId,
type Config,
} from "@reactive-dot/core";
import type { ChainDescriptorOf } from "@reactive-dot/core/internal.js";
import { useAtomValue } from "jotai";
import { atom, useAtomValue } from "jotai";
import type { TypedApi } from "polkadot-api";

/**
Expand All @@ -23,3 +28,24 @@ export function useTypedApi<TChainId extends ChainId | undefined>(
}),
) as TypedApi<ChainDescriptorOf<TChainId>>;
}

/**
* @internal
*/
export const typedApiAtom = atomFamilyWithErrorCatcher(
(param: { config: Config; chainId: ChainId }, withErrorCatcher) =>
withErrorCatcher(atom)(async (get) => {
const config = param.config.chains[param.chainId];

if (config === undefined) {
throw new ReactiveDotError(
`No config provided for chain ${param.chainId}`,
);
}

const client = await get(clientAtom(param));

return client.getTypedApi(config.descriptor);
}),
(a, b) => a.config === b.config && a.chainId === b.chainId,
);
2 changes: 1 addition & 1 deletion packages/react/src/hooks/use-wallet-connector.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { walletsAtom } from "../stores/wallets.js";
import { useAsyncAction } from "./use-async-action.js";
import { useConfig } from "./use-config.js";
import { walletsAtom } from "./use-wallets.js";
import { connectWallet } from "@reactive-dot/core";
import type { Wallet } from "@reactive-dot/core/wallets.js";
import { useAtomCallback } from "jotai/utils";
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/hooks/use-wallet-disconnector.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { walletsAtom } from "../stores/wallets.js";
import { useAsyncAction } from "./use-async-action.js";
import { useConfig } from "./use-config.js";
import { walletsAtom } from "./use-wallets.js";
import { disconnectWallet } from "@reactive-dot/core";
import type { Wallet } from "@reactive-dot/core/wallets.js";
import { useAtomCallback } from "jotai/utils";
Expand Down
Loading