3

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.

antonwilhelm
  • 5,768
  • 4
  • 19
  • 45
  • in ReactQuery I think you can use onError to rollback, like given in them [example](https://tanstack.com/query/v4/docs/guides/mutations#persist-mutations), I have not worked with TRPC – Azzy Dec 04 '22 at 09:56
  • 1
    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 properly – antonwilhelm Dec 04 '22 at 13:36

2 Answers2

7

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

TkDodo
  • 20,449
  • 3
  • 50
  • 65
  • 1
    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? – Jesse Winton Dec 28 '22 at 15:48
  • 2
    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: https://tkdodo.eu/blog/mastering-mutations-in-react-query#optimistic-updates – TkDodo Dec 29 '22 at 09:38
4

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