Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mutation Optimistic Update Causes Reference Changes and Unnecessary Rerenders #8382

Open
pavel-sindelka opened this issue Dec 2, 2024 · 2 comments

Comments

@pavel-sindelka
Copy link

pavel-sindelka commented Dec 2, 2024

Describe the bug

When using a mutation for deleting an item from a list with an optimistic response, the implementation causes reference changes for all items following the deleted one. This leads to unnecessary re-renders of components that receive these items as props, even if they use React.memo.

The issue seems to stem from how the list is updated optimistically (filter function creates a new array, causing reference changes for all remaining items).

Your minimal, reproducible example

https://stackblitz.com/edit/sb1-tgynyr?file=components%2Ftodo-list.tsx

Steps to reproduce

  1. Open the provided StackBlitz example.
  2. Observe that TodoItem components rerender even when their data does not change (e.g., via console logs in React.memo-wrapped components).
  3. Delete a todo item using the delete button.
  4. Note that all TodoItem components after the deleted one re-render.

Expected behavior

When deleting a todo item and updating the list optimistically:

  • Only the removed item's reference should change.
  • Other items should retain their references to prevent unnecessary re-renders.

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

any

Tanstack Query adapter

react-query

TanStack Query version

v5.62.0

TypeScript version

v5.2.2

Additional context

The issue is due to how the filter function creates a new array and changes references for all subsequent items. This behavior can negatively impact performance in scenarios with a large number of items or deeply nested component trees.

@TkDodo
Copy link
Collaborator

TkDodo commented Dec 6, 2024

when you change an array or object, we try to keep as many references as possible with our structuralSharing feature.

That means if you toggle one todo, even though the list is new and even if you’d provide a new copy of each element, only the changed one would re-render.

If you delete one item from the list, all other items after that would “compare” with a different item, and we therefor can’t keep the reference.

I wouldn’t know how to implement that, I don’t think it’s possible - or do you have an idea?

@pavel-sindelka
Copy link
Author

@TkDodo thanks for the response. So I have two options? Mutate object or use structuralSharing: false? Or is there another way to approach deleting items from the array without changing the reference of the following items?

const deleteMutation = useMutation({
    mutationFn: deleteTodo,
    onMutate: async (todoId) => {
      await queryClient.cancelQueries({ queryKey: ['todos'] });

      const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);

      queryClient.setQueryData<Todo[]>(['todos'], (old = []) => {
        const index = old.findIndex((todo) => todo.id === todoId);
        if (index !== -1) {
          const updatedTodos = old;
          updatedTodos.splice(index, 1);
          return updatedTodos;
        }
        return old;
      });

      return { previousTodos };
    },
    onError: (_, __, context) => {
      queryClient.setQueryData(['todos'], context?.previousTodos);
    },
  });
  const { data: todos = [] } = useQuery({
    queryKey: ['todos'],
    queryFn: getTodos,
    structuralSharing: false,
  });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants