Shared mutation status #3598
Replies: 3 comments 3 replies
-
@tiagotwistag The easy fix was to prevent further mutations until the first one is finished, but this does not address that another mutation instance for the same bookmark could be fired from another page before the first completes. I cant get to a second page with another mutation instance fast enough, so this shared mutation state has not been a huge priority for me 👀 . I'm interested in what others have done. Is there is a best way to abort a previous mutation if the user would like to apply another after an optimistic update? Screen.Recording.2022-05-11.at.00.33.30.movuse-save-query.tsximport axios from 'axios'
import {useQuery} from 'react-query'
const getSave = async ({
pk,
model,
}: {
pk: string
model: Types.Helper.LikeableModel
}): Promise<{isMySaved: boolean}> => {
return axios.get(`/api/save?pk=${pk}&model=${model}`).then((res) => res.data)
}
export const useSaveQuery = ({
pk,
model,
enabled,
}: {
pk: string
model: Types.Helper.LikeableModel
enabled: boolean
}) => {
return useQuery({
queryKey: ['save', pk],
queryFn: async () => getSave({pk, model}),
initialData: {isMySaved: false},
cacheTime: Infinity,
enabled,
})
} use-save-controller.tsximport {useVisible} from 'react-hooks-visible'
import {showNotification} from '../../ui'
import {useUserId} from '../user/use-user'
import {useSaveQuery} from './use-save-query'
import {useToggleSaveMutation} from './use-toggle-save-mutation'
export const useSaveController = ({
pk,
model,
}: {
pk: string
model: Types.Helper.LikeableModel
}) => {
const [ref, visible] = useVisible((vi: number) => vi > 0.1)
const userId = useUserId()
const saveQuery = useSaveQuery({pk, model, enabled: !!userId && visible})
const toggleSaveMutation = useToggleSaveMutation()
const cursor = (() => {
if (!userId) {
return 'not-allowed'
}
if (saveQuery.isLoading || toggleSaveMutation.isLoading) {
return 'wait'
}
return 'pointer'
})()
const onClick = () => {
let error = false
if (!userId) {
showNotification({
title: 'Warning',
color: 'orange',
message: 'please login',
})
error = true
}
if (saveQuery.isLoading || toggleSaveMutation.isLoading) {
showNotification({
title: 'Loading',
color: 'blue',
message: 'please wait',
})
error = true
}
if (error) return
toggleSaveMutation.mutate({
pk,
model,
})
}
return {
cursor,
onClick,
isSaved: saveQuery?.data?.isMySaved ?? false,
ref: ref as any,
}
} use-toggle-save-mutation.tsximport axios from 'axios'
import {useMutation, useQueryClient} from 'react-query'
const toggleSave = async ({
pk,
model,
}: {
pk: string
model: Types.Helper.LikeableModel
}): Promise<{isMySaved: boolean}> => {
return axios.put(`/api/save?pk=${pk}&model=${model}`).then((res) => res.data)
}
export const useToggleSaveMutation = () => {
const queryClient = useQueryClient()
return useMutation(toggleSave, {
retry: 2,
onMutate: async (props) => {
// cancel any outgoing refetch when the mutation starts
await queryClient.cancelQueries(['save', props.pk])
// snapshot the previous value
const prevSave = queryClient.getQueryData(['save', props.pk]) as {
isMySaved: boolean
}
// optimistically toggle the save
queryClient.setQueryData(['save', props.pk], () => {
return {isMySaved: !prevSave.isMySaved}
})
// return the rollback function
return () => {
queryClient.setQueriesData(['save', props.pk], prevSave)
}
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (err: any, props: any, rollback: any) => {
if (rollback) {
rollback()
}
},
// Always refetch after error or success
onSettled: (data: any, err: any, props) => {
queryClient.invalidateQueries(['save', props.pk])
},
})
} |
Beta Was this translation helpful? Give feedback.
-
I just want to point out that cancelling a mutation does not work like cancelling a query. Suppose the mutation is "send an email to person A" ... how would you abort that? see also: |
Beta Was this translation helpful? Give feedback.
-
@tiagotwistag I think this is a good middle ground for multiple mutation instance with optimistic updates. Using the useIsMutating hook allows you to prevent conflicting updates by disabling the buttons const SaveButton = (props: { id: string }) => {
const { id } = props;
const q = useSavedQuery(id);
const m = useSavedMuation(id);
const numMutating = useIsMutating({
mutationKey: savedKeys.savedMutation(id),
fetching: true,
exact: true
});
return (
<button
// disabled={m.isLoading}
disabled={numMutating > 0}
onClick={() => m.mutate()}
style={{ cursor: numMutating > 0 ? "wait" : "pointer" }}
>
{q.data?.saved ? "saved" : "save"}
</button>
);
};
const TwoSaves = () => {
return (
<QueryClientProvider client={client}>
<SaveButton id={"1"} />
<SaveButton id={"1"} />
</QueryClientProvider>
);
}; https://codesandbox.io/s/react-query-two-optimistic-saves-1wmhm6?file=/src/App.tsx |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Hey, I need someone help to figure out if there could be some better approaches compared to my implementation, I have a bookmark system where I intend to give optimistic updates, the way bookmarking is shown is through a card with a toggle button that when pressed toggles the current bookmark state.
Normally this wouldn't be a problem, I could just straight follow the docs and everything would go as expected, the issues for me
start when I might have this card in multiple screens that I can access while mutating it, for some more context, this is a mobile app(react-native), which has some bottom tabs, and we can bookmark an item on the first tab and go to the second tab and expect the bookmark to be reflected, otherwise the user could create some conflicts by pressing the bookmark button again. This seems to be a limitation of react-query because mutations do behave directly from queries, so they don't share state(saw some discussions about it in here).
So what I tried to implement was a mutation that had logic to cancel a pending mutation if the user would press the bookmark button again while mutating it, it also has some debouncing in it(just a detail).
Flow
On Mutate:
On success:
On error:
The optimistic update is given by calling bookmarksCache.onSetItemAsSaved which updates that specific item cache
I removed the updateLists logic since what it does is just going trough infinite queries and add/remove that item once the mutation succeeds or fails.
Overall this feels like a bit of an overkill to implement such a feature, it also has some complexity which I would need to abstract to use the same pattern in other places, and last but not least I would need to add some logic to clean the cache that's being stored on the maps(abortControllers, previousValues, requests).
Thank you for taking the time read trough this, any idea/opinion on the topic would be great 👐
Beta Was this translation helpful? Give feedback.
All reactions