From 20f6f6918f9c3f942cbc73c0eae999783c88516a Mon Sep 17 00:00:00 2001 From: Avraham Sakal Date: Sun, 31 Aug 2025 13:40:28 -0400 Subject: [PATCH] optimistically update "delete conversation" mutation --- layouts/LayoutDefault.tsx | 85 ++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 23 deletions(-) diff --git a/layouts/LayoutDefault.tsx b/layouts/LayoutDefault.tsx index 7f34c48..a57e62c 100644 --- a/layouts/LayoutDefault.tsx +++ b/layouts/LayoutDefault.tsx @@ -151,39 +151,78 @@ function NavLinkChat() { ); const deleteConversation = useMutation( trpc.chat.conversations.deleteOne.mutationOptions({ - onSuccess: () => { - queryClient.invalidateQueries({ + onMutate: async ({ id: conversationIdToDelete }) => { + /** Cancel affected queries that may be in-flight: */ + await queryClient.cancelQueries({ + queryKey: trpc.chat.conversations.fetchAll.queryKey(), + }); + + /** Optimistically update the affected queries in react-query's cache: */ + const previousConversations = await queryClient.getQueryData( + trpc.chat.conversations.fetchAll.queryKey() + ); + if (!previousConversations) { + return { + previousConversations: [], + newConversations: [], + }; + } + const newConversations = previousConversations.filter( + (c) => c.id !== conversationIdToDelete + ); + queryClient.setQueryData( + trpc.chat.conversations.fetchAll.queryKey(), + newConversations + ); + + return { previousConversations, newConversations }; + }, + onSettled: async (data, variables, context) => { + await queryClient.invalidateQueries({ queryKey: trpc.chat.conversations.fetchAll.queryKey(), }); }, + onError: async (error, variables, context) => { + console.error(error); + if (!context) return; + queryClient.setQueryData( + trpc.chat.conversations.fetchAll.queryKey(), + context.previousConversations + ); + }, }) ); const { data: conversations } = useQuery( trpc.chat.conversations.fetchAll.queryOptions() ); - // TODO: should we be using zustand for this, or trpc/react-query's useMutation? - const addConversation = useStore((state) => state.addConversation); - const removeConversation = useStore((state) => state.removeConversation); - const conversationId = useStore((state) => state.selectedConversationId); + const selectedConversationId = useStore( + (state) => state.selectedConversationId + ); - async function handleDeleteConversation(conversationId: string) { + async function handleDeleteConversation(conversationIdToDelete: string) { await deleteConversation.mutateAsync( - { id: conversationId } - // { - // onSuccess: () => { - // queryClient.invalidateQueries({ - // queryKey: trpc.chat.conversations.fetchAll.queryKey(), - // }); - // }, - // } + { id: conversationIdToDelete }, + { + onSuccess: async (x, y, { newConversations }) => { + /** If the selected conversation was deleted, navigate/select a + * different conversation (creating a new one if necessary): */ + if (conversationIdToDelete === selectedConversationId) { + if (newConversations && newConversations.length > 0) { + // Navigate to the first conversation + const lastConversation = + newConversations[newConversations.length - 1]; + await navigate(`/chat/${lastConversation.id}`); + } else { + // No conversations left, create a new one + const newConversation = await startConversation.mutateAsync(); + if (!newConversation?.id) return; + await navigate(`/chat/${newConversation.id}`); + } + } + }, + } ); - removeConversation(conversationId); - - const newConversation = await startConversation.mutateAsync(); - if (!newConversation?.id) return; - addConversation(newConversation); - await navigate(`/chat/${newConversation.id}`); } return ( @@ -202,7 +241,7 @@ function NavLinkChat() { e.stopPropagation(); startConversation.mutateAsync().then((res) => { if (!res?.id) return; - addConversation(res); + // addConversation(res); navigate(`/chat/${res.id}`); }); }} @@ -262,7 +301,7 @@ function NavLinkChat() { } variant="subtle" - active={conversation.id === conversationId} + active={conversation.id === selectedConversationId} /> ))}