Skip to content
devcards.space
ReactFrontend

What is React Query? Mutation Guide with Axios and TypeScript

Use React Query (TanStack Query) with an Axios baseURL, login mutation, create user and update user hooks in TypeScript.

9 minute read
React Query infographic card summarizing server state, cache, mutations and TypeScript

React Query (TanStack Query) manages server state in React applications. In real projects it usually sits on top of a single Axios API client: login, create user and update user mutations live in reusable hooks, while components focus on form state and UI.

What is React Query?

React Query caches API data, revalidates it and keeps the UI fresh after mutations. It focuses on server state: API responses, pending states, errors and cache synchronization, not local form inputs.

  • Cache and revalidation for server state
  • useQuery for reads, useMutation for writes
  • invalidateQueries after mutations
  • End-to-end TypeScript types for input and response models

Installation

We will use React Query and Axios together. React Query manages server state; Axios manages the HTTP client layer.

bash
npm install @tanstack/react-query axios

QueryClientProvider

Create a single QueryClient at the root so the whole app can share the same cache.

app/providers.tsx
tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

Axios BaseURL Setup

A single Axios instance centralizes baseURL, headers and token configuration. Hooks can use this api instance instead of scattered fetch or axios calls.

lib/api.ts
ts
import axios from "axios";

export const api = axios.create({
  baseURL: "https://api.zaferayan.com",
  headers: {
    Accept: "application/json",
    "Content-Type": "application/json",
  },
});

export function setAuthToken(token?: string) {
  if (token) {
    api.defaults.headers.common.Authorization = "Bearer " + token;
    return;
  }

  delete api.defaults.headers.common.Authorization;
}

Shared TypeScript Types

Define input and response types up front to keep mutation hooks readable. Login returns an auth response; create and update operations work with the User model.

features/users/types.ts
ts
export type User = {
  id: string;
  name: string;
  email: string;
  role: "admin" | "editor" | "viewer";
};

export type LoginInput = {
  email: string;
  password: string;
};

export type LoginResponse = {
  accessToken: string;
  user: User;
};

export type CreateUserInput = {
  name: string;
  email: string;
  role: User["role"];
};

export type UpdateUserInput = Partial<CreateUserInput>;

Login Mutation

Login is a write operation, so model it with useMutation. On success you can attach the token to the Axios instance and store it for later requests.

features/auth/useLogin.ts
tsx
import { useMutation } from "@tanstack/react-query";
import { api, setAuthToken } from "@/lib/api";
import type { LoginInput, LoginResponse } from "@/features/users/types";

export const useLogin = () => {
  return useMutation({
    mutationFn: (input: LoginInput) =>
      api.post<LoginResponse>("/auth/login", input).then(({ data }) => data),
    onSuccess: (data) => {
      setAuthToken(data.accessToken);
      localStorage.setItem("accessToken", data.accessToken);
    },
  });
};

mutateAsync in a Login Form

Use mutateAsync with .then/.catch when the submit flow needs to wait for a result. It keeps success redirects, toast messages and error handling in one promise chain.

components/LoginForm.tsx
tsx
"use client";

import { useRouter } from "next/navigation";
import { useLogin } from "@/features/auth/useLogin";

export function LoginForm() {
  const router = useRouter();
  const login = useLogin();

  function onSubmit(input: { email: string; password: string }) {
    login
      .mutateAsync(input)
      .then((session) => {
        router.push("/dashboard");
        console.log("Welcome", session.user.name);
      })
      .catch(() => {
        console.log("Invalid email or password");
      });
  }

  return (
    <button
      disabled={login.isPending}
      onClick={() => onSubmit({ email: "ada@dev.com", password: "secret" })}
    >
      {login.isPending ? "Signing in..." : "Sign in"}
    </button>
  );
}

User Query Keys

Create and update mutations need to know which cache entries should be refreshed. Generate query keys from one place.

features/users/queryKeys.ts
ts
export const userKeys = {
  all: ["users"] as const,
  lists: () => [...userKeys.all, "list"] as const,
  detail: (id: string) => [...userKeys.all, "detail", id] as const,
};

Create User Mutation

After creating a user, invalidate the list cache. React Query will refetch fresh data without manual list management.

features/users/useCreateUser.ts
tsx
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { api } from "@/lib/api";
import { userKeys } from "./queryKeys";
import type { CreateUserInput, User } from "./types";

export const useCreateUser = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (input: CreateUserInput) =>
      api.post<User>("/users", input).then(({ data }) => data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: userKeys.lists() });
    },
  });
};

Update User Mutation

The update mutation receives an id and an input payload. On success, invalidate both the detail cache and the list cache so stale data does not remain on screen.

features/users/useUpdateUser.ts
tsx
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { api } from "@/lib/api";
import { userKeys } from "./queryKeys";
import type { UpdateUserInput, User } from "./types";

type UpdateUserVariables = {
  id: string;
  input: UpdateUserInput;
};

export const useUpdateUser = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, input }: UpdateUserVariables) =>
      api.patch<User>("/users/" + id, input).then(({ data }) => data),
    onSuccess: (user) => {
      queryClient.invalidateQueries({ queryKey: userKeys.detail(user.id) });
      queryClient.invalidateQueries({ queryKey: userKeys.lists() });
    },
  });
};

Using Them in Forms

For simple button actions, mutate is enough. For submit flows that need a result, redirects or toast sequencing, use mutateAsync with .then/.catch.

components/UserActions.tsx
tsx
const createUser = useCreateUser();
const updateUser = useUpdateUser();

createUser.mutate({
  name: "Ada Lovelace",
  email: "ada@dev.com",
  role: "editor",
});

updateUser
  .mutateAsync({
    id: "user_123",
    input: { role: "admin" },
  })
  .then((user) => {
    console.log("Updated", user.name);
  });

v5 Status Flags

In TanStack Query v5, use isPending for the initial waiting state. isFetching means an active fetch is happening, isError means the operation failed, and mutation.isPending means a write is still running.

  • query.isPending — data is not ready yet
  • query.isFetching — initial load or background refetch is active
  • mutation.isPending — POST/PATCH/DELETE is running
  • mutation.isError — mutation finished with an error

Summary

React Query becomes much easier to read when the HTTP layer lives in an Axios instance and server-state work lives in small custom hooks. Login, create user and update user mutations stay typed, reusable and easy to call from forms.

Frequently Asked Questions

Common questions about this topic.

Should login use useQuery or useMutation?

useMutation. Login creates a new session/token on the server, so it is a write operation rather than a cacheable read query.

Should I use mutate or mutateAsync?

Use mutate for simple button clicks. Use mutateAsync when a submit flow needs a result, .then/.catch handling, redirects or toast sequencing.

Where should I store the token?

The example uses localStorage for simplicity. More secure apps often use httpOnly cookies; with that setup, configure Axios with withCredentials.

Why invalidateQueries after create/update?

A mutation changes server data. invalidateQueries marks the related cache as stale and lets React Query fetch fresh data.

Is TanStack Query the same as React Query?

Yes. React Query moved into the TanStack Query family. The React package is @tanstack/react-query.

Other infographics on connected topics.

Discover more developer infographics

Visit the homepage so you don't miss new content.

See all infographics