Files
gridpilot.gg/apps/website/lib/page/usePageData.ts
2026-01-24 12:47:49 +01:00

154 lines
4.0 KiB
TypeScript

'use client';
import { ApiError } from '@/lib/gateways/api/base/ApiError';
import { useMutation, UseMutationOptions, useQueries, useQuery } from '@tanstack/react-query';
import React from 'react';
export interface PageDataConfig<TData, TError = ApiError> {
queryKey: string[];
queryFn: () => Promise<TData>;
enabled?: boolean;
staleTime?: number;
onError?: (error: TError) => void;
}
/**
* Single query hook - STANDARDIZED PATTERN
* Use for: Simple CSR pages
*
* @example
* const { data, isLoading, error, refetch } = usePageData({
* queryKey: ['profile'],
* queryFn: () => driverService.getProfile(),
* });
*/
export function usePageData<TData, TError = ApiError>(
config: PageDataConfig<TData, TError>
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const queryOptions: any = {
queryKey: config.queryKey,
queryFn: config.queryFn,
enabled: config.enabled ?? true,
staleTime: config.staleTime ?? 1000 * 60 * 5,
};
if (config.onError) {
queryOptions.onError = config.onError;
}
return useQuery<TData, TError>(queryOptions);
}
/**
* Multiple queries hook - STANDARDIZED PATTERN
* Use for: Complex CSR pages with multiple data sources
*
* @example
* const { data, isLoading, error, refetch } = usePageDataMultiple({
* results: {
* queryKey: ['raceResults', raceId],
* queryFn: () => service.getResults(raceId),
* },
* sof: {
* queryKey: ['raceSOF', raceId],
* queryFn: () => service.getSOF(raceId),
* },
* });
*/
export function usePageDataMultiple<T extends Record<string, unknown>>(
queries: {
[K in keyof T]: PageDataConfig<T[K]>;
}
) {
const queryResults = useQueries({
queries: Object.entries(queries).map(([_key, config]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const queryOptions: any = {
queryKey: config.queryKey,
queryFn: config.queryFn,
enabled: config.enabled ?? true,
staleTime: config.staleTime ?? 1000 * 60 * 5,
};
if (config.onError) {
queryOptions.onError = config.onError;
}
return queryOptions;
}),
});
// Combine results
const combined = {} as { [K in keyof T]: T[K] | null };
const keys = Object.keys(queries) as (keyof T)[];
keys.forEach((key, index) => {
const result = queryResults[index]?.data;
if (result !== undefined) {
combined[key] = result as T[typeof key];
} else {
combined[key] = null as T[typeof key] | null;
}
});
const isLoading = queryResults.some(q => q.isLoading);
const error = queryResults.find(q => q.error)?.error ?? null;
return {
data: combined,
isLoading,
error,
refetch: () => queryResults.forEach(q => q.refetch()),
};
}
/**
* Mutation hook wrapper - STANDARDIZED PATTERN
* Use for: All mutation operations
*
* @deprecated Use Next.js Server Actions instead for all write operations.
* See docs/architecture/website/FORM_SUBMISSION.md
*
* @example
* const mutation = usePageMutation(
* (variables) => service.mutateData(variables),
* { onSuccess: () => refetch() }
* );
*/
export function usePageMutation<TData, TVariables, TError = ApiError>(
mutationFn: (variables: TVariables) => Promise<TData>,
options?: Omit<UseMutationOptions<TData, TError, TVariables>, 'mutationFn'>
) {
return useMutation<TData, TError, TVariables>({
mutationFn,
...options,
});
}
/**
* SSR Hydration Hook - NEW
* Use for: Passing SSR data to CSR to avoid re-fetching
*
* @example
* // In SSR page
* const ssrData = await PageDataFetcher.fetch(...);
*
* // In client component
* const { data } = useHydrateSSRData(ssrData, ['queryKey']);
*/
export function useHydrateSSRData<TData>(
ssrData: TData | null,
_queryKey: string[]
): { data: TData | null; isHydrated: boolean } {
const [isHydrated, setIsHydrated] = React.useState(false);
React.useEffect(() => {
if (ssrData !== null) {
setIsHydrated(true);
}
}, [ssrData]);
return {
data: ssrData,
isHydrated,
};
}