Back to Blog
6 January 2026

Why You Should be Using Tanstack Query

Tanstack Query
Why You Should be Using Tanstack Query

Let’s be honest: fetching data in React has historically been a bit of a faff at times. Trying to cleanly juggle loading states, trying to figure out why an API is being called three times on page load - sound familiar?

I present you TanStack Query. It’s not just another fetching library, it’s the tool that finally makes server state feel manageable.

So what’s the hype?

At its core, TanStack Query is a extremely powerful tool for fetching and cache management.

  • Performance Wins: It’s built to reduce unnecessary fetching. Why fetch the same data twice if you know it wouldn’t have changed?

  • Mutations: With useMutation, handling updates or deletions becomes super simple, giving you a clean way to sync your UI with the server.

  • Some magical props:

    • staleTime: This tells the app exactly how long data is considered "fresh" before it needs to check the server again. If the component/another tries to fetch the data again while data is still fresh, it will just use the data from the cache instead of making another api call.

    • enabled: Perfect for dependent queries. For example, if you don't want to fetch user details until you actually have a userId, you can just set enabled: !!userId.

    • select: You can transform your data before it ever reaches your component, meaning your components only re-render when the specific slice of data they care about changes. E.g. your endpoint returns user data with many fields, but the component is only interest in the user.name. We can use select to only return the user.name, which means if other user data changes, e.g. their average rating, the output of the select still stays the same and our component doesn’t need to re-render.

export const useCustomFlavourNotes = (userId: string | undefined) => {
  return useQuery({
    queryKey: [customFlavourNoteKeys.all, userId],
    queryFn: () => customFlavourNoteService.listCustomFlavourNotes(userId!),
    enabled: !!userId,
    select: (data) => data.flavourNotes.map((note) => note.name),
  })
}

But Can’t I Just Use useEffect or Redux?

useEffect 

Using useEffect for fetching might be fine for a very small project, but isn’t scalable or efficient for something larger. You’d typically fetch the data, manually shove it into a local useState, and suddenly you’re managing three different variables for every single API call. Or even worse, if multiple components are fetching the same data, we will just be making extra api calls for no reason.

Redux?

Redux is great for global state, but using it for server data feels like overkill. The amount of boilerplate required: actions, reducers, thunks is too much. TanStack Query replaces all that work with a single hook.

How We used Tanstack Query at Olio

We were initially fetching using Redux on our apps at Olio, but started using Tanstack query for any new fetching, whilst we slowly started migrating some of our heavier fetching via redux to Tanstack Query. Here’s how we used it at Olio:

1. A Hook-First Approach

We wrapped every query and mutation into custom hooks. This meant that if a developer needed to fetch "Articles," they didn't need to know the endpoint or the configuration, they just called useArticles(). Furthermore, it made fetching the same data in multiple places a breeze, we simply just used the same hook.

2. Query Keys in a const

We moved all our query keys into a dedicated constants file. This ensured that invalidating a query in a mutation used the exact same key as the initial fetch without any typo, and just made it easier to construct our query keys/ do invalidations of the cache when needed. It also means you aren’t hardcoding query keys across the application.

3. A Clean Folder Structure

To keep things from becoming a "junk drawer," we followed a strict hierarchy:

Directory

Purpose

api/articles/hooks.ts

Standard GET fetching hooks

api/articles/mutations.ts

POST, PATCH, and DELETE logic

api/articles/keys.ts

The "source of truth" for query keys

Improving this approach further

After watching a video on Tanstack Query I realise we could’ve improved this approach further: stop defining your query config inside the hook.

Instead, extract the configuration (the queryKey, queryFn, and staleTime) into its own object or file. This allows you to share the exact same logic across different parts of the app or even different hooks without duplicating the "rules" of that data. It would make our hooks even cleaner and your logic much easier to test.

Example:

export const flavourNotesQuery = {
  queryOptions: (userId: string) => ({
    queryKey: [customFlavourNoteKeys.all, userId],
    queryFn: () => customFlavourNoteService.listCustomFlavourNotes(userId!),
    enabled: !!userId,
  }),
}

export const useCustomFlavourNotes = (userId: string | undefined) => {
  return useQuery(flavourNotesQuery.queryOptions(userId!))
}