Using tRPC and React-query for optimistic updates is resulting in frequent and distracting flashing updates

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,
};
};

Answer №1

Presented below is the effective 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, // maintains fresh State for example: 1000ms(or Infinity) then switches to Stale State
    onError: (error) => {
      console.log("list category error: ", error);
    },
  });

  // Mutations

  const update = api.category.update.useMutation({
    onMutate: async (variables) => {
      // Cancel any ongoing refetches (to prevent them from overwriting 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;
        }
      });

      // handling the pass of function or value to the onError third argument:
      return () => utils.category.list.setData(undefined, previousQueryData);
    },
    onError: (error, variables, rollback) => {
      //   In case of an error, proceed with rollback
      if (rollback) {
        rollback();
        console.log("rollback");
      }
    },
    onSettled: async (data, variables, context) => {
      await utils.category.list.invalidate();
    },
  });

  return {
    list,
    update,
  };
};

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What causes my Redux component to re-render when the state it's not related to changes?

My redux state structure is as follows: { entities: { cars: { byId: {}, isFetching: true }, persons: { byId: {}, isFetching: false } } } Exploring the implementation of my Person container: class PersonPag ...

TSDX incorporates Rollup under the hood to bundle CSS Modules, even though they are not referenced

I have recently developed a React library with TSDX and you can find it here: https://github.com/deadcoder0904/react-typical This library utilizes CSS Modules and applies styles to the React components. Although the bundle successfully generates a CSS fi ...

Can we categorize various types by examining the characteristics of an object?

Is it feasible with TypeScript to deduce the result below from the given data: const data = { field1: {values: ['a', 'b', 'c']}, field2: {values: ['c', 'd', 'e'], multiple: true} } const fiel ...

The module './$types' or its related type declarations could not be located in server.ts

Issue with locating RequestHandler in +server.ts file despite various troubleshooting attempts (recreating file, restarting servers, running svelte-check) +server.ts development code: import type { RequestHandler } from './$types' /** @type {imp ...

The NextJS image URL remains constant across various endpoints

Currently experiencing an issue with my Channel Tab where the icon and name of the channel are displayed. The problem is that every time I switch channels, the icon remains the same as the first channel I clicked on. PREVIEW This is the code I am current ...

What functionality does the --use-npm flag serve in the create-next-app command?

When starting a new NextJS project using the CLI, there's an option to use --use-npm when running the command npx create-next-app. If you run the command without any arguments (in interactive mode), this choice isn't provided. In the documentati ...

TypeScript: Extending a Generic Type with a Class

Although it may seem generic, I am eager to create a class that inherits all props and prototypes from a generic type in this way: class CustomExtend<T> extends T { constructor(data: T) { // finding a workaround to distinguish these two ...

Having trouble with importing rxjs operators

After updating the imports for rxjs operators in my project to follow the new recommended syntax, I encountered an issue with the "do" operator. While switchMap and debounceTime were updated successfully like this: import { switchMap, debounceTime } ...

Utilizing variables for Protractor command line parameters

I am struggling to make variables work when passing parameters as a string in my code. Conf.ts params: { testEnvironment: TestEnvironment.Prod, }, env.ts export enum TestEnvironment { Dev = 'dev', QA = 'qa', Prod ...

Angular 8: Implementing functionality for the Parent Component to detect when the User clicks outside of the Child Component Textbox

I am working on a scenario where I have a Parent Component and a Child Component containing a Formbuilder and a ZipCode textbox. Is there a way to notify the Parent Component when the user clicks out of the Child Component Textbox? I need to trigger some ...

What is the best way to assign or convert an object of one type to another specific type?

So here's the scenario: I have an object of type 'any' and I want to assign it an object of type 'myResponse' as shown below. public obj: any; public set Result() { obj = myResponse; } Now, in another function ...

What is the correct way to convert a non-observable into an observable?

Can I convert a non-observable into an observable to receive direct image updates without having to refresh the page, but encountering this error: Type 'EntityImage[]' is missing the following properties from type 'Observable<EntityImage ...

Issue with setting context.cookies in Deno oak v10.5.1 not resolved

When I try to set cookies in oak, the cookies property of the context doesn't seem to update and always returns undefined. This happens even when following the example provided in their documentation. app.use(async ctx => { try { const ...

Guide to executing Jest tests with code coverage for a single file

Instead of seeing coverage reports for all files in the application, I only want to focus on one file that I am currently working on. The overwhelming nature of sifting through a full table of coverage for every file makes it difficult to pinpoint the spe ...

Warning: TypeScript linter alert - the no-unused-variable rule is now outdated; however, I do not have this configuration enabled

After 3 long months, I came across a warning in a project that was being refreshed today. The warning says "no-unused-variable is deprecated. Since TypeScript 2.9. Please use the built-in compiler checks instead." Oddly enough, my tsconfig.json file do ...

Is it possible to automatically correct all import statements in a TypeScript project?

After transferring some class member variables to a separate class in another file, I realized that these variables were extensively used in the project. As a result, approximately 1000 .ts files will need their imports modified to point to the new class/f ...

Looking to troubleshoot an error in my TypeScript reducer implementation. How can I fix this issue?

I'm currently using typescript along with react, redux, and typessafe-actions. However, I've come across a particular error that I need help with. The error message reads: "No overload matches this call. Overload 1 of 2, '(...items: Concat ...

Invoke the REST API when a checkbox is selected

I have a React component that I need help with: import * as React from 'react'; import './Pless.css'; interface Props { handleClose: () => void; showPless: boolean; } export class Pless extends React.Component<Props> ...

The "if(x in obj)" statement in Typescript does not properly narrow down my custom Record

I am struggling with a code snippet where I am trying to check if a string exists in my custom record using the if(x in obj) guard statement, but it seems to not be working as expected. Below is the sample code snippet that is throwing an error: type Ans ...

Updates to Angular form control values are not triggering notifications

I am currently working with a basic form control that subscribes to the valueChanges observable. @Component({ selector: 'my-app', template: ` <input [formControl]="control" /> <div>{{ name$ | async | json }}</div ...