React Query – The Basics of TanStack’s Latest Data Fetching Library

If you’re a React developer, you’ve probably faced the bottleneck of frequently fetching data and displaying it in your app. Even with the help of commonly used “client state libraries” in React, you’d have to write loads of boilerplate code, which isn’t always simple to implement correctly.

That’s why the TanStack community developed React Query – an easy workaround to data fetching, cache, and update server state in your React app with many out-of-the-box features.

Read below and find out how to use React Query and how it makes your life as a React developer much easier, especially regarding state management including data fetching.

Why is State Management in React Apps Necessary

State management has been a hot discussion topic ever since React was launched. That’s because the process was never easy since there are many ways to manage each type of state in React apps.

Developers rely on commonly used “client state libraries” in React like Redux, Context API, MobX, or Jotai. However, if the client suddenly wants features such as refetching, caching, deduping, and more, these libraries don’t provide a solution for server state, which I think is a good practice of “separation of concerns”.

This is where React Query comes in as a great solution. You get server state and client state living in different stores, making it easier to maintain code once you get used to this type of workflow.

How to Get Started with React Query

  1. Use npm or yarn to install the library into your project (npm i @tanstack/react-query or yarn @tanstack/react-query).
  2. Add the QueryClientProvider at the top of your application (check the image below):
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  );
}

Now, you can fully use React Query:

import { useQuery } from '@tanstack/react-query';

function Example() {
  const { isLoading, error, data, isFetching } = useQuery("repoData", () =>
    axios.get(
      "https://api.github.com/repos/tannerlinsley/react-query"
    ).then((res) => res.data)
  );

  if (isLoading) return "Loading...";

  if (error) return "An error has occurred: " + error.message;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
      <strong>👀 {data.subscribers_count}</strong>{" "}
      <strong>✨ {data.stargazers_count}</strong>{" "}
      <strong>🍴 {data.forks_count}</strong>
      <div>{isFetching ? "Updating..." : ""}</div>
    </div>
  );
}

Async Data Fetching With the “UseQuery” Hook

This is the most important hook of the library and the one you will use the most. In React Query, a query is a declarative dependency on an asynchronous data source that has a unique key.

Check the example below where the unique key is ‘todos’ and the asynchronous data source is the ‘fetchTodoList’ function.

import { useQuery } from '@tanstack/react-query'

function App() {
  const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}

Here’s what RQ does at this moment: it creates a new entry in its store, with the key ‘todos’. This is how it keeps all the relevant info regarding the query, like loading state, errors, data, and others.

Define Query Keys

Here are some examples of how you can define queryKeys:

import { useQuery } from 'react-query';

// An individual todo
useQuery({ queryKey: ['todo', 5] });

// An individual todo in a "preview" format
useQuery({ queryKey: ['todo', { preview: true }] });

// A list of todos that are done
useQuery({ queryKey: ['todo', { type: 'done' }] });

I recommend you manage every queryKey as an array; this can be like an array of dependencies like in useEffect’. Anytime something in the array changes, the data will refetch with the new input.

Here is a clearer example:

function Todos({ todoId }) {
  const result = useQuery({
    queryKey: ['todos', todoId],
    queryFn: () => fetchTodoById(todoId),
  })
}

Define Query Functions

You can define the query functions in many ways, but I’ll mention below the most common ones.

useQuery({ queryKey: ['todos'], queryFn: fetchAllTodos })
useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) })
useQuery({
  queryKey: ['todos', todoId],
  queryFn: async () => {
    const data = await fetchTodoById(todoId)
    return data
  },
})
useQuery({
  queryKey: ['todos', todoId],
  queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]),
})

How To Change Data Formats

Sometimes the data that the server gives you isn’t in the desired format, so you should apply some data manipulation. Using the ‘select’ option, include a function that modifies the output.

Take this example of how to transform the name of the todos to uppercase:

const transformTodoNames = (data: Todos) =>
  data.map((todo) => todo.name.toUpperCase())

export const useTodosQuery = () =>
  useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    // ✅ uses a stable function reference
    select: transformTodoNames,
  })

export const useTodosQuery = () =>
  useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    // ✅ memoizes with useCallback
    select: React.useCallback(
      (data: Todos) => data.map((todo) => todo.name.toUpperCase()),
      []
    ),
  })

