# React axios vs tanstack-fetch for TanStack Query

*Originally published at [mohamadgarmabi.github.io/tanstack-fetch/blog/react-axios-tanstack-fetch](https://mohamadgarmabi.github.io/tanstack-fetch/blog/react-axios-tanstack-fetch).*

> **Stack**
>
> **React 18+** · `tanstack-fetch/react` · `@tanstack/react-query`

Most React apps call axios inside `useQuery`, then stack a `.data` unwrap, an error mapper and a refresh-token queue on top. Each of those exists only because axios predates the `queryFn` contract: return data, throw on failure, honor `AbortSignal`.

That gap shows up as `.data` unwraps, custom error mappers, and refresh-token races. **tanstack-fetch** closes it with the same client every other stack uses — via `FetchProvider` / `useFetch` when you want context.

## TL;DR

| If you need… | Prefer |
| --- | --- |
| Drop into `useQuery` / `useMutation` | **tanstack-fetch** |
| Typed `FetchError` with status + body | **tanstack-fetch** |
| SSE with `Authorization` + resume | **tanstack-fetch/sse** + `useSse` |
| Huge axios interceptor stack, no Query | axios can stay |
| Share one client with Vue/Solid/Angular | **tanstack-fetch** |

```bash
npm install tanstack-fetch @tanstack/react-query
```

## Mental model

axios returns `AxiosResponse`. TanStack Query wants the **body** and a thrown error. Bridging is possible — and noisy.

**Flow:** Component (hooks · JSX) → React Query (useQuery · cache) → **tanstack-fetch** (`useFetch()` · `createFetch` — data · throw · signal) → API (REST · SSE · tRPC)

**Contract that matters for Query:**

1. Success → return the parsed body
2. HTTP failure → **throw** (so `isError` works)
3. Cancel → pass Query’s `signal` through

## Side-by-side

### axios + Query (glue)

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

const UsersPage = () => {
  const { data, error, isPending } = useQuery({
    queryKey: ['users'],
    queryFn: async ({ signal }) => {
      const response = await axios.get<User[]>('/users', { signal })
      return response.data // easy to forget
    },
  })
  // map AxiosError → UI yourself
}
```

### tanstack-fetch + Query (native)

```tsx
import { FetchProvider, useFetch } from 'tanstack-fetch/react'
import { useQuery } from '@tanstack/react-query'
import { isFetchError } from 'tanstack-fetch'

const App = () => (
  <FetchProvider
    baseUrl={import.meta.env.VITE_API_URL}
    getToken={() => localStorage.getItem('access_token')}
  >
    <UsersPage />
  </FetchProvider>
)

const UsersPage = () => {
  const api = useFetch()
  const { data, error, isPending } = useQuery({
    queryKey: ['users'],
    queryFn: ({ signal }) => api.get<User[]>('/users', { signal }),
  })

  if (isPending) return <p>Loading…</p>
  if (isFetchError(error)) return <p>{error.status}: {error.message}</p>
  return <ul>{data.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
}
```

## Bootstrap / wiring

```tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { FetchProvider } from 'tanstack-fetch/react'
import { createFetch } from 'tanstack-fetch'
import { createAuthInterceptor } from 'tanstack-fetch/plugins'

const api = createFetch({
  baseUrl: import.meta.env.VITE_API_URL,
  interceptors: [createAuthInterceptor({ getToken: () => localStorage.getItem('access_token') })],
})

const queryClient = new QueryClient()

export const App = () => (
  <QueryClientProvider client={queryClient}>
    <FetchProvider client={api}>
      <Routes />
    </FetchProvider>
  </QueryClientProvider>
)
```

Or skip the provider and import a module-level `api` into every `queryFn` — `FetchProvider` is optional.

## Comparison graph: where glue accumulates

Rough relative **boilerplate** (lower is better) — a map of where teams spend time, not a micro-benchmark.

| Task | tanstack-fetch | Typical axios path |
| --- | --- | --- |
| Query-ready GET | tanstack-fetch (~18%) | axios + .data (~70%) |
| Typed HTTP errors | FetchError (~22%) | AxiosError map (~65%) |
| 401 refresh + retry | refresh-token plugin (~28%) | interceptor queue (~88%) |
| SSE + Authorization | useSse (~30%) | EventSource / custom (~95%) |
| Shared with Vue/Solid | one createFetch (~20%) | rewrite per stack (~88%) |

## Feature matrix

| Capability | tanstack-fetch | axios |
| --- | --- | --- |
| Returns body for Query `queryFn` | Yes | Adapt (`.data`) |
| Throws typed error with status/body | `FetchError` | Partial |
| Native `AbortSignal` | First-class | Yes (adapt) |
| Status handlers `401`–`429` | `createStatusInterceptor` | Custom |
| Refresh token single-flight | Plugin | Custom |
| Path params typing `:id` | Yes | Manual |
| Upload + progress | `api.upload()` (`tanstack-fetch/sse`) | Yes |
| SSE + `Authorization` | `tanstack-fetch/sse` | No |
| tRPC transport | `tanstack-fetch/trpc` | DIY |
| Bundle (HTTP core) | 3.68KB gzip | Larger |

## Auth, status handlers, refresh

```ts
import { createFetch } from 'tanstack-fetch'
import {
  createAuthInterceptor,
  createRefreshTokenInterceptor,
  createStatusInterceptor,
} from 'tanstack-fetch/plugins'

let expiresAt = 0

const api = createFetch({
  baseUrl: '/api',
  interceptors: [
    createAuthInterceptor({ getToken: () => localStorage.getItem('access_token') }),
    createStatusInterceptor({ 403: () => toast.error('Forbidden') }),
  ],
})

api.use(
  'refresh-token',
  createRefreshTokenInterceptor({
    refresh: async () => {
      const body = await api.post<{ accessToken: string; expiresIn: number }>('/auth/refresh', {
        interceptors: { eject: ['refresh-token', 'auth'] },
      })
      localStorage.setItem('access_token', body.accessToken)
      expiresAt = Date.now() + body.expiresIn * 1000
    },
    before: { getExpiresAt: () => expiresAt, skewMs: 60_000 },
    after: { enabled: true },
  }),
)
```

The plugin keeps **one in-flight refresh** and retries the failed call — the usual interceptor queue, without the queue code.

## Mutations

```tsx
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useFetch } from 'tanstack-fetch/react'

const CreateUser = () => {
  const api = useFetch()
  const queryClient = useQueryClient()
  const mutation = useMutation({
    mutationFn: (input: CreateUserInput) => api.post<User>('/users', { body: input }),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
  })
  return <button onClick={() => mutation.mutate({ name: 'Ada' })}>Create</button>
}
```

## Live data: SSE

Browser `EventSource` cannot set `Authorization`. `tanstack-fetch/sse` streams over `fetch`.

```tsx
import { createFetch } from 'tanstack-fetch/sse'
import { useSse } from 'tanstack-fetch/react'

const api = createFetch({
  plugins: ['sse-resume'],
  getToken: () => localStorage.getItem('access_token'),
})

const OrdersLive = () => {
  const { data, status, error, close } = useSse<OrderEvent>('/orders/stream', { client: api })
  if (status === 'error') return <p>{error?.message}</p>
  return (
    <div>
      <p>{status}</p>
      <pre>{JSON.stringify(data, null, 2)}</pre>
      <button type="button" onClick={close}>Stop</button>
    </div>
  )
}
```

**FetchProvider is not required** for SSE — pass `{ client }`.

Status: `connecting` | `connected` | `disconnected` | `error`.

`connecting` → `connected` → `onMessage → data` → `close / unmount`

## tRPC over the same client

```tsx
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'
import { useQuery, QueryClient } from '@tanstack/react-query'
import { createTRPCClient, httpBatchLink } from '@trpc/client'
import { createTRPCFetch } from 'tanstack-fetch/trpc'
import { api } from './api'
import type { AppRouter } from './server'

const queryClient = new QueryClient()
const trpcClient = createTRPCClient<AppRouter>({
  links: [httpBatchLink({ url: '/api/trpc', fetch: createTRPCFetch(api) })],
})
const trpc = createTRPCOptionsProxy<AppRouter>({ client: trpcClient, queryClient })

useQuery(trpc.post.list.queryOptions())
```

## Migrate

```bash
npx tanstack-fetch migrate --from axios --framework react --write
npx tanstack-fetch doctor --dir ./src
```

1. One `createFetch` (+ optional `FetchProvider`)
2. Port `useQuery` reads first
3. Port mutations
4. Swap custom 401 refresh for the plugin
5. Delete axios when call sites are gone

## Suggested folder layout

```text
src/
  lib/api.ts
  queries/users.ts
  components/UsersPage.tsx
  components/OrdersLive.tsx
  App.tsx          # QueryClientProvider + FetchProvider
```

## Decision tree

1. **Is TanStack Query your source of truth for server state?**
   - Yes → start with `tanstack-fetch`
   - No → axios may stay
2. **Do you need SSE with Authorization or resume?**
   - Yes → `tanstack-fetch/sse` + `useSse`
   - No → either client works for simple REST
3. **Do other frameworks share this backend client?**
   - Yes → one `createFetch` across packages
   - No → still fine; adapters are optional

## Checklist

- [ ] Optional `FetchProvider` or shared `api` module
- [ ] Every `queryFn` / `mutationFn` forwards `{ signal }`
- [ ] Errors via `isFetchError` when you need `status`
- [ ] Refresh via plugin
- [ ] SSE from `tanstack-fetch/sse` when auth matters
- [ ] `npx tanstack-fetch doctor --dir ./src` clean

## Try it

```bash
npm install tanstack-fetch @tanstack/react-query
npx tanstack-fetch doctor --dir ./src
```

## Related

- [React guide](https://mohamadgarmabi.github.io/tanstack-fetch/guide/react)
- [SSE · React](https://mohamadgarmabi.github.io/tanstack-fetch/guide/sse#react)
- [tRPC · React](https://mohamadgarmabi.github.io/tanstack-fetch/guide/trpc#react)
- [Migrate CLI](https://mohamadgarmabi.github.io/tanstack-fetch/guide/migrate)
- [Comparison](https://mohamadgarmabi.github.io/tanstack-fetch/guide/comparison)

## Series

Same deep-dive format for every stack:

- [Vue](https://mohamadgarmabi.github.io/tanstack-fetch/blog/vue-ofetch-tanstack-fetch)
- [Next.js](https://mohamadgarmabi.github.io/tanstack-fetch/blog/nextjs-fetch-tanstack-fetch)
- [Nuxt](https://mohamadgarmabi.github.io/tanstack-fetch/blog/nuxt-ofetch-tanstack-fetch)
- [Solid](https://mohamadgarmabi.github.io/tanstack-fetch/blog/solid-fetch-tanstack-fetch)
- [TanStack Start / Router](https://mohamadgarmabi.github.io/tanstack-fetch/blog/tanstack-start-router-tanstack-fetch)
- [Svelte / SvelteKit](https://mohamadgarmabi.github.io/tanstack-fetch/blog/sveltekit-fetch-tanstack-fetch)
- [Angular](https://mohamadgarmabi.github.io/tanstack-fetch/blog/angular-http-client-tanstack-fetch)

