Skip to content

Commit

Permalink
refactor: colocate atoms & hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
tien committed Oct 22, 2024
1 parent 774a509 commit ba45a91
Show file tree
Hide file tree
Showing 17 changed files with 293 additions and 259 deletions.
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

0 comments on commit ba45a91

Please sign in to comment.