DEV Community

Cover image for Tanstack Query / React Query 🌸
Abdul Ahad Abeer
Abdul Ahad Abeer

Posted on • Originally published at abeer.hashnode.dev

Tanstack Query / React Query 🌸

Intro

This article is more like a note or cheatsheet. I hope this approach is not going to be confusing for you if you are already familiar with redux or such libraries before.

Most of it is actually noted from the Code Genix’s Tutorial.

export function useTodosIds() {
  return useQuery({
    queryKey: ["todos"],
    queryFn: getTodosIds,
  })
}
Enter fullscreen mode Exit fullscreen mode

The useQuery part is the most basic of tanstack-query.

Here. queryFn is the query function. Here getTodosIds is the function that mainly makes the API call. Here is its definition:

import axios from "axios"
import { Todo } from "../types/todo"

const BASE_URL = "http://localhost:8080"
const axiosInstance = axios.create({
  baseURL: BASE_URL,
})

export const getTodosIds = async () => {
  return (await axiosInstance.get<Todo[]>("todos")).data.map((todo) => todo.id)
}
Enter fullscreen mode Exit fullscreen mode

There are other options in the useQuery object as well. Some of them are refetchOnWindowFocus and enabled:

export function useTodosIds(something: boolean) {
  return useQuery({
    queryKey: ["todos"],
    queryFn: getTodosIds,
    refetchOnWindowFocus: false,
    enabled: something, // place a boolean value.
  })
}
Enter fullscreen mode Exit fullscreen mode

if refetchOnWindowFocus is true, it will make an api call whenever you get back to the app window.

if enabled is true, this query is going to work. Otherwise, it won’t.

By the way, the default value of refetchOnWindowFocus and enabled are always true.

Setup

Wrap your main ui or the App.tsx file with queryClientProvider and add the query client to it.

In the query-client object, there are many options. I incorporated 2 of them here:

import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import "./index.css"
import App from "./App.tsx"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 5,
      retryDelay: 1000,
    },
  },
})

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </StrictMode>
)
Enter fullscreen mode Exit fullscreen mode

Here, you can add Devtools as well (Tanstack-query specific)

import { ReactQueryDevtools } from "@tanstack/react-query-devtools"

<QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
Enter fullscreen mode Exit fullscreen mode

The notable thing here is that initialIsOpen's value of true means the dev tools will be open by default, while its value of false means they will not be open by default.

Rendering The Query Content

You can write useQuery right into the component where the query value is used. But separating them into a different module could be the better approach. With that, we get different states of the query in status form like .isPending and .isError

import { FC } from "react"
import { useTodosIds } from "../services/queries"

interface ComponentProps {}

const Todo: FC<ComponentProps> = () => {
  const todosIdsQuery = useTodosIds()

  if (todosIdsQuery.isPending) return <span>loading...</span>

  if (todosIdsQuery.isError) return <span>there is an error!</span>

  return (
    <>
      {todosIdsQuery.data.map((id) => (
        <p key={id}>{id}</p>
      ))}
    </>
  )
}

export default Todo
Enter fullscreen mode Exit fullscreen mode

We can look at those query state things more closely.

isLoading vs isPending vs isFetching, fetchStatus, status

const {data, isLoading, isPending, isFetching, isError, error, status} = useQuery({
    ...
    ......
})
Enter fullscreen mode Exit fullscreen mode

status

status has 3 possible values →

pending = when there's no cached data and no query attempt was finished yet.

error = when the query attempt resulted in an error. error property has the error received from the attempted fetch

success = when the query has received a response with no errors and is ready to display its data.

fetchStatus

fetchStatus is another thing that tells you the current status of data fetching.

const todosIdsQuery = useTodosIds()

return (
    <>
      <p>Query function status: {todosIdsQuery.fetchStatus}</p>
      {todosIdsQuery.data.map((id) => (
        <p key={id}>{id}</p>
      ))}
    </>
  )
Enter fullscreen mode Exit fullscreen mode

Normally, it returns idle as value when nothing is happening. But when the fetching is going on, it returns fetching.

isLoading vs isPending vs isFetching

isLoading: It gets true during the first fetch when no cached data exists.

isPending: It is not typically used for queries in TanStack Query (React Query). It is primarily used for  mutations  to indicate that a mutation is in progress (e.g., a POST or PUT request is being sent).

isFetching: Indicates active data fetching. This flag is true when queryFn is being executed, either for the first time or during background re-fetching.

Parallel queries with useQueries

With useQueries we can make multiple queries simultaneously and get the result in an array.

Example of useQueries Fetching Multiple APIs at the Same Time:

const results = useQueries({
  queries: [
    { queryKey: ["todos"], queryFn: getTodos },
    { queryKey: ["users"], queryFn: getUsers },
    { queryKey: ["posts"], queryFn: getPosts },
  ],
})

// Each query runs in parallel
const todos = results[0].data
const users = results[1].data
const posts = results[2].data
Enter fullscreen mode Exit fullscreen mode

Another way of doing it could be the following:

export function useTodos(ids: (number | undefined)[] | undefined) {
  return useQueries({
    queries: (ids ?? []).map((id) => {
      return {
        queryKey: ["todo", id],
        queryFn: () => getTodo(id!),
      }
    }),
  })
}
Enter fullscreen mode Exit fullscreen mode

This example serves a different purpose here.

useMutation() used for posting, updating and deleting

useQuery and useQueries Both are used to fetch data from the server. But useMutation() is used to make any change in the data saved in the server.

