I have two media hide and unhide, I am using optimistic update approach from react query
It works smoothly, the setup of my react is list of checkbox, 1 list for hidden and 1 list for non-hidden items. I am having problem with race conditions when the checkbox is clicked very fast when unhiding.
This is the optimistic hook for hiding a media:
const useOptimisticHideMedia = (file_type: FileType, profileId: string) => {
const mutationKey = [REPRESENTATIVES_MUTATION.hide_attached_media_key, file_type, profileId];
return useMutation({
mutationKey,
mutationFn: async (payload: IRepresentativeHideMediaPayload) => {
return await unSelectProfileMedia(payload.profileId, payload.mediaId);
},
onMutate: async (payload: IRepresentativeHideMediaPayload) => {
let hiddenNode: IRepresentativeSingleProfileHiddenMedia;
const attachedMediaQueryKey = [
REPRESENTATIVES_QUERY_STRING.representatives_single_profile_attached_media,
payload.profileId,
payload.fileType,
];
const hiddenMediaQueryKey = [
REPRESENTATIVES_QUERY_STRING.representatives_single_profile_hidden_media,
payload.profileId,
payload.fileType,
];
await Promise.all([
queryClient.cancelQueries(attachedMediaQueryKey),
// queryClient.cancelQueries(hiddenMediaQueryKey),
]);
const previousAttachedMedia: ISingleRepresentativeAttachedMediaPayloadResponse | undefined =
queryClient.getQueryData(attachedMediaQueryKey);
const previousHiddenMedia = queryClient.getQueryData(hiddenMediaQueryKey);
queryClient.setQueryData(attachedMediaQueryKey, (oldData: any) => {
const prevData: ISingleRepresentativeAttachedMediaPayloadResponse = oldData;
return {
...prevData,
data: {
...prevData.data,
medium_attachments_connection: {
...prevData.data?.medium_attachments_connection,
nodes: prevData.data?.medium_attachments_connection?.nodes?.filter((node) => {
if (node.id !== payload.mediaId) return true;
hiddenNode = {
id: node?.medium.id || '',
file_name: node?.medium?.file_name || '',
file_type: node?.medium?.file_type || '',
attachment_url: node?.medium?.attachment_url || '',
cropped_attachment_url: node?.medium?.cropped_attachment_url,
modified_attachment_url: node?.medium?.modified_attachment_url,
modified_metadata: node?.medium?.modified_metadata || '',
};
return false;
}),
},
},
};
});
queryClient.setQueryData(hiddenMediaQueryKey, (oldData: any) => {
const prevData: ISingleRepresentativeHiddenMediaPayloadResponse = oldData;
return {
...prevData,
data: {
...prevData.data,
media_connection: {
...prevData.data.media_connection,
nodes: [hiddenNode, ...(prevData.data.media_connection?.nodes || [])],
},
},
};
});
return { previousAttachedMedia, previousHiddenMedia };
},
onError: (err, payload, context) => {
// Rollback optimistic update on error
const attachedMediaQueryKey = [
REPRESENTATIVES_QUERY_STRING.representatives_single_profile_attached_media,
payload.profileId,
payload.fileType,
];
const hiddenMediaQueryKey = [
REPRESENTATIVES_QUERY_STRING.representatives_single_profile_hidden_media,
payload.profileId,
payload.fileType,
];
queryClient.setQueryData(attachedMediaQueryKey, context?.previousAttachedMedia);
queryClient.setQueryData(hiddenMediaQueryKey, context?.previousHiddenMedia);
},
onSettled: async (data, error, payload) => {
if (queryClient.isMutating({ mutationKey }) === 1) {
queryClient.invalidateQueries([
REPRESENTATIVES_QUERY_STRING.representatives_single_profile_attached_media,
payload.profileId,
payload.fileType,
]);
queryClient.invalidateQueries([
REPRESENTATIVES_QUERY_STRING.representatives_single_profile_hidden_media,
payload.profileId,
payload.fileType,
]);
}
},
});
};
This is the Unhide:
const useOptimisticUnHideMedia = (file_type: FileType, profileId: string) => {
const mutationKey = [REPRESENTATIVES_MUTATION.select_media_key, file_type, profileId];
return useMutation({
mutationKey,
mutationFn: async (payload: IRrepresentativeShowHiddenMediaPayload[]) => {
return await postRepresentativeShowHiddenMedia(profileId, payload);
},
onMutate: async (payload: IRrepresentativeShowHiddenMediaPayload[]) => {
let hiddenNode: IRepresentativeSingleProfileMedia;
const attachedMediaQueryKey = [
REPRESENTATIVES_QUERY_STRING.representatives_single_profile_attached_media,
profileId,
file_type,
];
const hiddenMediaQueryKey = [
REPRESENTATIVES_QUERY_STRING.representatives_single_profile_hidden_media,
profileId,
file_type,
];
await Promise.all([
// queryClient.cancelQueries(attachedMediaQueryKey),
queryClient.cancelQueries(hiddenMediaQueryKey),
]);
const previousHiddenMedia: ISingleRepresentativeHiddenMediaPayloadResponse | undefined =
queryClient.getQueryData(hiddenMediaQueryKey);
const previousAttachedMedia = queryClient.getQueryData(attachedMediaQueryKey);
queryClient.setQueryData(hiddenMediaQueryKey, (oldData: any) => {
const prevData: ISingleRepresentativeHiddenMediaPayloadResponse = oldData;
return {
...prevData,
data: {
...prevData.data,
media_connection: {
...prevData?.data.media_connection,
nodes: prevData?.data?.media_connection?.nodes.filter((node) => {
return !payload.some((item) => node.id === item.id);
}),
},
},
};
});
return { previousAttachedMedia, previousHiddenMedia };
},
onError: (err, payload, context) => {
// Rollback optimistic update on error
const attachedMediaQueryKey = [
REPRESENTATIVES_QUERY_STRING.representatives_single_profile_attached_media,
profileId,
file_type,
];
const hiddenMediaQueryKey = [
REPRESENTATIVES_QUERY_STRING.representatives_single_profile_hidden_media,
profileId,
file_type,
];
queryClient.setQueryData(attachedMediaQueryKey, context?.previousAttachedMedia);
queryClient.setQueryData(hiddenMediaQueryKey, context?.previousHiddenMedia);
},
onSettled: async (data, error, payload) => {
if (queryClient.isMutating({ mutationKey }) === 1) {
queryClient.invalidateQueries([
REPRESENTATIVES_QUERY_STRING.representatives_single_profile_attached_media,
profileId,
file_type,
]);
queryClient.invalidateQueries([
REPRESENTATIVES_QUERY_STRING.representatives_single_profile_hidden_media,
profileId,
file_type,
]);
}
},
});
};
I was expecting it to refetch the fresh data from the server. But when I unhide media quickly (e.g. rapidly toggling checkboxes) using the useOptimisticUnHideMedia, it seems like there's a race condition: the invalidation doesn't always trigger a refetch or get the correct updated state. Only when I refresh the page, I get the correct data.
queryClient.invalidateQueries([
REPRESENTATIVES_QUERY_STRING.representatives_single_profile_attached_media,
profileId,
file_type,
]);
What can I improve here? Really appreciate the help thanks.