- Add Skeletons.tsx with TastingListSkeleton, ChartSkeleton, etc. - Add useOptimistic.ts hooks for React 19 optimistic updates - Update stats page to use skeleton loading instead of spinner - Remove force-dynamic exports (12 files) for SSG compatibility - Note: PPR (cacheComponents) tested but reverted - requires RSC-first refactor
71 lines
1.9 KiB
TypeScript
71 lines
1.9 KiB
TypeScript
'use client';
|
|
|
|
import { useOptimistic, useTransition } from 'react';
|
|
|
|
/**
|
|
* Hook for optimistic updates with automatic rollback on error.
|
|
* Uses React 19's useOptimistic + startTransition pattern.
|
|
*
|
|
* @example
|
|
* const { optimisticData, isPending, mutate } = useOptimisticMutation(
|
|
* tastings,
|
|
* async (newTasting) => await saveTasting(newTasting)
|
|
* );
|
|
*/
|
|
export function useOptimisticMutation<T, TInput>(
|
|
initialData: T[],
|
|
mutationFn: (input: TInput) => Promise<{ success: boolean; error?: string }>
|
|
) {
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
const [optimisticData, addOptimistic] = useOptimistic<T[], TInput>(
|
|
initialData,
|
|
(currentData, newItem) => [...currentData, newItem as unknown as T]
|
|
);
|
|
|
|
const mutate = async (input: TInput, optimisticValue: T) => {
|
|
startTransition(async () => {
|
|
// Immediately show optimistic update
|
|
addOptimistic(input);
|
|
|
|
// Perform actual mutation
|
|
const result = await mutationFn(input);
|
|
|
|
if (!result.success) {
|
|
console.error('[OptimisticMutation] Failed:', result.error);
|
|
// Note: React will automatically rollback on error
|
|
}
|
|
});
|
|
};
|
|
|
|
return {
|
|
optimisticData,
|
|
isPending,
|
|
mutate,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Simple optimistic state for single values (like ratings).
|
|
*/
|
|
export function useOptimisticValue<T>(
|
|
serverValue: T,
|
|
updateFn: (value: T) => Promise<{ success: boolean }>
|
|
) {
|
|
const [isPending, startTransition] = useTransition();
|
|
const [optimisticValue, setOptimistic] = useOptimistic(serverValue);
|
|
|
|
const setValue = (value: T) => {
|
|
startTransition(async () => {
|
|
setOptimistic(value);
|
|
await updateFn(value);
|
|
});
|
|
};
|
|
|
|
return {
|
|
value: optimisticValue,
|
|
isPending,
|
|
setValue,
|
|
};
|
|
}
|