// ./api.ts
export const createTodo = async (data: Todo) => {
  await axiosInstance.post("todos", data)
}

// ./mutations.ts
export function useCreateTodo() {
  return useMutation({
    mutationFn: (data: Todo) => createTodo(data),
  })
}
Enter fullscreen mode Exit fullscreen mode

But this is not what Tanstack-query was built for.

You can intercept the process of making the mutation request to the server by different other life-cycle events/callbacks like onMutate, onError, and so on:

export function useCreateTodo() {
  return useMutation({
    mutationFn: (data: Todo) => createTodo(data),
    onMutate: () => {
      console.log("mutate")
    },
    onError: () => {
      console.log("error")
    },
    onSuccess: () => {
      console.log("success")
    },
    onSettled: () => {
      console.log("settled")
    },
  })
}
Enter fullscreen mode Exit fullscreen mode
  • onMutate: This callback is triggered immediately before the mutation function (mutationFn) is fired. It's often used to update the UI.

  • onError: This callback is triggered if the mutation encounters an error. You can also use this to revert optimistic updates or show error messages to the user.

  • onSuccess: This callback is triggered when the mutation gets completed successfully.

  • onSettled: This callback is triggered once the mutation is either successfully completed or encounters an error (like how finally in try-catch block works).

onSettled

onSettled has some useful parameters.

onSettled: (data, error, variables) => {
      console.log("mutate")
    },
Enter fullscreen mode Exit fullscreen mode

Here, data is the returned data and variables is the parameter you pass through mutationFn function.

Invalidating Queries

onSettled can be very useful. Within this callback, we can invalidate data.

export function useCreateTodo() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (data: Todo) => createTodo(data),
    ...
    onSettled: async (_, error) => {
      console.log("settled")
      if (error) {
        console.log(error)
      } else {
        await queryClient.invalidateQueries({ queryKey: ["todos"] })
      }
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

So, here it refetches the query with query key ‘todos’ with the latest included data.

The following is how you would use the mutation in a React component:

const createTodoMutation = useCreateTodo()

const handleCreateTodoSubmit: SubmitHandler<TodoType> = (data) => {
   createTodoMutation.mutate(data)
}
Enter fullscreen mode Exit fullscreen mode

Update Data with useMutation

// api.ts
export const updateTodo = async (data: Todo) => {
  await axiosInstance.put(`todos/${data.id}`, data)
}

// mutations.ts
export function useUpdateTodo() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (data: Todo) => updateTodo(data),
    onSettled: async (_, error, variables) => {
      if (error) {
        console.log(error)
      } else {
        await queryClient.invalidateQueries({ queryKey: ["todos"] })
        await queryClient.invalidateQueries({
          queryKey: ["todo", { id: variables.id }],
        })
      }
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

Here, we are invalidating the whole to-do list and the single to-do we updated.

The earlier example shows how to use this mutation in a React component.

Delete Data with useMutation

export const deleteTodo = async (id: number) => {
  await axiosInstance.delete(`todos/${id}`)
}

export function useDeleteTodo() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (id: number) => deleteTodo(id),

    onSuccess: async (_, error) => {
      if (error) {
        console.log(error)
      } else {
        await queryClient.invalidateQueries({ queryKey: ["todos"] })
      }
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

If you want to do something after the mutation, use mutateAsync instead of mutate like this:

  const handleDeleteTodo = async (id: number) => {
    await deleteTodoMutation.mutateAsync(id)
  }
Enter fullscreen mode Exit fullscreen mode

placeholderData: keepPreviousData

export const getProjects = async (page = 1) => {
  ;(await axiosInstance.get<Project[]>(`projects?_page=${page}&limit=3`)).data
}

export function useProjects(page: number) {
  return useQuery({
    queryKey: ["projects", { page }],
    queryFn: () => getProjects(page),
    placeholderData: keepPreviousData,
  })
}
Enter fullscreen mode Exit fullscreen mode

placeholderData: keepPreviousData

This keeps the previous data in the matter of pagination. It keeps the previous page data even when the next page data has not been fetched yet.

Pagination

Define mutation:

export const getProjects = async (page = 1) => {
  return (await axiosInstance.get<Project[]>(`projects?_page=${page}&_limit=3`))
    .data
}

export function useProjects(page: number) {
  return useQuery({
    queryKey: ["projects", { page }],
    queryFn: () => getProjects(page),
    placeholderData: keepPreviousData,
  })
}
Enter fullscreen mode Exit fullscreen mode

Now, use this mutation in the React component for pagination:

import { useState } from "react"
import { useProjects } from "../services/mutations"

export default function Projects() {
  const [page, setPage] = useState(1)

  const { data, isPending, error, isError, isPlaceholderData, isFetching } =
    useProjects(page)

  return (
    <div>
      {isPending ? (
        <div>loading...</div>
      ) : isError ? (
        <div>Error: {error.message}</div>
      ) : (
        <div>
          {data.map((project) => (
            <p key={project.id}>{project.name}</p>
          ))}
        </div>
      )}
      <span>Current page: {page}</span>
      <button onClick={() => setPage((old) => Math.max(old - 1, 0))}>
        Previous Page
      </button>{" "}
      <button
        onClick={() => {
          if (!isPlaceholderData) {
            setPage((old) => old + 1)
          }
        }}
        disabled={isPlaceholderData}
      >
        Next Page
      </button>
      {isFetching ? <span>Loading...</span> : null}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)