feat: Add UX optimizations - skeletons and optimistic hooks
- 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
This commit is contained in:
70
src/hooks/useOptimistic.ts
Normal file
70
src/hooks/useOptimistic.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
'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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user