T
TanStack•2y ago
other-emerald

Handling rerendering and useMutation

Hi all, currently when wanting to force a rerender on a mutation I move the mutation up in state and pass it as a callback down to the component that needs to trigger it. This often results in prop drilling and coupled state. What is a better approach? Simplified example:
const Chat = () => {
const { mutate: addMessage } = useAddMessage();

return(
<ChatHeader>
<ChatMessages>
<ChatInput addMessage={addMessage}>
);
}
const Chat = () => {
const { mutate: addMessage } = useAddMessage();

return(
<ChatHeader>
<ChatMessages>
<ChatInput addMessage={addMessage}>
);
}
The useAddMessage hook inserts the message into the cache of an InfiniteQuery using setQueryData Using version 4.24.6
12 Replies
other-emerald
other-emeraldOP•2y ago
In the example above its not that bad, but I do run into situations as my app grows where it gets out of hand
xenophobic-harlequin
xenophobic-harlequin•2y ago
Hello I don't get it. Do you want to rerender ChatInput after addMessage mutation?
other-emerald
other-emeraldOP•2y ago
No the ChatMessages, so that it renders the message that was added to the cache 🙂 AFAIK and experience, a mutation function only rerenders the component the hook is called from And thus its children
xenophobic-harlequin
xenophobic-harlequin•2y ago
Can you show ChatMessages internals?
other-emerald
other-emeraldOP•2y ago
Basically it does something like this:
const {
data: messages,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
status,
} = useMessages(chatUuid);
const {
data: messages,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
status,
} = useMessages(chatUuid);
Then flatMaps messages and render it. Where useMessages looks like this:
export default function useMessages(chatUuid) {
return useInfiniteQuery(
["chatMessages", chatUuid],
async ({ pageParam }) => {
const url = constructCursorPaginatedUrl(
`/chats/${chatUuid}/messages/`,
pageParam
);
const { data } = await api.get(url);
return data;
},
{
getNextPageParam: (lastPage) => lastPage.next,
enabled: !!chatUuid,
}
);
}
export default function useMessages(chatUuid) {
return useInfiniteQuery(
["chatMessages", chatUuid],
async ({ pageParam }) => {
const url = constructCursorPaginatedUrl(
`/chats/${chatUuid}/messages/`,
pageParam
);
const { data } = await api.get(url);
return data;
},
{
getNextPageParam: (lastPage) => lastPage.next,
enabled: !!chatUuid,
}
);
}
Please keep in mind the chat example is just an example (although it exists) The question is more: How do I ensure a component using a certain cache rerenders when I setQueryData in a useMutation from another component that is not one of its parents
xenophobic-harlequin
xenophobic-harlequin•2y ago
You may use queryClient.invalidateQuery() function to invalidate the key of another query. Here's an example
export function useLogin() {
const queryClient = useQueryClient();
const { setValue } = useToken();

return useMutation(services.auth.login, {
onSuccess: (res, dto) => {
// Persist token
const { token } = res.data;
setValue(token);
// Send analitic events
loginEvent();
dataLayer.push({
event: 'login',
user_properties: {
email: dto.email,
},
});
// Get user data
queryClient.invalidateQueries(['users']);
},
onError: (err) => {
if (isHttpError(err)) {
errorEvent(err.message);
toast.error(err.message);
}
},
});
}
export function useLogin() {
const queryClient = useQueryClient();
const { setValue } = useToken();

return useMutation(services.auth.login, {
onSuccess: (res, dto) => {
// Persist token
const { token } = res.data;
setValue(token);
// Send analitic events
loginEvent();
dataLayer.push({
event: 'login',
user_properties: {
email: dto.email,
},
});
// Get user data
queryClient.invalidateQueries(['users']);
},
onError: (err) => {
if (isHttpError(err)) {
errorEvent(err.message);
toast.error(err.message);
}
},
});
}
other-emerald
other-emeraldOP•2y ago
But I don't want to invalidateQueries, I want to setQueryData to directly update based on the backend response:)
xenophobic-harlequin
xenophobic-harlequin•2y ago
Then you need somehow get chatUuid in your mutation so that you can access ["chatMessages", chatUuid] Here's an example of an optimistic update that uses setQueryData. You may use it as an example, but the idea of manually updating infiniteQuery after mutation is not the best.
const queryClient = useQueryClient()

useMutation({
mutationFn: updateTodo,
// When mutate is called:
onMutate: async (newTodo) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await queryClient.cancelQueries({ queryKey: ['todos'] })

// Snapshot the previous value
const previousTodos = queryClient.getQueryData(['todos'])

// Optimistically update to the new value
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

// Return a context object with the snapshotted value
return { previousTodos }
},
// If the mutation fails,
// use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
const queryClient = useQueryClient()

useMutation({
mutationFn: updateTodo,
// When mutate is called:
onMutate: async (newTodo) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await queryClient.cancelQueries({ queryKey: ['todos'] })

// Snapshot the previous value
const previousTodos = queryClient.getQueryData(['todos'])

// Optimistically update to the new value
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

// Return a context object with the snapshotted value
return { previousTodos }
},
// If the mutation fails,
// use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
Keep in mind that you need to manually manage all the edge cases, like when you are on the last page, or there's only 1 page and your message is added to the 2 page. You will need to recreate all the pagination logic on FE in order to keep it in sync with your BE That's why invalidation is preferable in most of the cases.
other-emerald
other-emeraldOP•2y ago
We are already doing this and managing the edge cases, I feel like you don't understand my question. I know how to mutate the data, the question is if there is a better way to handle rerendering of components using this cache then moving the call to the mutation function up the component tree @TkDodo 🔮 could you maybe give your take?
xenophobic-harlequin
xenophobic-harlequin•2y ago
I'm sorry, I really don't understand why you use mutation to force your component to rerender. It feels like weird and unpredictable behavior. Maybe @TkDodo 🔮 will help you with this 🙂
other-emerald
other-emeraldOP•2y ago
I dont use it to force a rerender, I use it to do a mutation on the server data and then also update the cache directly. However it does not rerender other components using the same query. To work around this I usually just move the mutation up in the component tree, as it does rerender the component its called from. Well I guess I understand why we did not understand each other The issue I'm describing does not really exist It was just an immutability issue, related to the fact that an infinite query ofcourse has pages and pageParams which need to be deeply cloned 🤡 which apprently I have been doing wrong for like 1,5 years now thx for the help regardless!!
xenophobic-harlequin
xenophobic-harlequin•2y ago
It happens 🙂 np mate

Did you find this page helpful?