Solid fetch vs tanstack-fetch
Stack
Solid 1.8+ · tanstack-fetch/solid · @tanstack/solid-query
Solid’s fine-grained reactivity loves small primitives. Raw fetch / axios still force you to unwrap bodies and map errors before createQuery.
tanstack-fetch/solid mirrors the React adapters: FetchProvider, useFetch, and useSse that return accessors.
This post is the deep dive: architecture, side-by-side code, graphs, migrate path, SSE, tRPC, and when to keep the default stack client.
TL;DR
| If you need… | Prefer |
|---|---|
createQuery without .data unwrap | tanstack-fetch |
SSE accessors (status(), data()) | useSse |
| Share client with React monorepo | same createFetch |
| Tiny app, no Query | native fetch is fine |
npm install tanstack-fetch solid-js @tanstack/solid-queryMental model
Solid Query’s queryFn is still Promise + AbortSignal. Accessors (query.data) are separate from the HTTP client contract.
Contract that matters for Query:
- Success → return the parsed body
- HTTP failure → throw (so
isErrorworks) - Cancel → pass Query’s
signalthrough
Side-by-side
axios + Solid Query (glue)
import axios from 'axios'
import { createQuery } from '@tanstack/solid-query'
const query = createQuery(() => ({
queryKey: ['users'],
queryFn: async ({ signal }) => (await axios.get<User[]>('/users', { signal })).data,
}))tanstack-fetch + Solid Query
import { createFetch } from 'tanstack-fetch'
import { FetchProvider, useFetch } from 'tanstack-fetch/solid'
import { createQuery } from '@tanstack/solid-query'
import { isFetchError } from 'tanstack-fetch'
const api = createFetch({
baseUrl: import.meta.env.VITE_API_URL,
getToken: () => localStorage.getItem('access_token'),
})
const App = () => (
<FetchProvider client={api}>
<UsersPage />
</FetchProvider>
)
const UsersPage = () => {
const client = useFetch()
const query = createQuery(() => ({
queryKey: ['users'],
queryFn: ({ signal }) => client.get<User[]>('/users', { signal }),
}))
if (query.isPending) return <p>Loading…</p>
if (isFetchError(query.error)) return <p>{query.error.status}</p>
return <ul>{query.data?.map((u) => <li>{u.name}</li>)}</ul>
}Bootstrap / wiring
SolidStart: use absolute URL + ssr-forward on the server like other SSR guides. See tRPC · SolidStart.
Comparison graph: where glue accumulates
Rough relative boilerplate (lower is better) — a map of where teams spend time, not a micro-benchmark.
tanstack-fetchtypical axios / fetch path
Feature matrix
| Capability | tanstack-fetch | axios / fetch |
|---|---|---|
| Solid Query fit | Native | Adapt |
| FetchProvider / useFetch | Yes | DIY |
| useSse accessors | Yes | DIY |
| Typed errors | FetchError | DIY |
| Refresh plugin | Yes | DIY |
| Path params | Yes | Manual |
| Bundle | ~4.8KB gzip | Varies |
Auth, status handlers, refresh
import { createFetch } from 'tanstack-fetch'
import { createRefreshTokenInterceptor } from 'tanstack-fetch/plugins'
let expiresAt = 0
const api = createFetch({
baseUrl: '/api',
getToken: () => localStorage.getItem('access_token'),
})
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
import { createMutation, useQueryClient } from '@tanstack/solid-query'
import { useFetch } from 'tanstack-fetch/solid'
const CreateUser = () => {
const api = useFetch()
const queryClient = useQueryClient()
const mutation = createMutation(() => ({
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.
import { createFetch } from 'tanstack-fetch/sse'
import { useSse } from 'tanstack-fetch/solid'
const api = createFetch({ plugins: ['sse-resume'] })
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>
)
}Status: connecting | connected | disconnected | error.
tRPC over the same client
import { createTRPCClient, httpBatchLink } from '@trpc/client'
import { createTRPCFetch } from 'tanstack-fetch/trpc'
import { createQuery } from '@tanstack/solid-query'
import { api } from './api'
import type { AppRouter } from './server'
const trpc = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: '/api/trpc', fetch: createTRPCFetch(api) })],
})
const posts = createQuery(() => ({
queryKey: ['posts'],
queryFn: ({ signal }) => trpc.post.list.query(undefined, { signal }),
}))Migrate
npx tanstack-fetch migrate --from axios --framework solid --write
npx tanstack-fetch doctor --dir ./srcSuggested folder layout
src/
lib/api.ts
app.tsx # FetchProvider + QueryClientProvider
queries/users.ts
components/UsersPage.tsxDecision tree
- Is TanStack Query your source of truth for server state?
- Yes → start with
tanstack-fetch - No → axios/fetch may stay
- Yes → start with
- Do you need SSE with Authorization or resume?
- Yes →
tanstack-fetch/sse+useSse - No → either client works for simple REST
- Yes →
- Do other frameworks share this backend client?
- Yes → one
createFetchacross packages - No → still fine; adapters are optional
- Yes → one
Checklist
- [ ]
FetchProvideror shared api - [ ] Forward
signal - [ ]
isFetchErrorfor status - [ ] SSE accessors via
useSse - [ ] Doctor clean
Try it
npm install tanstack-fetch solid-js @tanstack/solid-query
npx tanstack-fetch doctor --dir ./srcRelated
Series
Same deep-dive format for every stack:
