diff --git a/.api-reports/api-report-react_internal.md b/.api-reports/api-report-react_internal.md index 0332db52334..9b76bda9bbc 100644 --- a/.api-reports/api-report-react_internal.md +++ b/.api-reports/api-report-react_internal.md @@ -308,6 +308,42 @@ type AsStoreObject; + +// Warning: (ae-forgotten-export) The symbol "QueryHookOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface BackgroundQueryHookOptions extends Pick, "client" | "variables" | "errorPolicy" | "context" | "canonizeResults" | "returnPartialData" | "refetchWritePolicy"> { + // Warning: (ae-forgotten-export) The symbol "BackgroundQueryHookFetchPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fetchPolicy?: BackgroundQueryHookFetchPolicy; + // (undocumented) + queryKey?: string | number | any[]; + // @deprecated + skip?: boolean; +} + +// Warning: (ae-forgotten-export) The symbol "BackgroundQueryHookOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "NoInfer" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type BackgroundQueryHookOptionsNoInfer = BackgroundQueryHookOptions, NoInfer>; + +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface BaseQueryOptions extends SharedWatchQueryOptions { + // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" + client?: ApolloClient; + context?: DefaultContext; + ssr?: boolean; +} + // @public (undocumented) namespace Cache_2 { // (undocumented) @@ -527,6 +563,40 @@ interface DataProxy { writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } +// Warning: (ae-forgotten-export) The symbol "DeepPartialPrimitive" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlyMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialSet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlySet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialObject" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartial = T extends DeepPartialPrimitive ? T : T extends Map ? DeepPartialMap : T extends ReadonlyMap ? DeepPartialReadonlyMap : T extends Set ? DeepPartialSet : T extends ReadonlySet ? DeepPartialReadonlySet : T extends (...args: any[]) => unknown ? T | undefined : T extends object ? T extends (ReadonlyArray) ? TItem[] extends (T) ? readonly TItem[] extends T ? ReadonlyArray> : Array> : DeepPartialObject : DeepPartialObject : unknown; + +// Warning: (ae-forgotten-export) The symbol "DeepPartial" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialMap = {} & Map, DeepPartial>; + +// @public (undocumented) +type DeepPartialObject = { + [K in keyof T]?: DeepPartial; +}; + +// Warning: (ae-forgotten-export) The symbol "Primitive" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialPrimitive = Primitive | Date | RegExp; + +// @public (undocumented) +type DeepPartialReadonlyMap = {} & ReadonlyMap, DeepPartial>; + +// @public (undocumented) +type DeepPartialReadonlySet = {} & ReadonlySet>; + +// @public (undocumented) +type DeepPartialSet = {} & Set>; + // @public (undocumented) interface DefaultContext extends Record { } @@ -626,6 +696,16 @@ interface ExecutionPatchResultBase { hasNext?: boolean; } +// Warning: (ae-forgotten-export) The symbol "FetchMoreQueryOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type FetchMoreFunction = (fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TData; + variables: TVariables; + }) => TData; +}) => Promise>; + // @public (undocumented) type FetchMoreOptions = Parameters["fetchMore"]>[0]; @@ -719,7 +799,6 @@ const getApolloClientMemoryInternals: (() => { }; }) | undefined; -// Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SuspenseCache" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -749,6 +828,13 @@ interface GraphQLRequest> { variables?: TVariables; } +// Warning: (ae-forgotten-export) The symbol "WrappableHooks" needs to be exported by the entry point index.d.ts +// +// @internal +export type HookWrappers = { + [K in keyof WrappableHooks]?: (originalHook: WrappableHooks[K]) => WrappableHooks[K]; +}; + // @public (undocumented) interface IgnoreModifier { // (undocumented) @@ -1051,8 +1137,6 @@ enum NetworkStatus { // @public (undocumented) interface NextFetchPolicyContext { - // Warning: (ae-forgotten-export) The symbol "WatchQueryFetchPolicy" needs to be exported by the entry point index.d.ts - // // (undocumented) initialFetchPolicy: WatchQueryFetchPolicy; // (undocumented) @@ -1069,6 +1153,9 @@ type NextLink = (operation: Operation) => Observable; // @public (undocumented) type NextResultListener = (method: "next" | "error" | "complete", arg?: any) => any; +// @public +type NoInfer = [T][T extends any ? 0 : never]; + // @public (undocumented) class ObservableQuery extends Observable> { constructor({ queryManager, queryInfo, options, }: { @@ -1076,7 +1163,6 @@ class ObservableQuery; }); - // Warning: (ae-forgotten-export) The symbol "FetchMoreQueryOptions" needs to be exported by the entry point index.d.ts fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1133,6 +1219,24 @@ class ObservableQuery { + fetchMore: (fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }) => Promise>; + refetch: (variables?: Partial) => Promise>; + // @internal (undocumented) + reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>; + startPolling: (pollInterval: number) => void; + stopPolling: () => void; + subscribeToMore: (options: SubscribeToMoreOptions) => () => void; + updateQuery: (mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData) => void; + variables: TVariables | undefined; +} + // @public (undocumented) const OBSERVED_CHANGED_OPTIONS: readonly ["canonizeResults", "context", "errorPolicy", "fetchPolicy", "refetchWritePolicy", "returnPartialData"]; @@ -1172,6 +1276,9 @@ interface PendingPromise extends Promise { status: "pending"; } +// @public (undocumented) +type Primitive = null | undefined | string | number | boolean | symbol | bigint; + // @public (undocumented) const PROMISE_SYMBOL: unique symbol; @@ -1185,6 +1292,23 @@ type PromiseWithState = PendingPromise | FulfilledPromise extends BaseQueryOptions { + // @internal (undocumented) + defaultOptions?: Partial>; + onCompleted?: (data: TData) => void; + onError?: (error: ApolloError) => void; + skip?: boolean; +} + +// Warning: (ae-forgotten-export) The symbol "QueryFunctionOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface QueryHookOptions extends QueryFunctionOptions { +} + // @public (undocumented) class QueryInfo { constructor(queryManager: QueryManager, queryId?: string); @@ -1411,6 +1535,20 @@ export interface QueryReference { // @public (undocumented) type QueryRefPromise = PromiseWithState>; +// Warning: (ae-forgotten-export) The symbol "ObservableQueryFields" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface QueryResult extends ObservableQueryFields { + called: boolean; + client: ApolloClient; + data: TData | undefined; + error?: ApolloError; + loading: boolean; + networkStatus: NetworkStatus; + observable: ObservableQuery; + previousData?: TData; +} + // @public (undocumented) type QueryStoreValue = Pick; @@ -1439,6 +1577,9 @@ interface Reference { readonly __ref: string; } +// @public (undocumented) +type RefetchFunction = ObservableQueryFields["refetch"]; + // @public (undocumented) type RefetchQueriesInclude = RefetchQueryDescriptor[] | RefetchQueriesIncludeShorthand; @@ -1550,6 +1691,12 @@ interface SingleExecutionResult, TContext = DefaultC data?: TData | null; } +// @public (undocumented) +type SkipToken = typeof skipToken; + +// @public (undocumented) +const skipToken: unique symbol; + // @public (undocumented) type Source = MaybeAsync>; @@ -1572,6 +1719,9 @@ type StoreObjectValueMaybeReference = StoreVal extends Array = ObservableQueryFields["subscribeToMore"]; + // @public (undocumented) type SubscribeToMoreOptions = { document: DocumentNode | TypedDocumentNode; @@ -1606,6 +1756,27 @@ export interface SuspenseCacheOptions { // @public (undocumented) const suspenseCacheSymbol: unique symbol; +// @public (undocumented) +type SuspenseQueryHookFetchPolicy = Extract; + +// @public (undocumented) +interface SuspenseQueryHookOptions { + // @deprecated + canonizeResults?: boolean; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" + client?: ApolloClient; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + // Warning: (ae-forgotten-export) The symbol "SuspenseQueryHookFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: SuspenseQueryHookFetchPolicy; + queryKey?: string | number | any[]; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + // @deprecated + skip?: boolean; + variables?: TVariables; +} + // @public (undocumented) type ToReferenceFunction = (objOrIdOrRef: StoreObject | string | Reference, mergeIntoStore?: boolean) => Reference | undefined; @@ -1662,16 +1833,228 @@ interface UriFunction { (operation: Operation): string; } +// Warning: (ae-forgotten-export) The symbol "BackgroundQueryHookOptionsNoInfer" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UseBackgroundQueryResult" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; +function useBackgroundQuery, "variables">>(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer & TOptions): [ +(QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables> | (TOptions["skip"] extends boolean ? undefined : never)), +UseBackgroundQueryResult +]; -// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// @public (undocumented) +function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; +}): [ +QueryReference | undefined, TVariables>, +UseBackgroundQueryResult +]; + +// @public (undocumented) +function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { + errorPolicy: "ignore" | "all"; +}): [ +QueryReference, +UseBackgroundQueryResult +]; + +// @public (undocumented) +function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { + skip: boolean; + returnPartialData: true; +}): [ +QueryReference, TVariables> | undefined, +UseBackgroundQueryResult +]; + +// @public (undocumented) +function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { + returnPartialData: true; +}): [ +QueryReference, TVariables>, +UseBackgroundQueryResult +]; + +// @public (undocumented) +function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { + skip: boolean; +}): [ +QueryReference | undefined, +UseBackgroundQueryResult +]; + +// @public (undocumented) +function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer): [ +QueryReference, +UseBackgroundQueryResult +]; + +// Warning: (ae-forgotten-export) The symbol "SkipToken" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken): [undefined, UseBackgroundQueryResult]; + +// @public (undocumented) +function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken | (BackgroundQueryHookOptionsNoInfer & { + returnPartialData: true; +})): [ +QueryReference, TVariables> | undefined, +UseBackgroundQueryResult +]; + +// @public (undocumented) +function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: SkipToken | BackgroundQueryHookOptionsNoInfer): [ +QueryReference | undefined, +UseBackgroundQueryResult +]; + +// @public (undocumented) +type UseBackgroundQueryResult = { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; +}; + +// Warning: (ae-forgotten-export) The symbol "UseFragmentOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UseFragmentResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +function useFragment(options: UseFragmentOptions): UseFragmentResult; + +// @public (undocumented) +interface UseFragmentOptions extends Omit, NoInfer>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" + client?: ApolloClient; + // (undocumented) + from: StoreObject | Reference | string; + // (undocumented) + optimistic?: boolean; +} + +// @public (undocumented) +type UseFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing?: MissingTree; +}; + +// Warning: (ae-forgotten-export) The symbol "QueryResult" needs to be exported by the entry point index.d.ts +// +// @public +function useQuery(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; + +// Warning: (ae-forgotten-export) The symbol "UseReadQueryResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +function useReadQuery(queryRef: QueryReference): UseReadQueryResult; + +// @public (undocumented) +interface UseReadQueryResult { + data: TData; + error: ApolloError | undefined; + networkStatus: NetworkStatus; +} + +// Warning: (ae-forgotten-export) The symbol "SuspenseQueryHookOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UseSuspenseQueryResult" needs to be exported by the entry point index.d.ts // +// @public (undocumented) +function useSuspenseQuery, "variables">>(query: DocumentNode | TypedDocumentNode, options?: SuspenseQueryHookOptions, NoInfer> & TOptions): UseSuspenseQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? TOptions["skip"] extends boolean ? DeepPartial | undefined : DeepPartial : TOptions["skip"] extends boolean ? TData | undefined : TData, TVariables>; + +// @public (undocumented) +function useSuspenseQuery(query: DocumentNode | TypedDocumentNode, options: SuspenseQueryHookOptions, NoInfer> & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; +}): UseSuspenseQueryResult | undefined, TVariables>; + +// @public (undocumented) +function useSuspenseQuery(query: DocumentNode | TypedDocumentNode, options: SuspenseQueryHookOptions, NoInfer> & { + errorPolicy: "ignore" | "all"; +}): UseSuspenseQueryResult; + +// @public (undocumented) +function useSuspenseQuery(query: DocumentNode | TypedDocumentNode, options: SuspenseQueryHookOptions, NoInfer> & { + skip: boolean; + returnPartialData: true; +}): UseSuspenseQueryResult | undefined, TVariables>; + +// @public (undocumented) +function useSuspenseQuery(query: DocumentNode | TypedDocumentNode, options: SuspenseQueryHookOptions, NoInfer> & { + returnPartialData: true; +}): UseSuspenseQueryResult, TVariables>; + +// @public (undocumented) +function useSuspenseQuery(query: DocumentNode | TypedDocumentNode, options: SuspenseQueryHookOptions, NoInfer> & { + skip: boolean; +}): UseSuspenseQueryResult; + +// @public (undocumented) +function useSuspenseQuery(query: DocumentNode | TypedDocumentNode, options?: SuspenseQueryHookOptions, NoInfer>): UseSuspenseQueryResult; + +// @public (undocumented) +function useSuspenseQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken | (SuspenseQueryHookOptions, NoInfer> & { + returnPartialData: true; +})): UseSuspenseQueryResult | undefined, TVariables>; + +// @public (undocumented) +function useSuspenseQuery(query: DocumentNode | TypedDocumentNode, options?: SkipToken | SuspenseQueryHookOptions, NoInfer>): UseSuspenseQueryResult; + +// @public (undocumented) +interface UseSuspenseQueryResult { + // (undocumented) + client: ApolloClient; + // (undocumented) + data: TData; + // (undocumented) + error: ApolloError | undefined; + // (undocumented) + fetchMore: FetchMoreFunction; + // (undocumented) + networkStatus: NetworkStatus; + // (undocumented) + refetch: RefetchFunction; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts + // + // (undocumented) + subscribeToMore: SubscribeToMoreFunction; +} + +// @public (undocumented) +type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; + // @public interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; } +// @public (undocumented) +interface WrappableHooks { + // Warning: (ae-forgotten-export) The symbol "useBackgroundQuery" needs to be exported by the entry point index.d.ts + // + // (undocumented) + useBackgroundQuery: typeof useBackgroundQuery; + // Warning: (ae-forgotten-export) The symbol "useFragment" needs to be exported by the entry point index.d.ts + // + // (undocumented) + useFragment: typeof useFragment; + // Warning: (ae-forgotten-export) The symbol "useQuery" needs to be exported by the entry point index.d.ts + // + // (undocumented) + useQuery: typeof useQuery; + // Warning: (ae-forgotten-export) The symbol "useReadQuery" needs to be exported by the entry point index.d.ts + // + // (undocumented) + useReadQuery: typeof useReadQuery; + // Warning: (ae-forgotten-export) The symbol "useSuspenseQuery" needs to be exported by the entry point index.d.ts + // + // (undocumented) + useSuspenseQuery: typeof useSuspenseQuery; +} + // @public (undocumented) export function wrapQueryRef(internalQueryRef: InternalQueryReference): QueryReference; @@ -1693,6 +2076,8 @@ export function wrapQueryRef(inter // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:29:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.changeset/curvy-maps-give.md b/.changeset/curvy-maps-give.md new file mode 100644 index 00000000000..8217041c861 --- /dev/null +++ b/.changeset/curvy-maps-give.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Allow Apollo Client instance to intercept hook functionality diff --git a/.size-limits.json b/.size-limits.json index 8d2f9f6c9d7..a4c53b7f485 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39075, + "dist/apollo-client.min.cjs": 39209, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32584 } diff --git a/config/rollup.config.js b/config/rollup.config.js index 90dd8f56dec..5ed93b35165 100644 --- a/config/rollup.config.js +++ b/config/rollup.config.js @@ -8,7 +8,9 @@ import cleanup from "rollup-plugin-cleanup"; const entryPoints = require("./entryPoints"); const distDir = "./dist"; -const removeComments = cleanup({}); +const removeComments = cleanup({ + comments: ["some", /#__PURE__/, /#__NO_SIDE_EFFECTS__/], +}); function isExternal(id, parentId, entryPointsAreExternal = true) { let posixId = toPosixPath(id); diff --git a/src/react/hooks/internal/index.ts b/src/react/hooks/internal/index.ts index 71cfbdc1799..ce58b546f69 100644 --- a/src/react/hooks/internal/index.ts +++ b/src/react/hooks/internal/index.ts @@ -4,3 +4,4 @@ export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js"; export { useRenderGuard } from "./useRenderGuard.js"; export { useLazyRef } from "./useLazyRef.js"; export { __use } from "./__use.js"; +export { wrapHook } from "./wrapHook.js"; diff --git a/src/react/hooks/internal/wrapHook.ts b/src/react/hooks/internal/wrapHook.ts new file mode 100644 index 00000000000..abf9a49c035 --- /dev/null +++ b/src/react/hooks/internal/wrapHook.ts @@ -0,0 +1,88 @@ +import type { + useQuery, + useSuspenseQuery, + useBackgroundQuery, + useReadQuery, + useFragment, +} from "../index.js"; +import type { QueryManager } from "../../../core/QueryManager.js"; +import type { ApolloClient } from "../../../core/ApolloClient.js"; +import type { ObservableQuery } from "../../../core/ObservableQuery.js"; + +const wrapperSymbol = Symbol.for("apollo.hook.wrappers"); + +interface WrappableHooks { + useQuery: typeof useQuery; + useSuspenseQuery: typeof useSuspenseQuery; + useBackgroundQuery: typeof useBackgroundQuery; + useReadQuery: typeof useReadQuery; + useFragment: typeof useFragment; +} + +/** + * @internal + * Can be used to correctly type the [Symbol.for("apollo.hook.wrappers")] property of + * `QueryManager`, to override/wrap hook functionality. + */ +export type HookWrappers = { + [K in keyof WrappableHooks]?: ( + originalHook: WrappableHooks[K] + ) => WrappableHooks[K]; +}; + +interface QueryManagerWithWrappers extends QueryManager { + [wrapperSymbol]?: HookWrappers; +} + +/** + * @internal + * + * Makes an Apollo Client hook "wrappable". + * That means that the Apollo Client instance can expose a "wrapper" that will be + * used to wrap the original hook implementation with additional logic. + * @example + * ```tsx + * // this is already done in `@apollo/client` for all wrappable hooks (see `WrappableHooks`) + * // following this pattern + * function useQuery() { + * return wrapHook('useQuery', _useQuery, options.client)(query, options); + * } + * function _useQuery(query, options) { + * // original implementation + * } + * + * // this is what a library like `@apollo/client-react-streaming` would do + * class ApolloClientWithStreaming extends ApolloClient { + * constructor(options) { + * super(options); + * this.queryManager[Symbol.for("apollo.hook.wrappers")] = { + * useQuery: (original) => (query, options) => { + * console.log("useQuery was called with options", options); + * return original(query, options); + * } + * } + * } + * } + * + * // this will now log the options and then call the original `useQuery` + * const client = new ApolloClientWithStreaming({ ... }); + * useQuery(query, { client }); + * ``` + */ +export function wrapHook any>( + hookName: keyof WrappableHooks, + useHook: Hook, + clientOrObsQuery: ObservableQuery | ApolloClient +): Hook { + const queryManager = ( + clientOrObsQuery as unknown as { + // both `ApolloClient` and `ObservableQuery` have a `queryManager` property + // but they're both `private`, so we have to cast around for a bit here. + queryManager: QueryManagerWithWrappers; + } + )["queryManager"]; + const wrappers = queryManager && queryManager[wrapperSymbol]; + const wrapper: undefined | ((wrap: Hook) => Hook) = + wrappers && (wrappers[hookName] as any); + return wrapper ? wrapper(useHook) : useHook; +} diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index ab9105d4243..8d83c50fc00 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -15,7 +15,7 @@ import { } from "../internal/index.js"; import type { CacheKey, QueryReference } from "../internal/index.js"; import type { BackgroundQueryHookOptions, NoInfer } from "../types/types.js"; -import { __use } from "./internal/index.js"; +import { __use, wrapHook } from "./internal/index.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; import { canonicalStringify } from "../../cache/index.js"; @@ -182,6 +182,26 @@ export function useBackgroundQuery< ): [ QueryReference | undefined, UseBackgroundQueryResult, +] { + return wrapHook( + "useBackgroundQuery", + _useBackgroundQuery, + useApolloClient(typeof options === "object" ? options.client : undefined) + )(query, options); +} + +function _useBackgroundQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: + | (SkipToken & + Partial>) + | BackgroundQueryHookOptionsNoInfer +): [ + QueryReference | undefined, + UseBackgroundQueryResult, ] { const client = useApolloClient(options.client); const suspenseCache = getSuspenseCache(client); diff --git a/src/react/hooks/useFragment.ts b/src/react/hooks/useFragment.ts index 80e59d0e49a..96e2a1c014a 100644 --- a/src/react/hooks/useFragment.ts +++ b/src/react/hooks/useFragment.ts @@ -14,7 +14,7 @@ import { useApolloClient } from "./useApolloClient.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; import type { ApolloClient, OperationVariables } from "../../core/index.js"; import type { NoInfer } from "../types/types.js"; -import { useDeepMemo, useLazyRef } from "./internal/index.js"; +import { useDeepMemo, useLazyRef, wrapHook } from "./internal/index.js"; export interface UseFragmentOptions extends Omit< @@ -53,6 +53,16 @@ export type UseFragmentResult = export function useFragment( options: UseFragmentOptions +): UseFragmentResult { + return wrapHook( + "useFragment", + _useFragment, + useApolloClient(options.client) + )(options); +} + +function _useFragment( + options: UseFragmentOptions ): UseFragmentResult { const { cache } = useApolloClient(options.client); diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 4bc596ed371..225577521b4 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -36,6 +36,7 @@ import { isNonEmptyArray, maybeDeepFreeze, } from "../../utilities/index.js"; +import { wrapHook } from "./internal/index.js"; const { prototype: { hasOwnProperty }, @@ -85,6 +86,20 @@ export function useQuery< NoInfer > = Object.create(null) ): QueryResult { + return wrapHook( + "useQuery", + _useQuery, + useApolloClient(options && options.client) + )(query, options); +} + +function _useQuery< + TData = any, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: QueryHookOptions, NoInfer> +) { return useInternalState(useApolloClient(options.client), query).useQuery( options ); diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index 3f110b26164..9e8e4621a83 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -5,7 +5,7 @@ import { updateWrappedQueryRef, } from "../internal/index.js"; import type { QueryReference } from "../internal/index.js"; -import { __use } from "./internal/index.js"; +import { __use, wrapHook } from "./internal/index.js"; import { toApolloError } from "./useSuspenseQuery.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; import type { ApolloError } from "../../errors/index.js"; @@ -38,6 +38,16 @@ export interface UseReadQueryResult { export function useReadQuery( queryRef: QueryReference +): UseReadQueryResult { + return wrapHook( + "useReadQuery", + _useReadQuery, + unwrapQueryRef(queryRef)["observable"] + )(queryRef); +} + +function _useReadQuery( + queryRef: QueryReference ): UseReadQueryResult { const internalQueryRef = React.useMemo( () => unwrapQueryRef(queryRef), diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index 3a69e175ed5..77159eb31f2 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -20,7 +20,7 @@ import type { ObservableQueryFields, NoInfer, } from "../types/types.js"; -import { __use, useDeepMemo } from "./internal/index.js"; +import { __use, useDeepMemo, wrapHook } from "./internal/index.js"; import { getSuspenseCache } from "../internal/index.js"; import { canonicalStringify } from "../../cache/index.js"; import { skipToken } from "./constants.js"; @@ -174,6 +174,22 @@ export function useSuspenseQuery< options: | (SkipToken & Partial>) | SuspenseQueryHookOptions = Object.create(null) +): UseSuspenseQueryResult { + return wrapHook( + "useSuspenseQuery", + _useSuspenseQuery, + useApolloClient(typeof options === "object" ? options.client : undefined) + )(query, options); +} + +function _useSuspenseQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: + | (SkipToken & Partial>) + | SuspenseQueryHookOptions ): UseSuspenseQueryResult { const client = useApolloClient(options.client); const suspenseCache = getSuspenseCache(client); diff --git a/src/react/internal/index.ts b/src/react/internal/index.ts index cbcab8f0209..a453c6f802c 100644 --- a/src/react/internal/index.ts +++ b/src/react/internal/index.ts @@ -9,3 +9,4 @@ export { wrapQueryRef, } from "./cache/QueryReference.js"; export type { SuspenseCacheOptions } from "./cache/SuspenseCache.js"; +export type { HookWrappers } from "../hooks/internal/wrapHook.js";