diff --git a/.changeset/smart-pandas-carry.md b/.changeset/smart-pandas-carry.md new file mode 100644 index 00000000000..df516d90c52 --- /dev/null +++ b/.changeset/smart-pandas-carry.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Use hooks exported from `@clerk/shared` to query commerce data. diff --git a/.changeset/violet-terms-fix.md b/.changeset/violet-terms-fix.md new file mode 100644 index 00000000000..0c277435f5c --- /dev/null +++ b/.changeset/violet-terms-fix.md @@ -0,0 +1,10 @@ +--- +'@clerk/shared': minor +'@clerk/types': minor +--- + +Introduce experimental paginated hooks for commerce data. +- `useStatements` +- `usePaymentAttempts` +- `usePaymentMethods` +Prefixed with `__experimental_` diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx index a6f08a684bc..1a07961c06f 100644 --- a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx @@ -17,7 +17,7 @@ import { Select, SelectButton, SelectOptionList } from '@/ui/elements/Select'; import { Tooltip } from '@/ui/elements/Tooltip'; import { DevOnly } from '../../common/DevOnly'; -import { useCheckoutContext, usePaymentSources } from '../../contexts'; +import { useCheckoutContext, usePaymentMethods } from '../../contexts'; import { Box, Button, Col, descriptors, Flex, Form, localizationKeys, Text } from '../../customizables'; import { ChevronUpDown, InformationCircle } from '../../icons'; import { handleError } from '../../utils'; @@ -191,8 +191,7 @@ const useCheckoutMutations = () => { }; const CheckoutFormElements = ({ checkout }: { checkout: CommerceCheckoutResource }) => { - const { data } = usePaymentSources(); - const { data: paymentSources } = data || { data: [] }; + const { data: paymentSources } = usePaymentMethods(); const [paymentMethodSource, setPaymentMethodSource] = useState(() => paymentSources.length > 0 ? 'existing' : 'new', diff --git a/packages/clerk-js/src/ui/components/PaymentAttempts/PaymentAttemptsList.tsx b/packages/clerk-js/src/ui/components/PaymentAttempts/PaymentAttemptsList.tsx index ffa3bea2d7a..bbc1e61abed 100644 --- a/packages/clerk-js/src/ui/components/PaymentAttempts/PaymentAttemptsList.tsx +++ b/packages/clerk-js/src/ui/components/PaymentAttempts/PaymentAttemptsList.tsx @@ -12,14 +12,14 @@ import { formatDate, truncateWithEndVisible } from '../../utils'; * -----------------------------------------------------------------------------------------------*/ export const PaymentAttemptsList = () => { - const { data: paymentAttempts, isLoading } = usePaymentAttempts(); + const { data: paymentAttempts, isLoading, count } = usePaymentAttempts(); const localizationRoot = useSubscriberTypeLocalizationRoot(); return ( {}} - itemCount={paymentAttempts?.total_count || 0} + itemCount={count} pageCount={1} itemsPerPage={10} isLoading={isLoading} @@ -29,7 +29,7 @@ export const PaymentAttemptsList = () => { localizationKeys(`${localizationRoot}.billingPage.paymentHistorySection.tableHeader__amount`), localizationKeys(`${localizationRoot}.billingPage.paymentHistorySection.tableHeader__status`), ]} - rows={(paymentAttempts?.data || []).map(i => ( + rows={paymentAttempts.map(i => ( { const localizationRoot = useSubscriberTypeLocalizationRoot(); const resource = subscriberType === 'org' ? clerk?.organization : clerk.user; - const { data, isLoading, mutate: mutatePaymentSources } = usePaymentSources(); - - const { data: paymentSources = [] } = data || {}; + const { data: paymentMethods, isLoading, revalidate: revalidatePaymentMethods } = usePaymentMethods(); const sortedPaymentSources = useMemo( - () => paymentSources.sort((a, b) => (a.isDefault && !b.isDefault ? -1 : 1)), - [paymentSources], + () => paymentMethods.sort((a, b) => (a.isDefault && !b.isDefault ? -1 : 1)), + [paymentMethods], ); - const revalidatePaymentSources = useCallback(() => void mutatePaymentSources(), [mutatePaymentSources]); - if (!resource) { return null; } @@ -156,7 +152,7 @@ export const PaymentSources = withCardStateProvider(() => { @@ -164,7 +160,7 @@ export const PaymentSources = withCardStateProvider(() => { @@ -178,7 +174,7 @@ export const PaymentSources = withCardStateProvider(() => { - + diff --git a/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx index eb027a0e4ae..710d9191463 100644 --- a/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx +++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx @@ -2,7 +2,7 @@ import { useClerk } from '@clerk/shared/react'; import type { CommercePlanResource, CommerceSubscriptionPlanPeriod, PricingTableProps } from '@clerk/types'; import { useEffect, useMemo, useState } from 'react'; -import { usePaymentSources, usePlans, usePlansContext, usePricingTableContext, useSubscriptions } from '../../contexts'; +import { usePaymentMethods, usePlans, usePlansContext, usePricingTableContext, useSubscriptions } from '../../contexts'; import { Flow } from '../../customizables'; import { PricingTableDefault } from './PricingTableDefault'; import { PricingTableMatrix } from './PricingTableMatrix'; @@ -60,7 +60,7 @@ const PricingTableRoot = (props: PricingTableProps) => { }; // Pre-fetch payment sources - usePaymentSources(); + usePaymentMethods(); return ( { - const { data: statements, isLoading } = useStatements(); + const { data: statements, isLoading, count } = useStatements(); const localizationRoot = useSubscriberTypeLocalizationRoot(); return ( {}} - itemCount={statements?.total_count || 0} + itemCount={count} pageCount={1} itemsPerPage={10} isLoading={isLoading} @@ -29,7 +29,7 @@ export const StatementsList = () => { localizationKeys(`${localizationRoot}.billingPage.statementsSection.tableHeader__date`), localizationKeys(`${localizationRoot}.billingPage.statementsSection.tableHeader__amount`), ]} - rows={(statements?.data || []).map(i => ( + rows={statements.map(i => ( { const { data: payments } = usePaymentAttempts(); const getPaymentAttemptById = useCallback( (paymentAttemptId: string) => { - return payments?.data.find(payment => payment.id === paymentAttemptId); + return payments.find(payment => payment.id === paymentAttemptId); }, - [payments?.data], + [payments], ); return { diff --git a/packages/clerk-js/src/ui/contexts/components/Plans.tsx b/packages/clerk-js/src/ui/contexts/components/Plans.tsx index 0ec0005d9b5..28412926f0e 100644 --- a/packages/clerk-js/src/ui/contexts/components/Plans.tsx +++ b/packages/clerk-js/src/ui/contexts/components/Plans.tsx @@ -1,4 +1,12 @@ -import { useClerk, useOrganization, useSession, useUser } from '@clerk/shared/react'; +import { + __experimental_usePaymentAttempts, + __experimental_usePaymentMethods, + __experimental_useStatements, + useClerk, + useOrganization, + useSession, + useUser, +} from '@clerk/shared/react'; import type { Appearance, CommercePlanResource, @@ -8,7 +16,8 @@ import type { import { useCallback, useMemo } from 'react'; import useSWR from 'swr'; -import { CommerceSubscription } from '../../../core/resources/internal'; +import { CommerceSubscription } from '@/core/resources/CommerceSubscription'; + import type { LocalizationKey } from '../../localization'; import { localizationKeys } from '../../localization'; import { getClosestProfileScrollBox } from '../../utils'; @@ -30,51 +39,36 @@ export const usePaymentSourcesCacheKey = () => { }; }; -export const usePaymentSources = () => { - const { organization } = useOrganization(); - const { user } = useUser(); - const subscriberType = useSubscriberTypeContext(); - const cacheKey = usePaymentSourcesCacheKey(); - - return useSWR(cacheKey, () => (subscriberType === 'org' ? organization : user)?.getPaymentSources({}), dedupeOptions); -}; - -export const usePaymentAttemptsCacheKey = () => { - const { organization } = useOrganization(); - const { user } = useUser(); +// TODO(@COMMERCE): Rename payment sources to payment methods at the API level +export const usePaymentMethods = () => { const subscriberType = useSubscriberTypeContext(); - - return { - key: `commerce-payment-history`, - userId: user?.id, - args: { orgId: subscriberType === 'org' ? organization?.id : undefined }, - }; + return __experimental_usePaymentMethods({ + for: subscriberType === 'org' ? 'organization' : 'user', + initialPage: 1, + pageSize: 10, + keepPreviousData: true, + }); }; export const usePaymentAttempts = () => { - const { billing } = useClerk(); - const cacheKey = usePaymentAttemptsCacheKey(); - - return useSWR(cacheKey, ({ args, userId }) => (userId ? billing.getPaymentAttempts(args) : undefined), dedupeOptions); -}; - -export const useStatementsCacheKey = () => { - const { organization } = useOrganization(); - const { user } = useUser(); const subscriberType = useSubscriberTypeContext(); - - return { - key: `commerce-statements`, - userId: user?.id, - args: { orgId: subscriberType === 'org' ? organization?.id : undefined }, - }; + return __experimental_usePaymentAttempts({ + for: subscriberType === 'org' ? 'organization' : 'user', + initialPage: 1, + pageSize: 10, + keepPreviousData: true, + }); }; -export const useStatements = () => { - const { billing } = useClerk(); - const cacheKey = useStatementsCacheKey(); - - return useSWR(cacheKey, ({ args, userId }) => (userId ? billing.getStatements(args) : undefined), dedupeOptions); +export const useStatements = (params?: { mode: 'cache' }) => { + const subscriberType = useSubscriberTypeContext(); + return __experimental_useStatements({ + for: subscriberType === 'org' ? 'organization' : 'user', + initialPage: 1, + pageSize: 10, + keepPreviousData: true, + __experimental_mode: params?.mode, + }); }; export const useSubscriptions = () => { @@ -181,18 +175,17 @@ export const usePlansContext = () => { }); // Invalidates cache but does not fetch immediately - const { mutate: mutateStatements } = - useSWR>>(useStatementsCacheKey()); + const { revalidate: revalidateStatements } = useStatements({ mode: 'cache' }); - const { mutate: mutatePaymentSources } = usePaymentSources(); + const { revalidate: revalidatePaymentSources } = usePaymentMethods(); const revalidateAll = useCallback(() => { // Revalidate the plans and subscriptions void mutateSubscriptions(); void mutatePlans(); - void mutateStatements(); - void mutatePaymentSources(); - }, [mutateSubscriptions, mutatePlans, mutateStatements, mutatePaymentSources]); + void revalidateStatements(); + void revalidatePaymentSources(); + }, [mutateSubscriptions, mutatePlans, revalidateStatements, revalidatePaymentSources]); // should the default plan be shown as active const isDefaultPlanImplicitlyActiveOrUpcoming = useMemo(() => { diff --git a/packages/clerk-js/src/ui/contexts/components/Statements.tsx b/packages/clerk-js/src/ui/contexts/components/Statements.tsx index f3e612d9cfe..b33f537c5d6 100644 --- a/packages/clerk-js/src/ui/contexts/components/Statements.tsx +++ b/packages/clerk-js/src/ui/contexts/components/Statements.tsx @@ -6,9 +6,9 @@ export const useStatementsContext = () => { const { data: statements } = useStatements(); const getStatementById = useCallback( (statementId: string) => { - return statements?.data.find(statement => statement.id === statementId); + return statements.find(statement => statement.id === statementId); }, - [statements?.data], + [statements], ); return { diff --git a/packages/shared/src/react/hooks/createCommerceHook.tsx b/packages/shared/src/react/hooks/createCommerceHook.tsx new file mode 100644 index 00000000000..b562568db7f --- /dev/null +++ b/packages/shared/src/react/hooks/createCommerceHook.tsx @@ -0,0 +1,101 @@ +import type { ClerkPaginatedResponse, ClerkResource } from '@clerk/types'; + +import { eventMethodCalled } from '../../telemetry/events/method-called'; +import { + useAssertWrappedByClerkProvider, + useClerkInstanceContext, + useOrganizationContext, + useUserContext, +} from '../contexts'; +import type { PagesOrInfiniteOptions, PaginatedHookConfig, PaginatedResources } from '../types'; +import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; + +/** + * @internal + */ +type CommerceHookConfig = { + hookName: string; + resourceType: string; + useFetcher: ( + param: 'organization' | 'user', + ) => ((params: TParams) => Promise>) | undefined; +}; + +/** + * A hook factory that creates paginated data fetching hooks for commerce-related resources. + * It provides a standardized way to create hooks that can fetch either user or organization resources + * with built-in pagination support. + * + * The generated hooks handle: + * - Clerk authentication context + * - Resource-specific data fetching + * - Pagination (both traditional and infinite scroll) + * - Telemetry tracking + * - Type safety for the specific resource. + * + * @internal + */ +export function createCommerceHook({ + hookName, + resourceType, + useFetcher, +}: CommerceHookConfig) { + type HookParams = PaginatedHookConfig & { + for: 'organization' | 'user'; + }; + + return function useCommerceHook( + params: T, + ): PaginatedResources { + const { for: _for, ...paginationParams } = params; + + useAssertWrappedByClerkProvider(hookName); + + const fetchFn = useFetcher(_for); + + const safeValues = useWithSafeValues(paginationParams, { + initialPage: 1, + pageSize: 10, + keepPreviousData: false, + infinite: false, + __experimental_mode: undefined, + } as unknown as T); + + const clerk = useClerkInstanceContext(); + const user = useUserContext(); + const { organization } = useOrganizationContext(); + + clerk.telemetry?.record(eventMethodCalled(hookName)); + + const hookParams = + typeof paginationParams === 'undefined' + ? undefined + : ({ + initialPage: safeValues.initialPage, + pageSize: safeValues.pageSize, + ...(_for === 'organization' ? { orgId: organization?.id } : {}), + } as TParams); + + const isClerkLoaded = !!(clerk.loaded && user); + + const isEnabled = !!hookParams && isClerkLoaded; + + const result = usePagesOrInfinite>( + (hookParams || {}) as TParams, + fetchFn, + { + keepPreviousData: safeValues.keepPreviousData, + infinite: safeValues.infinite, + enabled: isEnabled, + __experimental_mode: safeValues.__experimental_mode, + }, + { + type: resourceType, + userId: user?.id, + ...(_for === 'organization' ? { orgId: organization?.id } : {}), + }, + ); + + return result; + }; +} diff --git a/packages/shared/src/react/hooks/index.ts b/packages/shared/src/react/hooks/index.ts index e9d80343a6f..e801745a0b7 100644 --- a/packages/shared/src/react/hooks/index.ts +++ b/packages/shared/src/react/hooks/index.ts @@ -8,3 +8,7 @@ export { useUser } from './useUser'; export { useClerk } from './useClerk'; export { useDeepEqualMemo, isDeeplyEqual } from './useDeepEqualMemo'; export { useReverification } from './useReverification'; +export { useStatements as __experimental_useStatements } from './useStatements'; +export { usePaymentAttempts as __experimental_usePaymentAttempts } from './usePaymentAttempts'; +export { usePaymentMethods as __experimental_usePaymentMethods } from './usePaymentMethods'; +export { useSubscriptionItems as __experimental_useSubscriptionItems } from './useSubscriptionItems'; diff --git a/packages/shared/src/react/hooks/usePagesOrInfinite.ts b/packages/shared/src/react/hooks/usePagesOrInfinite.ts index 3dd3d7d6046..6ffe9df7851 100644 --- a/packages/shared/src/react/hooks/usePagesOrInfinite.ts +++ b/packages/shared/src/react/hooks/usePagesOrInfinite.ts @@ -11,6 +11,25 @@ import type { ValueOrSetter, } from '../types'; +/** + * Returns an object containing only the keys from the first object that are not present in the second object. + * Useful for extracting unique parameters that should be passed to a request while excluding common cache keys. + * + * @internal + * + * @example + * ```typescript + * // Example 1: Basic usage + * const obj1 = { name: 'John', age: 30, city: 'NY' }; + * const obj2 = { name: 'John', age: 30 }; + * getDifferentKeys(obj1, obj2); // Returns { city: 'NY' } + * + * // Example 2: With cache keys + * const requestParams = { page: 1, limit: 10, userId: '123' }; + * const cacheKeys = { userId: '123' }; + * getDifferentKeys(requestParams, cacheKeys); // Returns { page: 1, limit: 10 } + * ``` + */ function getDifferentKeys(obj1: Record, obj2: Record): Record { const keysSet = new Set(Object.keys(obj2)); const differentKeysObject: Record = {}; @@ -24,6 +43,33 @@ function getDifferentKeys(obj1: Record, obj2: Record(params: T | true | undefined, defaultValues: T) => { const shouldUseDefaults = typeof params === 'boolean' && params; @@ -57,24 +103,40 @@ type ExtractData = Type extends { data: infer Data } ? ArrayType : T type UsePagesOrInfinite = < Params extends PagesOrInfiniteOptions, FetcherReturnData extends Record, - CacheKeys = Record, + CacheKeys extends Record = Record, TConfig extends PagesOrInfiniteConfig = PagesOrInfiniteConfig, >( /** - * The parameters will be passed to the fetcher + * The parameters will be passed to the fetcher. */ params: Params, /** - * A Promise returning function to fetch your data + * A Promise returning function to fetch your data. */ fetcher: ((p: Params) => FetcherReturnData | Promise) | undefined, /** - * Internal configuration of the hook + * Internal configuration of the hook. */ config: TConfig, cacheKeys: CacheKeys, ) => PaginatedResources, TConfig['infinite']>; +/** + * A flexible pagination hook that supports both traditional pagination and infinite loading. + * It provides a unified API for handling paginated data fetching, with built-in caching through SWR. + * The hook can operate in two modes: + * - Traditional pagination: Fetches one page at a time with page navigation + * - Infinite loading: Accumulates data as more pages are loaded. + * + * Features: + * - Cache management with SWR + * - Loading and error states + * - Page navigation helpers + * - Data revalidation and updates + * - Support for keeping previous data while loading. + * + * @internal + */ export const usePagesOrInfinite: UsePagesOrInfinite = (params, fetcher, config, cacheKeys) => { const [paginatedPage, setPaginatedPage] = useState(params.initialPage ?? 1); @@ -83,6 +145,7 @@ export const usePagesOrInfinite: UsePagesOrInfinite = (params, fetcher, config, const pageSizeRef = useRef(params.pageSize ?? 10); const enabled = config.enabled ?? true; + const cacheMode = config.__experimental_mode === 'cache'; const triggerInfinite = config.infinite ?? false; const keepPreviousData = config.keepPreviousData ?? false; @@ -93,22 +156,25 @@ export const usePagesOrInfinite: UsePagesOrInfinite = (params, fetcher, config, pageSize: pageSizeRef.current, }; + // cacheMode being `true` indicates that the cache key is defined, but the fetcher is not. + // This allows to ready the cache instead of firing a request. + const shouldFetch = !triggerInfinite && enabled && (!cacheMode ? !!fetcher : true); + const swrKey = shouldFetch ? pagesCacheKey : null; + const swrFetcher = + !cacheMode && !!fetcher + ? (cacheKeyParams: Record) => { + const requestParams = getDifferentKeys(cacheKeyParams, cacheKeys); + return fetcher({ ...params, ...requestParams }); + } + : null; + const { data: swrData, isValidating: swrIsValidating, isLoading: swrIsLoading, error: swrError, mutate: swrMutate, - } = useSWR( - !triggerInfinite && !!fetcher && enabled ? pagesCacheKey : null, - cacheKeyParams => { - // @ts-ignore - const requestParams = getDifferentKeys(cacheKeyParams, cacheKeys); - // @ts-ignore - return fetcher?.(requestParams); - }, - { keepPreviousData, ...cachingSWROptions }, - ); + } = useSWR(swrKey, swrFetcher, { keepPreviousData, ...cachingSWROptions }); const { data: swrInfiniteData, @@ -177,7 +243,7 @@ export const usePagesOrInfinite: UsePagesOrInfinite = (params, fetcher, config, const error = (triggerInfinite ? swrInfiniteError : swrError) ?? null; const isError = !!error; /** - * Helpers + * Helpers. */ const fetchNext = useCallback(() => { fetchPage(n => Math.max(0, n + 1)); diff --git a/packages/shared/src/react/hooks/usePaymentAttempts.tsx b/packages/shared/src/react/hooks/usePaymentAttempts.tsx new file mode 100644 index 00000000000..56cf4135b05 --- /dev/null +++ b/packages/shared/src/react/hooks/usePaymentAttempts.tsx @@ -0,0 +1,16 @@ +import type { CommercePaymentResource, GetPaymentAttemptsParams } from '@clerk/types'; + +import { useClerkInstanceContext } from '../contexts'; +import { createCommerceHook } from './createCommerceHook'; + +/** + * @internal + */ +export const usePaymentAttempts = createCommerceHook({ + hookName: 'usePaymentAttempts', + resourceType: 'commerce-payment-attempts', + useFetcher: () => { + const clerk = useClerkInstanceContext(); + return clerk.billing.getPaymentAttempts; + }, +}); diff --git a/packages/shared/src/react/hooks/usePaymentMethods.tsx b/packages/shared/src/react/hooks/usePaymentMethods.tsx new file mode 100644 index 00000000000..3524c8ca139 --- /dev/null +++ b/packages/shared/src/react/hooks/usePaymentMethods.tsx @@ -0,0 +1,21 @@ +import type { CommercePaymentSourceResource, GetPaymentSourcesParams } from '@clerk/types'; + +import { useOrganizationContext, useUserContext } from '../contexts'; +import { createCommerceHook } from './createCommerceHook'; + +/** + * @internal + */ +export const usePaymentMethods = createCommerceHook({ + hookName: 'usePaymentMethods', + resourceType: 'commerce-payment-methods', + useFetcher: resource => { + const { organization } = useOrganizationContext(); + const user = useUserContext(); + + if (resource === 'organization') { + return organization?.getPaymentSources; + } + return user?.getPaymentSources; + }, +}); diff --git a/packages/shared/src/react/hooks/useStatements.tsx b/packages/shared/src/react/hooks/useStatements.tsx new file mode 100644 index 00000000000..1ef1f9c0ff9 --- /dev/null +++ b/packages/shared/src/react/hooks/useStatements.tsx @@ -0,0 +1,16 @@ +import type { CommerceStatementResource, GetStatementsParams } from '@clerk/types'; + +import { useClerkInstanceContext } from '../contexts'; +import { createCommerceHook } from './createCommerceHook'; + +/** + * @internal + */ +export const useStatements = createCommerceHook({ + hookName: 'useStatements', + resourceType: 'commerce-statements', + useFetcher: () => { + const clerk = useClerkInstanceContext(); + return clerk.billing.getStatements; + }, +}); diff --git a/packages/shared/src/react/hooks/useSubscriptionItems.tsx b/packages/shared/src/react/hooks/useSubscriptionItems.tsx new file mode 100644 index 00000000000..db2db7eb889 --- /dev/null +++ b/packages/shared/src/react/hooks/useSubscriptionItems.tsx @@ -0,0 +1,16 @@ +import type { CommerceSubscriptionResource, GetSubscriptionsParams } from '@clerk/types'; + +import { useClerkInstanceContext } from '../contexts'; +import { createCommerceHook } from './createCommerceHook'; + +/** + * @internal + */ +export const useSubscriptionItems = createCommerceHook({ + hookName: 'useSubscriptionItems', + resourceType: 'commerce-subscription-items', + useFetcher: () => { + const clerk = useClerkInstanceContext(); + return clerk.billing.getSubscriptions; + }, +}); diff --git a/packages/shared/src/react/types.ts b/packages/shared/src/react/types.ts index 07ef8354e50..eb5d39e9b9b 100644 --- a/packages/shared/src/react/types.ts +++ b/packages/shared/src/react/types.ts @@ -90,11 +90,13 @@ export type PaginatedResourcesWithDefault = { export type PaginatedHookConfig = T & { /** * If `true`, newly fetched data will be appended to the existing list rather than replacing it. Useful for implementing infinite scroll functionality. + * * @default false */ infinite?: boolean; /** * If `true`, the previous data will be kept in the cache until new data is fetched. + * * @default false */ keepPreviousData?: boolean; @@ -102,10 +104,18 @@ export type PaginatedHookConfig = T & { export type PagesOrInfiniteConfig = PaginatedHookConfig<{ /** - * If `true`, a request will be triggered. + * If `true`, a request will be triggered when the hook is mounted. + * * @default true */ enabled?: boolean; + /** + * @experimental + * On `cache` mode, no request will be triggered when the hook is mounted and the data will be fetched from the cache. + * + * @default undefined + */ + __experimental_mode?: 'cache'; }>; /** @@ -114,12 +124,21 @@ export type PagesOrInfiniteConfig = PaginatedHookConfig<{ export type PagesOrInfiniteOptions = { /** * A number that specifies which page to fetch. For example, if `initialPage` is set to 10, it will skip the first 9 pages and fetch the 10th page. + * * @default 1 */ initialPage?: number; /** * A number that specifies the maximum number of results to return per page. + * * @default 10 */ pageSize?: number; + /** + * @experimental + * On `cache` mode, no request will be triggered when the hook is mounted and the data will be fetched from the cache. + * + * @default undefined + */ + __experimental_mode?: 'cache'; };