English version | Русская версия
Experimental (0.x) - TanStack React Query adapter for @asouei/safe-fetch
Converts safe-fetch results to throws and provides sensible defaults for React Query integration.
This adapter bridges the gap between safe-fetch
's safe result API ({ ok: true | false }
) and React Query's expectation of thrown errors for failed requests. It provides:
-
Result conversion:
{ ok: false, error }
→throw error
-
Factory functions: Ready-made
queryFn
andmutationFn
creators -
Sensible defaults: Recommends
retry: false
to let safe-fetch handle retries
npm install @asouei/safe-fetch @asouei/safe-fetch-react-query @tanstack/react-query
# or
pnpm add @asouei/safe-fetch @asouei/safe-fetch-react-query @tanstack/react-query
import { createSafeFetch } from '@asouei/safe-fetch';
import { createQueryFn, createMutationFn, rqDefaults } from '@asouei/safe-fetch-react-query';
import { useQuery, useMutation } from '@tanstack/react-query';
const api = createSafeFetch({
baseURL: '/api',
retries: { retries: 2 } // Let safe-fetch handle retries
});
const queryFn = createQueryFn(api);
const mutationFn = createMutationFn(api);
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: queryFn<User[]>('/users'),
...rqDefaults(), // Important: { retry: false }
});
}
export function useCreateUser() {
return useMutation({
mutationFn: mutationFn<User>('/users', { method: 'POST' }),
});
}
// Usage in component
function UserList() {
const { data: users, error, isLoading } = useUsers();
const createUser = useCreateUser();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.name}</div>; // Typed error from safe-fetch
return (
<div>
{users?.map(user => <div key={user.id}>{user.name}</div>)}
<button onClick={() => createUser.mutate({ name: 'New User' })}>
Add User
</button>
</div>
);
}
Creates a query function factory for React Query.
const queryFn = createQueryFn(api);
const getUsersFn = queryFn<User[]>('/users', {
headers: { Authorization: 'Bearer token' }
});
useQuery({
queryKey: ['users'],
queryFn: getUsersFn,
...rqDefaults()
});
Creates a mutation function factory. Defaults to POST
method.
const mutationFn = createMutationFn(api);
const createUserFn = mutationFn<User>('/users'); // POST by default
const updateUserFn = mutationFn<User>('/users', { method: 'PUT' });
useMutation({
mutationFn: createUserFn // (body) => Promise<User>
});
Returns recommended React Query defaults.
rqDefaults(); // { retry: false }
Why retry: false
? Let safe-fetch handle retries with proper exponential backoff, jitter, and Retry-After
support instead of React Query's simpler retry logic.
Utility to convert safe results to throws (re-exported from core for convenience).
const result = await unwrap(safeFetch.get('/users'));
// Throws on error, returns data on success
import { z } from 'zod';
const UserSchema = z.array(z.object({
id: z.number(),
name: z.string(),
email: z.string().email()
}));
export function useUsers() {
const queryFn = createQueryFn(api);
return useQuery({
queryKey: ['users'],
queryFn: queryFn<z.infer<typeof UserSchema>>('/users', {
validate: (raw) => {
const result = UserSchema.safeParse(raw);
return result.success
? { success: true, data: result.data }
: { success: false, error: result.error };
}
}),
...rqDefaults()
});
}
import type { HttpError, NetworkError } from '@asouei/safe-fetch';
const isHttpError = (error: any): error is HttpError =>
error?.name === 'HttpError';
const isNetworkError = (error: any): error is NetworkError =>
error?.name === 'NetworkError';
function UserList() {
const { data, error } = useUsers();
if (error) {
if (isHttpError(error)) {
return <div>Server error: {error.status} {error.statusText}</div>;
}
if (isNetworkError(error)) {
return <div>Network error: Check your connection</div>;
}
return <div>Unknown error: {error.message}</div>;
}
return <div>{/* render users */}</div>;
}
export function useInfiniteUsers() {
const queryFn = createQueryFn(api);
return useInfiniteQuery({
queryKey: ['users', 'infinite'],
queryFn: ({ pageParam = 1 }) =>
queryFn<{ users: User[]; nextPage?: number }>('/users', {
query: { page: pageParam, limit: 10 }
})(),
getNextPageParam: (lastPage) => lastPage.nextPage,
...rqDefaults()
});
}
// ✅ Good
useQuery({
queryKey: ['users'],
queryFn: queryFn('/users'),
...rqDefaults()
});
// ❌ Avoid - React Query will retry with its own logic
useQuery({
queryKey: ['users'],
queryFn: queryFn('/users')
// missing rqDefaults()
});
// ✅ Good
const api = createSafeFetch({
retries: {
retries: 2,
baseDelayMs: 300
}
});
// ❌ Avoid - double retries
useQuery({
queryFn: queryFn('/users'),
retry: 3 // Don't do this with safe-fetch
});
function UserProfile({ id }: { id: string }) {
const { data: user, isLoading, error } = useQuery({
queryKey: ['users', id],
queryFn: queryFn<User>(`/users/${id}`),
...rqDefaults()
});
// Handle all states explicitly
if (isLoading) return <UserSkeleton />;
if (error) return <ErrorBoundary error={error} />;
if (!user) return <NotFound />; // Shouldn't happen, but be safe
return <div>{user.name}</div>;
}
- React Query: v5.x
- SSR/Next.js: Compatible (pure functions, no runtime React dependency)
- Bundle size: Minimal - only thin wrapper functions
Instead of providing custom hooks like useSafeQuery
, this adapter focuses on:
- Minimal API surface: Just factory functions
- No React peer dependency: Works in any React Query setup
- Composable: Use with existing React Query patterns
- Type-safe: Preserves safe-fetch's error typing
This is expected! The adapter converts { ok: false }
results into thrown errors that React Query can handle.
Make sure to specify the expected return type:
// ✅ Good
const queryFn = createQueryFn(api);
const getUserFn = queryFn<User>('/user/123');
// ❌ Type issues
const getUserFn = queryFn('/user/123'); // unknown return type
Remember to use rqDefaults()
to disable React Query's retries:
useQuery({
queryKey: ['data'],
queryFn: queryFn('/data'),
...rqDefaults() // This sets retry: false
});
Before:
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: async () => {
const result = await safeFetch.get<User[]>('/users');
if (!result.ok) throw result.error;
return result.data;
},
retry: false
});
}
After:
const queryFn = createQueryFn(api);
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: queryFn<User[]>('/users'),
...rqDefaults()
});
}
- v0.1: Core adapter functions ✅ Published
-
v0.2: Optional custom hooks (
useSafeQuery
,useSafeMutation
) - v1.0: Stable production release after community feedback
MIT © Aleksandr Mikhailishin