From 1ed0ae9de48ae669b002ef7e31479eae1826ab6c Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Wed, 3 Dec 2025 00:19:45 +0100 Subject: [PATCH] fix: Improve optimistic updates in favorites mutation handling --- client/src/data-provider/Favorites.ts | 16 ++++++++++++++-- client/src/hooks/useFavorites.ts | 23 +++++++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/client/src/data-provider/Favorites.ts b/client/src/data-provider/Favorites.ts index f62c153da3..655a371363 100644 --- a/client/src/data-provider/Favorites.ts +++ b/client/src/data-provider/Favorites.ts @@ -24,8 +24,20 @@ export const useUpdateFavoritesMutation = () => { (favorites: FavoritesState) => dataService.updateFavorites(favorites) as Promise, { - onSuccess: (data) => { - queryClient.setQueryData(['favorites'], data); + // Optimistic update to prevent UI flickering when toggling favorites + // Sets query cache immediately before the request completes + onMutate: async (newFavorites) => { + await queryClient.cancelQueries(['favorites']); + + const previousFavorites = queryClient.getQueryData(['favorites']); + queryClient.setQueryData(['favorites'], newFavorites); + + return { previousFavorites }; + }, + onError: (_err, _newFavorites, context) => { + if (context?.previousFavorites) { + queryClient.setQueryData(['favorites'], context.previousFavorites); + } }, }, ); diff --git a/client/src/hooks/useFavorites.ts b/client/src/hooks/useFavorites.ts index c32570f783..6fbe311689 100644 --- a/client/src/hooks/useFavorites.ts +++ b/client/src/hooks/useFavorites.ts @@ -1,4 +1,4 @@ -import { useEffect, useCallback } from 'react'; +import { useEffect, useCallback, useRef } from 'react'; import { useAtom } from 'jotai'; import { useToastContext } from '@librechat/client'; import type { Favorite } from '~/store/favorites'; @@ -44,7 +44,14 @@ export default function useFavorites() { const getFavoritesQuery = useGetFavoritesQuery(); const updateFavoritesMutation = useUpdateFavoritesMutation(); + const isMutatingRef = useRef(false); + useEffect(() => { + // Skip updating local state if a mutation is in progress or just completed + // The local state is already optimistically updated by saveFavorites + if (isMutatingRef.current || updateFavoritesMutation.isLoading) { + return; + } if (getFavoritesQuery.data) { if (Array.isArray(getFavoritesQuery.data)) { setFavorites(getFavoritesQuery.data); @@ -52,12 +59,13 @@ export default function useFavorites() { setFavorites([]); } } - }, [getFavoritesQuery.data, setFavorites]); + }, [getFavoritesQuery.data, setFavorites, updateFavoritesMutation.isLoading]); const saveFavorites = useCallback( async (newFavorites: typeof favorites) => { const cleaned = cleanFavorites(newFavorites); setFavorites(cleaned); + isMutatingRef.current = true; try { await updateFavoritesMutation.mutateAsync(cleaned); } catch (error) { @@ -65,6 +73,12 @@ export default function useFavorites() { showToast({ message: localize('com_ui_error'), status: 'error' }); // Refetch to resync state with server getFavoritesQuery.refetch(); + } finally { + // Use a small delay to prevent the useEffect from triggering immediately + // after the mutation completes but before React has finished processing + setTimeout(() => { + isMutatingRef.current = false; + }, 100); } }, [setFavorites, updateFavoritesMutation, showToast, localize, getFavoritesQuery], @@ -129,6 +143,7 @@ export default function useFavorites() { const cleaned = cleanFavorites(newFavorites); setFavorites(cleaned); if (persist) { + isMutatingRef.current = true; try { await updateFavoritesMutation.mutateAsync(cleaned); } catch (error) { @@ -136,6 +151,10 @@ export default function useFavorites() { showToast({ message: localize('com_ui_error'), status: 'error' }); // Refetch to resync state with server getFavoritesQuery.refetch(); + } finally { + setTimeout(() => { + isMutatingRef.current = false; + }, 100); } } },