@desolint/tanstack-query
v0.0.1
Published
Framework-agnostic TanStack React Query orchestration layer (query/mutation handlers, SSR prefetch, toast + cache-invalidation wiring) for Desolint frontend projects.
Readme
@desolint/tanstack-query
Framework-agnostic TanStack React Query orchestration for Desolint frontend projects — standardized query/mutation hooks, refetch-mode configuration, toast side-effects, cache invalidation, and SSR prefetching, with zero hard dependency on any specific HTTP client or toast library.
Requirements
- React 18 or newer (peer dependency)
- npm 7 or newer — npm 7+ installs peer dependencies automatically
Install
npm install @desolint/tanstack-queryThat is all you need on npm 7+: @tanstack/react-query, react are listed as peer dependencies, so npm resolves and installs them for you.
@tanstack/react-query-devtools is an optional peer — install it only if you want
the devtools panel.
Neither installs peer dependencies automatically, so name them explicitly:
yarn add @desolint/tanstack-query @tanstack/react-query react
# or
pnpm add @desolint/tanstack-query @tanstack/react-query reactWhy these are peer dependencies, not regular ones
This is the classic case. Two copies of React break hooks outright — React tracks
hook state in module-level internals, so a component rendered by one copy and a hook
from another throws Invalid hook call. Likewise, @tanstack/react-query holds the
query cache in its own module scope: a second copy means a second, empty cache, so
your queries would refetch instead of resolving from data you already have.
Declaring them as peer dependencies means npm reuses the copy your application already has instead of nesting a second one under this package. You keep control of the version; this package just states the range it works with.
Quick start
1. Build a config object
The package never talks to an HTTP client or a toast library directly — you provide adapters in a config object. There is no global setup call: the config is passed to the provider and lives in React context, so the client and server graphs never share or race on state.
// e.g. src/reactQueryConfig.client.ts
import type {ReactQueryConfig} from '@desolint/tanstack-query';
import {getRequest, requestFunctions} from './your-http-client';
import {showToast} from './your-toast-library';
export const reactQueryConfig: ReactQueryConfig = {
request: getRequest, // used for GET, by useQueryHandler
requestFunctions, // {POST, PUT, PATCH, DELETE}, used by useMutationHandler
onToast: ({type, message}) => showToast({type, message}),
};For SSR prefetch, build a second config that adds getServerRequest — keep it in a server-only module so getServerRequest never reaches the browser bundle.
Every request function must resolve to {data: unknown, ...} (an axios-response-shaped object). Whatever data is gets returned as-is by useQueryHandler/useMutationHandler/prefetchQueries — no envelope assumptions. If your backend wraps responses, unwrap inside your adapter before returning.
2. Wrap your app
'use client';
import {ReactQueryProvider} from '@desolint/tanstack-query';
import {reactQueryConfig} from './reactQueryConfig.client';
export function Providers({children}) {
return (
<ReactQueryProvider
config={reactQueryConfig}
devtools={process.env.NODE_ENV === 'development'}
>
{children}
</ReactQueryProvider>
);
}Pass devtools to mount TanStack's devtools panel — the package renders
<ReactQueryDevtools> for you from its own @tanstack/react-query instance,
so you never import @tanstack/react-query-devtools yourself and its version
stays in lockstep with the query client. It no-ops in production builds.
@tanstack/react-query-devtools is an optional peer dependency.
3. Queries and mutations
See examples/basic-query.tsx and examples/basic-mutation.tsx. endpoint may be a string or a builder (params) => string; when you pass params, the same value builds both the URL and the cache key, so they can't drift.
4. SSR prefetch (Next.js App Router)
See examples/ssr-prefetch.tsx. prefetchQueries / ReactPrefetchQueryProvider take config and cookieHeader explicitly — resolve the cookie header yourself ((await cookies()).toString()) so this package has no dependency on Next.js. Each QueryToPrefetch carries its own params, keyed identically to the client hook.
.
The client entry point. Everything below is imported from
@desolint/tanstack-query.
ReactQueryProvider
| Prop | Type | Notes |
| ---------- | ------------------ | -------------------------------------------------------------- |
| config | ReactQueryConfig | Required. The adapter object from step 1. |
| children | ReactNode | Required. |
| devtools | boolean | Mounts TanStack's devtools panel. No-ops in production builds. |
Render the devtools through this prop rather than importing
@tanstack/react-query-devtools yourself — importing it separately can resolve a
second copy of @tanstack/react-query and throw No QueryClient set.
Hooks
useQueryHandler<TData>({queryKey, endpoint, params?, enabled?, customQueryOptions?, callbacks?, refetchConfig?, mergedToastConfig?})
Wraps useQuery. endpoint may be a string or (params) => string; when you pass
params, the same value builds both the URL and the cache key, so the two cannot
drift. refetchConfig.mode: 'live' polls every refreshInSeconds (default 30).
useMutationHandler<TData, TParams>({endpoint, method, callBackFuncs?, queriesToChangeAlways?, toastConfigAlways?})
Wraps useMutation. method selects the function to use from
config.requestFunctions (POST, PUT, PATCH, DELETE). Invalidates and
refetches the queries named in queriesToChange* after a successful mutation.
useInvalidateAndRefetchQuery()
Returns {handleQueries} for invalidating or refetching query keys by hand.
Utilities
getQueryClient(), buildQueryKey(), createRefetchOptions(),
processToastConfig(), extractResponseMessage(), safeMerge().
Constants
| Constant | What it is |
| ----------------------------- | --------------------------------------------------------------------------------- |
| QUERY_TIMINGS | STALE_TIME (30 min), GC_TIME (30 min), MAX_RETRY_DELAY, REFETCH_INTERVAL. |
| QUERY_RETRY_CONFIG | Retry policy. Does not retry a 401; otherwise retries twice. |
| DEFAULT_QUERY_OPTIONS | The defaults ReactQueryProvider applies. |
| REFETCH_MODES | 'static' | 'live'. |
| extractErrorStatus({error}) | Reads an HTTP status from an axios, fetch, or bare-envelope error shape. |
/server
SSR only, imported from @desolint/tanstack-query/server. Kept in a separate entry
point so server code never lands in the client bundle.
prefetchQueries({config, queriesToFetch, cookieHeader}) → Promise<QueryClient>
Throws if config.getServerRequest is missing. Each QueryToPrefetch carries its own
params, keyed identically to the client hook, so a prefetched query resolves from
cache on the client instead of refetching.
ReactPrefetchQueryProvider — dehydrates the prefetched client to the browser.
Both take config and cookieHeader explicitly; resolve the cookie header yourself
((await cookies()).toString()) so this package never depends on Next.js.
Full type definitions ship in dist/index.d.ts and dist/server.d.ts.
Behaviour worth knowing
- Every request function must resolve to
{data: unknown, ...}(an axios-response-shaped object). Whateverdataholds is returned as-is — no envelope assumptions. Unwrap inside your adapter if your backend wraps responses. - Side effects (
onSuccess,onError, toasts) fire once per settled fetch, never once per retry attempt. - There is no global setup call. Config lives in React context, so the client and server graphs never share or race on state.
Development
npm install # install dependencies (Husky wires up git hooks)
npm run build # bundle to dist/
npm run type-check # tsc --noEmit
npm test # vitest
npm run lint # eslintA pre-commit hook runs lint-staged over staged files, and the same lint runs in CI
on every push and PR.
License
MIT © Desolint — see LICENSE.
Free to use, modify and redistribute, commercially or otherwise. Provided "as is", without warranty or liability of any kind.
