As I implement optimistic updates using the tRPC useMutation React-Query hook, everything seems to be working fine. However, there is a peculiar issue occurring after updating the data - the response quickly changes to the new list but then switches back to the old value momentarily before settling on the new value again. I'm puzzled about what mistake I might have made. Below is the code snippet in question.
import { api } from "@/utils/api";
import type { Category } from "@prisma/client";
export const useCategoryActions = () => {
const utils = api.useContext();
//Queries
const list = api.category.list.useQuery();
// Mutations
const update = api.category.update.useMutation({
onMutate: (variables) => {
// Cancel any outgoing refetches (so they don't overwrite(race condition) our optimistic update)
void utils.category.list.cancel();
const previousQueryData = utils.category.list.getData();
const newCategory: Category = {
id: crypto.randomUUID(),
name: variables.name,
slug: variables.name,
createdAt: new Date(),
updatedAt: new Date(),
};
utils.category.list.setData(undefined, (oldQueryData) => {
if (oldQueryData) {
const filteredData =
oldQueryData.filter((item) => item.slug !== variables.slug) ?? [];
const elementIndex =
oldQueryData.findIndex((item) => item.slug === variables.slug) ??
-1;
filteredData.splice(elementIndex, 0, newCategory);
return filteredData;
}
});
// return will pass the function or the value to the onError third argument:
return () => utils.category.list.setData(undefined, previousQueryData);
},
onError: (error, variables, rollback) => {
// If there is an errror, then we will rollback
if (rollback) {
rollback();
console.log("rollback");
}
},
onSettled: async (data, variables, context) => {
await utils.category.list.invalidate();
},
});
return {
list,
update,
};
};
There is a potential solution available that has been tried out, however, it doesn't seem to resolve the issue effectively, possibly due to an incorrect implementation.
import { useRef } from "react";
import { api } from "@/utils/api";
import type { Category } from "@prisma/client";
export const useCategoryActions = () => {
const utils = api.useContext();
const mutationCounterRef = useRef(0);
//Queries
const list = api.category.list.useQuery();
// Mutations
const update = api.category.update.useMutation({
onMutate: (variables) => {
//Cancel any outgoing refetches (so they don't overwrite(race condition) our optimistic update)
void utils.category.list.cancel();
const previousQueryData = utils.category.list.getData();
const newCategory: Category = {
id: crypto.randomUUID(),
name: variables.name,
slug: variables.name,
createdAt: new Date(),
updatedAt: new Date(),
};
utils.category.list.setData(undefined, (oldQueryData) => {
if (oldQueryData) {
const filteredData =
oldQueryData.filter((item) => item.slug !== variables.slug) ?? [];
const elementIndex =
oldQueryData.findIndex((item) => item.slug === variables.slug) ??
-1;
filteredData.splice(elementIndex, 0, newCategory);
return filteredData;
}
});
//Increment the mutation counter
mutationCounterRef.current++;
//return will pass the function or the value to the onError third argument:
return async () => {
//Decrement the mutation counter
mutationCounterRef.current--;
//Only invalidate queries if there are no ongoing mutations
if (mutationCounterRef.current === 0) {
utils.category.list.setData(undefined, previousQueryData);
await utils.category.list.invalidate();
}
};
},
onError: async (error, variables, rollback) => {
//If there is an error, then we will rollback
if (rollback) {
await rollback();
console.log("rollback");
}
},
onSettled: async (data, variables, context) => {
// Decrement the mutation counter
mutationCounterRef.current--;
// Only invalidate queries if there are no ongoing mutations
if (mutationCounterRef.current === 0) {
await utils.category.list.invalidate();
}
},
});
return {
list,
update,
};
};