import { useQuery, useMutation } from 'convex/react';
import { useEffect, useState } from 'react';
import type {
OptionalQueryArgs,
OptionalQueryArgsOrSkip,
FunctionArgs,
FunctionResult,
FunctionReference
} from 'convex/server';
/**
* Hook that wraps Convex useQuery with local storage caching
* @param key - Local storage key to use for caching
* @param queryFn - Convex query function reference
* @param args - Arguments to pass to query function
* @returns Data from local storage while loading, then remote data
*/
export function useQueryWithLocalStorage<Query extends FunctionReference<'query', 'public'>>(
key: string,
queryFn: Query,
...args: OptionalQueryArgsOrSkip<Query>
): FunctionResult<Query> | null {
// Get initial data from local storage
const [localData, setLocalData] = useState<FunctionResult<Query> | null>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : null;
});
// Get remote data
const remoteData = useQuery(queryFn, ...args);
// Update local storage when remote data changes
useEffect(() => {
if (remoteData !== undefined) {
localStorage.setItem(key, JSON.stringify(remoteData));
setLocalData(remoteData);
}
}, [key, remoteData]);
// Return local data while loading, then remote data
return remoteData === undefined ? localData : remoteData;
}
/**
* Hook that wraps Convex useMutation with optimistic local storage updates
* @param key - Local storage key to update
* @param mutationFn - Convex mutation function reference
* @returns Wrapped mutation function that handles local storage updates
*/
export function useMutationWithLocalStorage<Mutation extends FunctionReference<'mutation', 'public'>>(
key: string,
mutationFn: Mutation
): (args: FunctionArgs<Mutation>) => Promise<FunctionResult<Mutation>> {
const mutation = useMutation(mutationFn);
const wrappedMutation = async (args: FunctionArgs<Mutation>) => {
// Get current data
const currentData = localStorage.getItem(key);
const parsedData = currentData ? JSON.parse(currentData) : null;
try {
// Optimistically update local storage
if (parsedData) {
// Assuming the mutation affects the data in a predictable way
// You may need to customize this based on your mutation logic
const optimisticData = Array.isArray(parsedData)
? [...parsedData, args] // If array, append new item
: { ...parsedData, ...args }; // If object, merge new data
localStorage.setItem(key, JSON.stringify(optimisticData));
}
// Call remote mutation
const result = await mutation(args);
return result;
} catch (error) {
// Revert local storage on error
if (currentData) {
localStorage.setItem(key, currentData);
}
throw error;
}
};
return wrappedMutation;
}