0

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,
  };
};
John John
  • 1,305
  • 2
  • 16
  • 37

1 Answers1

0

Here is the working solution

import { useCallback } from "react";
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(undefined, {
    select: useCallback((data: Category[]) => {
      return data;
    }, []),
    staleTime: Infinity, // stays in fresh State for ex:1000ms(or Infinity) then turns into Stale State
    onError: (error) => {
      console.log("list category error: ", error);
    },
  });

  // Mutations

  const update = api.category.update.useMutation({
    onMutate: async (variables) => {
      // Cancel any outgoing refetches (so they don't overwrite(race condition) our optimistic update)
      await 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,
  };
};
John John
  • 1,305
  • 2
  • 16
  • 37