Optimistic Updates with React-Query (TRPC)
Asked Answered
N

2

5

I am not sure how I would do optimistic updates with trpc? Is this "built-in" or do I have to use react-query's useQuery hook?

So far, I am trying it like so, but it's not working:

 const queryClient = useQueryClient();

    const updateWord = trpc.word.update.useMutation({
        onMutate: async newTodo => {
            // Cancel any outgoing refetches (so they don't overwrite our optimistic update)
            await queryClient.cancelQueries({ queryKey: ['text', 'getOne'] })

            // Snapshot the previous value
            const previousText = queryClient.getQueryData(['text', 'getOne'])

            // Optimistically update to the new value
            queryClient.setQueryData(['text', 'getOne'], old => old ? {...old, { title: "Hello" }} : undefined)

            // Return a context object with the snapshotted value
            return { previousText }
        },
//...

Does this look like it should make sense? It's updating the value, but not optimistically.

Nailbrush answered 3/12, 2022 at 23:45 Comment(2)
in ReactQuery I think you can use onError to rollback, like given in them example, I have not worked with TRPCBurschenschaft
thanks, but the optimistic update itself already isn't working. onError would roll back the update and set it to server state, but the update is already the problem, there is nothing to be rolled back, bc it's not updating properlyNailbrush
C
15

After 2 days of searching how to do it, I finally found out how to solve this.

We have to use api.useContext() like previous answer mentioned.

Here is the complete optimistic update example that is working.

import { useCallback, useState } from "react";
import { Button } from "../shadcn-ui/button";
import { Input } from "../shadcn-ui/input";
import { api } from "~/utils/api";
import { useToast } from "~/components/shadcn-ui/use-toast";
import { type TodoWithUser } from "./Todo.type";
import { useSession } from "next-auth/react";

export const TodoBar = () => {
  const [todo, setTodo] = useState("");
  const { data: session } = useSession();
  const utils = api.useContext();
  const addTodo = api.todo.create.useMutation({
    onMutate: async (newTodo) => {
      setTodo("");
      // Cancel any outgoing refetches
      // (so they don't overwrite our optimistic update)
      await utils.todo.findAll.cancel();

      // Snapshot the previous value
      const previousTodos = utils.todo.findAll.getData();

      // Optimistically update to the new value
      utils.todo.findAll.setData(
        undefined,
        (oldQueryData: TodoWithUser[] | undefined) =>
          [
            ...(oldQueryData ?? []),
            {
              author: {
                name: session?.user?.name,
                id: session?.user?.id,
              },
              content: newTodo.content,
              done: false,
              createdAt: new Date(),
              updatedAt: new Date(),
            },
          ] as TodoWithUser[]
      );

      // Return a context object with the snapshotted value
      return { previousTodos };
    },
    onError: (err, _newTodo, context) => {
      // Rollback to the previous value if mutation fails
      utils.todo.findAll.setData(undefined, context?.previousTodos);
    },
    onSuccess: () => {
      console.log("inside onSuccess");
    },
    onSettled: () => {
      void utils.todo.findAll.invalidate();
    },
  });

  const handleInputOnChange = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setTodo(event.target.value);
    },
    []
  );

  const handleAddTodo = useCallback(() => {
    addTodo.mutate({
      content: todo,
    });
  }, [addTodo, todo]);

  return (
    <div className="flex w-full items-center space-x-2 self-center px-6 pt-2  md:w-2/3 md:flex-grow-0 lg:w-2/3 xl:w-1/2">
      <Input
        placeholder="Enter your task!"
        value={todo}
        onChange={handleInputOnChange}
      />
      <Button type="submit" onClick={handleAddTodo}>
        Add
      </Button>
    </div>
  );
};
Cyclotron answered 23/7, 2023 at 9:35 Comment(0)
B
10

trpc v10 offers type-safe variants of most functions from the queryClient via their own useContext hook:

const utils = trpc.useContext()

then, you should be able to do:

utils.text.getOne.cancel()
utils.text.getOne.getData()
utils.text.getOne.setData()

see: https://trpc.io/docs/useContext

Berton answered 4/12, 2022 at 17:12 Comment(2)
So, one problem I'm running into; I'm trying to optimistically update, but the the data passed from the onMutate callback doesn't include all of the properties from the data I'm trying to update (auto-generated ID, relations etc), so I am having to manually create temp data for all of those properties in the mutation. Is that the expected behavior, or is there a type-safe way around that?Intuitionism
that's the crux with optimistic updates - you have to replicate the logic that usually happens on the backend on your frontend. It's one of the reasons why I try to avoid them: tkdodo.eu/blog/…Berton

© 2022 - 2024 — McMap. All rights reserved.