React query mutation typescript

user3887366 picture user3887366 · Jan 17, 2021 · Viewed 7.8k times · Source

I'm just playing around with react-query

with typescript

I mean I do my first attempts

Is it the right way?

const useCreateTodo = () => {
  const queryClient = useQueryClient();
  return useMutation(
    (todo: TodoDto) => axios.post(`${URL}/todos`, todo).then((res) => res.data),
    {
      onMutate: async (newTodo: TodoDto) => {
        // Cancel any outgoing refetches (so they don't overwrite our optimistic update)
        await queryClient.cancelQueries("todos");

        // Snapshot the previous value
        const previousTodos = queryClient.getQueryData("todos");

        // Optimistically update to the new value
        queryClient.setQueryData<TodoDto[] | undefined>("todos", (old) =>
          old ? [...old, newTodo] : old
        );

        // Return a context object with the snapshotted value
        return { previousTodos };
      },
      // If the mutation fails, use the context returned from onMutate to roll back
      onError: (
        err,
        newTodo,
        context:
          | {
              previousTodos: unknown;
            }
          | undefined
      ) => {
        queryClient.setQueryData(
          "todos",
          context ? context.previousTodos : context
        );
      },
      // Always refetch after error or success:
      onSettled: () => {
        queryClient.invalidateQueries("todos");
      },
    }
  );
};

Answer

TkDodo picture TkDodo · Jan 17, 2021

optimistic updates are a bit tricky for type inference. There is now an example for this exact case in the docs.

From that example:

const addTodoMutation = useMutation(
    newTodo => axios.post('/api/data', { text: newTodo }),
    {
      // When mutate is called:
      onMutate: async (newTodo: string) => {
        setText('')
        // Cancel any outgoing refetches (so they don't overwrite our optimistic update)
        await queryClient.cancelQueries('todos')

        // Snapshot the previous value
        const previousTodos = queryClient.getQueryData<Todos>('todos')

        // Optimistically update to the new value
        if (previousTodos) {
          queryClient.setQueryData<Todos>('todos', {
            ...previousTodos,
            items: [
              ...previousTodos.items,
              { id: Math.random().toString(), text: newTodo },
            ],
          })
        }

        return { previousTodos }
      },
      // If the mutation fails, use the context returned from onMutate to roll back
      onError: (err, variables, context) => {
        if (context?.previousTodos) {
          queryClient.setQueryData<Todos>('todos', context.previousTodos)
        }
      },
      // Always refetch after error or success:
      onSettled: () => {
        queryClient.invalidateQueries('todos')
      },
    }
  )

A couple of explanations:

  • Basically, you want to set the type definition only on onMutate, so that type inference will work for the mutateFn (newTodo is inferred) as well as for the context in onError.
  • add generics for getQueryData so that previousTodos is typed. You don't need to union with undefined though - react-query will do that for you.
  • the functional updater for setQueryData is tricky, because it requires you to return an Array, but old can be undefined. I prefer to use the previousTodos returned by getQueryData