The key detail here is to ensure that a memoized transformation function passes. This way, whenever a rerender happens, it’ll not go through the select again as long as the data hasn’t changed.

Make Code Easier to Write and Read with Type Inference

React-Query has good Typescript support, and its type inference is great, helping you to avoid confusion with your code.

In the following example, you can see that the ‘fetchGroups’ function will return a Promise that contains an array of Group elements. 

By passing ‘fetchGroups’ as the query function, the type of the data will be inferred from that function, but resolved. 

If you use the select property, the data type will know to infer the type based on what the select function does (e.g., it selects the length of the groups, so the new type should be a number).

You basically define the request and response, so the function will return the desired data.

function fetchGroups(): Promise<Group[]> {
  return axios.get('groups').then((response) => response.data)
}

// ✅ data will be `Group[] | undefined` here
function useGroups() {
  return useQuery({ queryKey: ['groups'], queryFn: fetchGroups })
}

// ✅ data will be `number | undefined` here
function useGroupCount() {
  return useQuery({
    queryKey: ['groups'],
    queryFn: fetchGroups,
    select: (groups) => groups.length,
  })
}

Query Keys Factory  – The Solution to Organize and Manage Query Keys

As you might have seen, the query keys are arrays of anything (strings, numbers, objects). As the application grows larger and larger, it becomes harder to remember every key. A common use case is that you’ll need to invalidate a query based on the key, but you first have to remember or search for that specific key. 

The solution to this problem is to create a “key factory” per feature, as stated in this example:

const todoKeys = {
  all: ['todos'] as const,
  lists: () => [...todoKeys.all, 'list'] as const,
  list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
  details: () => [...todoKeys.all, 'detail'] as const,
  detail: (id: number) => [...todoKeys.details(), id] as const,
}

Now, it becomes much easier to target the key that you need.

How to Disable a Query

Another common use case is that you might need to “stop” the query until a certain condition is met (e.g., when an input is not empty or when another query finished loading). For this, you can use the ‘enabled’ option where you can pass any boolean value. 

As the ‘enabled’ option is set to false by default, the query will proceed and call the query function when it switches to true.

function Todos() {
  const [filter, setFilter] = React.useState('')

  const { data } = useQuery({
      queryKey: ['todos', filter],
      queryFn: () => fetchTodos(filter),
      // ⬇️ disabled as long as the filter is empty
      enabled: !!filter
  })

  return (
      <div>
        // 🚀 applying the filter will enable and execute the query
        <FiltersForm onApply={setFilter} />
        {data && <TodosTable data={data}} />
      </div>
  )
}

 Essential Defaults To Take into Account

Maintainers of the TanStack community have come up with a set of default values for many of React Query parameters to minimize setup. They also have a good balance between fresh data and not spamming the server too often. 

It’s essential that you know what each parameter does because sometimes you may wonder why a request was made without your knowledge.

You’ll first  need to understand what an ‘active query’ means, so you can also understand the defaults. 

Active query means that the query is ‘enabled’ and it has at least one ‘observable’ (at least one mounted component uses that query).

The defaults include:

  1. ‘refetchOnWindowFocus’ (default = true) – will refetch active queries if the window was focused
  2. ‘refetchOnReconnect’ (default = true) – will refetch active queries if the internet connection of the client becomes online (if it was previously offline)
  3. ‘refetchInterval’ (default  = false) – will refetch active queries every X milliseconds
  4. ‘staleTime’ (default = 0) – by default every query is considered stale
    1. Fresh query – data is served from cache without fetching for fresh data
    2. Stale query – data is served from cache and also does a background fetch for fresh data
  5. ‘cacheTime’ (default = 5 minutes)
    1. If the data is expired (5 minutes elapsed), it is deleted from cache and will trigger a fetch on next usage
    2. If the data has not expired, it is served from cache, and based on stale, you can update it or not in the background

Conclusion

There are several state management tools, but React Query makes it simple to get everything up and running. You simply need to tell React Query where to fetch your data and it’ll cache async data and get background updates without the need to write additional code or install extra configuration.

Overall, you’ll have a React app that’s fast and responsive.