|
import { useCustomQuery } from "@gs-libs/bridge"; |
|
import { useCallback, useEffect, useMemo, useState } from "react"; |
|
import { queryClient } from "@/services/queryClient"; |
|
|
|
export type CustomQueryOptions<QueryData> = { |
|
queryFn: () => Promise<QueryData>; |
|
queryKey: unknown[]; |
|
retry?: |
|
| boolean |
|
| number |
|
| ((failureCount: number, error: unknown) => boolean); |
|
retryDelay?: number | ((failureCount: number, error: unknown) => number); |
|
initialData?: QueryData; |
|
enabled?: boolean; |
|
cacheTime?: number; |
|
isDataEqual?: (oldData: QueryData | undefined, newData: QueryData) => boolean; |
|
keepPreviousData?: boolean; |
|
onError?: (error: unknown) => void; |
|
onSettled?: (data: QueryData | undefined, error: unknown) => void; |
|
onSuccess?: (data: QueryData) => void; |
|
refetchInterval?: number | false; |
|
refetchIntervalInBackground?: boolean; |
|
refetchOnMount?: boolean | "always"; |
|
refetchOnWindowFocus?: boolean | "always"; |
|
refetchOnReconnect?: boolean | "always"; |
|
staleTime?: number; |
|
structuralSharing?: boolean; |
|
useErrorBoundary?: boolean; |
|
}; |
|
|
|
export type CustomQueryOptionsWithSelect<QueryData, Selection> = |
|
CustomQueryOptions<QueryData> & { |
|
select: (value: QueryData) => Selection; |
|
}; |
|
|
|
/** |
|
* Instead of defining our re-usable queries as custom hooks, we can |
|
* instead define them as functions that return their options object. |
|
* This gives us more control over how we use the query, as it becomes |
|
* extremely easy to override options in different contexts. |
|
* |
|
* @example |
|
* const getUserQuery = (userId:string) => |
|
* queryOptions({ |
|
* queryFn: () => fetchUser(userId), |
|
* queryKey: ['user', userId], |
|
* }); |
|
*/ |
|
export function queryOptions<QueryData, Selection>( |
|
options: CustomQueryOptionsWithSelect<QueryData, Selection>, |
|
): CustomQueryOptionsWithSelect<QueryData, Selection>; |
|
export function queryOptions<QueryData>( |
|
options: CustomQueryOptions<QueryData>, |
|
): CustomQueryOptions<QueryData>; |
|
export function queryOptions(options: any) { |
|
return options; |
|
} |
|
|
|
export function useCustomSuspenseQueryData<Data, Selection>( |
|
options: CustomQueryOptionsWithSelect<Data, Selection>, |
|
): Selection; |
|
export function useCustomSuspenseQueryData<Data>( |
|
options: CustomQueryOptions<Data>, |
|
): Data; |
|
export function useCustomSuspenseQueryData(options: any) { |
|
return useCustomQuery({ |
|
...options, |
|
suspense: true, |
|
}).data; |
|
} |
|
|
|
export const getQueryState = <Data>(options: CustomQueryOptions<Data>) => |
|
queryClient.getQueryState<Data>(options.queryKey); |
|
|
|
export const fetchQuery = async <Data>( |
|
options: CustomQueryOptions<Data>, |
|
): Promise<Data> => queryClient.fetchQuery<Data>(options); |
|
|
|
/** |
|
* If query has already been fetched, return that data. Otherwise, |
|
* fetch the query and return the data. |
|
*/ |
|
export const ensureQuery = async <Data>( |
|
options: CustomQueryOptions<Data>, |
|
): Promise<Data> => { |
|
const beforePrefetchQueryState = getQueryState(options); |
|
if (beforePrefetchQueryState?.data === "success") { |
|
return beforePrefetchQueryState.data; |
|
} |
|
await queryClient.prefetchQuery(options); |
|
const afterPrefetchQueryState = getQueryState(options); |
|
|
|
if (afterPrefetchQueryState?.data) { |
|
return afterPrefetchQueryState.data; |
|
} |
|
if (afterPrefetchQueryState?.error) { |
|
throw afterPrefetchQueryState.error; |
|
} |
|
|
|
throw new Error("Query failed to fetch"); |
|
}; |
|
|
|
type StateUpdater<T> = ((oldState: T) => T) | T; |
|
|
|
const deriveNewState = <T>(oldState: T, updater: StateUpdater<T>): T => |
|
typeof updater === "function" |
|
? (updater as (_oldState: T) => T)(oldState) |
|
: updater; |
|
|
|
export const useQueryOptimisticUpdater = <Data>( |
|
options: CustomQueryOptions<Data>, |
|
) => { |
|
const query = useCustomQuery(options); |
|
const [previousData, setPreviousData] = useState<Data | undefined>(undefined); |
|
const [lastOptimisticUpdateDate, setLastOptimisticUpdateDate] = useState< |
|
Date | undefined |
|
>(undefined); |
|
const [lastDetectedDataUpdateDate, setLastDetectedDataUpdateDate] = useState< |
|
Date | undefined |
|
>(undefined); |
|
|
|
useEffect(() => { |
|
setLastDetectedDataUpdateDate(new Date()); |
|
}, [query.data]); |
|
|
|
const currentQueryDataIsOptimistic = useMemo(() => { |
|
if (!lastOptimisticUpdateDate) return false; |
|
if (!lastDetectedDataUpdateDate) return true; |
|
|
|
const msDiffBetweenLastOptimisticUpdateAndLastDetectedDataUpdate = Math.abs( |
|
lastOptimisticUpdateDate.getTime() - lastDetectedDataUpdateDate.getTime(), |
|
); |
|
|
|
return msDiffBetweenLastOptimisticUpdateAndLastDetectedDataUpdate < 100; |
|
}, [lastOptimisticUpdateDate, lastDetectedDataUpdateDate]); |
|
|
|
const revert = useCallback(() => { |
|
if (currentQueryDataIsOptimistic) { |
|
queryClient.setQueryData(options.queryKey, previousData); |
|
} |
|
}, [previousData, options.queryKey, currentQueryDataIsOptimistic]); |
|
|
|
const optimisticUpdate = useCallback( |
|
(stateUpdater: StateUpdater<Data | undefined>) => { |
|
const nextState = deriveNewState(query.data, stateUpdater); |
|
setPreviousData(query.data); |
|
|
|
queryClient.setQueryData(options.queryKey, nextState); |
|
setLastOptimisticUpdateDate(new Date()); |
|
}, |
|
[query.data, options.queryKey], |
|
); |
|
|
|
/** |
|
* Takes a promise and an optimistic update for the query. Applies the |
|
* optimistic update straight away, then waits for the promise to resolve. |
|
* If the promise resolves, the query is refetched, and if a value adapter |
|
* function is provided, the value return by the promise is adapted and |
|
* inserted into the query while the query is being refetched. |
|
* |
|
* If the promise rejects, the optimistic update is reverted. |
|
*/ |
|
const connectToPromise = useCallback( |
|
async <PromiseValue>( |
|
thePromise: Promise<PromiseValue>, |
|
optimisticValue: Data, |
|
valueAdapter?: (promiseValue: PromiseValue) => Data, |
|
) => { |
|
optimisticUpdate(optimisticValue); |
|
try { |
|
const result = await thePromise; |
|
if (valueAdapter) { |
|
const newQueryData = valueAdapter(result); |
|
optimisticUpdate(newQueryData); |
|
} |
|
query.refetch(); |
|
} catch (error) { |
|
revert(); |
|
throw error; |
|
} |
|
}, |
|
[optimisticUpdate, revert, query], |
|
); |
|
|
|
return { |
|
optimisticUpdate, |
|
revert, |
|
connectToPromise, |
|
currentQueryDataIsOptimistic, |
|
}; |
|
}; |