12

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

1 Answer 1

14

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
Sign up to request clarification or add additional context in comments.

3 Comments

Is it fine to call useQueryClient inside a custom hook, like this? const useCreateTodo = () => { 'const queryClient = useQueryClient();' return useMutation(...); }
yes, definitely. useQueryClient is just react-context :)
Wouldn't setting the type on axios' post be a more complete example? The mutationFn would be: (newTodo: <Pick<Todo, 'text'>) => axios.post<Todo>('/api/todos', { data: newTodo})

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.