If you're using React + TanStack Query, you've probably written code like this:
queryFn: async ({ signal }) => {
const { data } = await axios.get('/users', { signal })
return data
}
Or with ky:
queryFn: ({ signal }) =>
ky.get('/users', { signal }).json<User[]>()
Both are perfectly valid.
But I kept asking myself:
Why does the HTTP client need to make my TanStack Query
queryFnadapt its response?
That question led me to build tanstack-fetch.
The idea
TanStack Query is responsible for server state.
The HTTP client should be responsible for HTTP transport.
So the architecture becomes:
React
↓
TanStack Query
↓
tanstack-fetch
↓
Fetch
The HTTP client should fit naturally into the queryFn contract.
That means:
- Return the actual data
- Throw on HTTP errors
- Support
AbortSignal - Keep TypeScript types
- Avoid unnecessary response transformations
Compare the APIs
Axios
queryFn: async ({ signal }) => {
const { data } = await axios.get<User[]>('/users', {
signal,
})
return data
}
ky
queryFn: ({ signal }) =>
ky.get('/users', { signal }).json<User[]>()
tanstack-fetch
queryFn: ({ signal }) =>
api.get<User[]>('/users', { signal })
The difference is small.
But across hundreds of query functions, small pieces of glue code add up.
Authentication is another problem
Real applications usually need more than:
fetch(url)
You may need:
- Access tokens
- Refresh tokens
- Concurrent
401handling - Request retries
- Interceptors
- SSR cookies
- SSE
- Request cancellation
For example, if five requests receive 401 simultaneously, you generally don't want five refresh requests.
You want:
401 ─┐
401 ─┤
401 ─┼──→ one refresh → retry requests
401 ─┤
401 ─┘
tanstack-fetch includes a refresh-token interceptor with single-flight behavior.
tanstack-fetch vs ky vs Axios
| tanstack-fetch | ky | Axios | |
|---|---|---|---|
| TanStack Query focused | ✅ | ❌ | ❌ |
| Fetch-based | ✅ | ✅ | ❌ |
| Typed errors | ✅ | ✅ | ✅ |
| AbortSignal | ✅ | ✅ | ✅ |
| Token refresh | Built-in | Custom | Custom |
| Single-flight refresh | ✅ | Custom | Custom |
| SSR support | First-class | Manual | Manual |
| SSE | ✅ | Custom | Custom |
| Ecosystem | New | Mature | Very mature |
This isn't about declaring a universal winner.
It's about choosing the right abstraction for your architecture.
When should you use tanstack-fetch?
It makes the most sense when:
- You're already using TanStack Query
- You want simpler
queryFns - You want a typed Fetch-based client
- You need auth refresh
- You need SSR support
- You need authenticated SSE
- You're starting a new application
- You're considering migrating from Axios, ky, ofetch, or Fetch
If you simply want a small Fetch wrapper, ky may be a better fit.
If you're already deeply invested in Axios, there may be no reason to migrate.
Migration
Migration is often harder than choosing a new library.
That's why tanstack-fetch includes a migration CLI that can scan existing HTTP call sites and help migrate Axios, ky, ofetch, and Fetch code toward a TanStack Query-oriented architecture.
The goal isn't:
Rewrite everything.
It's:
Make migration incremental and practical.
The bigger idea
I don't think every React application needs a new HTTP client.
But if TanStack Query is already the center of your data layer, your HTTP client should ideally complement it instead of forcing every query function to adapt between two different abstractions.
That's the idea behind tanstack-fetch.
TanStack Query → Server State
tanstack-fetch → HTTP Transport
Fetch → Network Primitive
tanstack-fetch is an open-source project and is not an official TanStack project.
If you're interested, check out the project and let me know what you think.
Top comments (0)