page wrapper
This commit is contained in:
87
apps/website/lib/page/PageDataFetcher.ts
Normal file
87
apps/website/lib/page/PageDataFetcher.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { ContainerManager } from '@/lib/di/container';
|
||||
|
||||
export interface FetchResult<T> {
|
||||
data: T | null;
|
||||
errors: Record<string, Error>;
|
||||
hasErrors: boolean;
|
||||
}
|
||||
|
||||
export class PageDataFetcher {
|
||||
/**
|
||||
* Fetch data using DI container
|
||||
* Use for: Simple SSR pages with single service
|
||||
* WARNING: Container is singleton - avoid stateful services
|
||||
*/
|
||||
static async fetch<TService, TMethod extends keyof TService>(
|
||||
ServiceToken: string | symbol,
|
||||
method: TMethod,
|
||||
...args: TService[TMethod] extends (...params: infer P) => Promise<infer R> ? P : never
|
||||
): Promise<(TService[TMethod] extends (...params: any[]) => Promise<infer R> ? R : never) | null> {
|
||||
try {
|
||||
const container = ContainerManager.getInstance().getContainer();
|
||||
const service = container.get<TService>(ServiceToken);
|
||||
const result = await (service[method] as Function)(...args);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch: ${String(ServiceToken)}.${String(method)}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch using manual service instantiation
|
||||
* Use for: Multiple dependencies, request-scoped services, or auth context
|
||||
* RECOMMENDED for SSR over fetch() with DI
|
||||
*/
|
||||
static async fetchManual<TData>(
|
||||
serviceFactory: () => Promise<TData> | TData
|
||||
): Promise<TData | null> {
|
||||
try {
|
||||
const result = await serviceFactory();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch manual:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch multiple datasets in parallel with error aggregation
|
||||
* Use for: Pages needing multiple service calls
|
||||
* UPDATED: Returns both data and errors for proper handling
|
||||
*/
|
||||
static async fetchMultiple<T extends Record<string, any>>(
|
||||
queries: T
|
||||
): Promise<FetchResult<{ [K in keyof T]: T[K] }>> {
|
||||
const results = {} as { [K in keyof T]: T[K] };
|
||||
const errors = {} as Record<string, Error>;
|
||||
|
||||
const entries = await Promise.all(
|
||||
Object.entries(queries).map(async ([key, query]) => {
|
||||
try {
|
||||
const result = await query();
|
||||
return [key, { success: true, data: result }] as const;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch ${key}:`, error);
|
||||
return [key, { success: false, error: error instanceof Error ? error : new Error(String(error)) }] as const;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
entries.forEach(([key, result]) => {
|
||||
if (typeof result === 'object' && result !== null && 'success' in result) {
|
||||
if (result.success) {
|
||||
results[key as keyof T] = (result as any).data;
|
||||
} else {
|
||||
errors[key] = (result as any).error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
data: results,
|
||||
errors,
|
||||
hasErrors: Object.keys(errors).length > 0
|
||||
};
|
||||
}
|
||||
}
|
||||
149
apps/website/lib/page/usePageData.ts
Normal file
149
apps/website/lib/page/usePageData.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useQuery, useQueries, UseQueryOptions, useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
|
||||
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>
|
||||
) {
|
||||
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, any>>(
|
||||
queries: {
|
||||
[K in keyof T]: PageDataConfig<T[K]>;
|
||||
}
|
||||
) {
|
||||
const queryResults = useQueries({
|
||||
queries: Object.entries(queries).map(([key, config]) => {
|
||||
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
|
||||
*
|
||||
* @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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user