I'm currently implementing optimistic updates with the tRPC useMutation React-Query hook and it appears to be functioning properly. However, I'm encountering an issue where, upon updating the data, the response updates quickly with the new list but then reverts back to the old value after a few milliseconds, before finally returning to the new value. I'm unsure what I might be doing wrong. Below is my code.
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,
};
};
One potential resolution exists, but it appears ineffective, 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 errror, 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,
};
};