featherquery
v1.0.1
Published
Data fetching, light as a feather. A lightweight React library for fetching, caching, and mutating API data with minimal boilerplate.
Maintainers
Readme
🪶 FeatherQuery

This version is the first stable version. If you see any problems, contact with me via email ([email protected]) or feel free to create PRs.
Data fetching, light as a feather. A lightweight React library for fetching, caching, and mutating API data with minimal boilerplate.
✨ Why FeatherQuery?
Modern data-fetching libraries like TanStack Query or RTK Query are powerful, but they often feel heavy and require tons of setup. FeatherQuery is designed to be:
🪶 Lightweight – no clutter, no extra configs
⚡ Fast & efficient – built with caching, refetching, and mutations in mind
🎯 Simple API – just drop in a hook and go
🔧 Extensible – start simple, scale when needed
🚀 Features (Hooks)
useQueryClient
Overview
A React hook that provides access to the appropriate cache instance based on the specified cache mode. Must be used within a QueryProvider context.
Usage
import useQueryClient from './useQueryClient';
function MyComponent() {
const { cache } = useQueryClient('session'); // or 'volatile', 'permanent'
// Use cache methods
const data = cache.get(['my-data']);
cache.set(['my-data'], { value: 'test' });
}Cache Modes
volatile (default) - In-memory cache, clears on page refresh
session - Persists for the browser session only
permanent - Persists until manually cleared
Returns
cache: Cache - The cache instance for the specified mode
Context Requirement
All the hooks must be used within a QueryProvider component, otherwise throws an error.
useFetch
Is a lightweight React hook for fetching data from an API in the simplest way possible. It’s ideal when you just want to fetch and display data without setting up complex state management.
The hook returns:
data – the fetched data (or null if not yet loaded)
status – the current status: "IDLE", "LOADING", "SUCCESS", or "ERROR"
error – any error encountered during fetching
isLoading – boolean, true while the request is in progress
isIdle – boolean, true if no request has been made yet
refetch – a function to manually trigger a new request
Optional pollInterval lets you repeatedly fetch data at a given interval. All requests sent from pollInterval refetch data even if it is non-stale. Requests are automatically aborted on refetch or unmount, so you don’t have to worry about race conditions.
const { data, status, error, isLoading, refetch } = useFetch<User[]>('/api/users', {
pollInterval: 5000,
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data?.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
This keeps it beginner-friendly, explains what it returns, and shows a clear minimal usage.
useQuery
A powerful React hook for data fetching with built-in caching, polling, error handling, and request management. Provides type-safe asynchronous data fetching with optimal performance characteristics
Features
- Automatic caching with configurable strategies
- Request deduplication and cancellation
- Polling support with customizable intervals
- Stale-time management for fresh data
- Comprehensive error handling system
- TypeScript support with generics
- Multiple cache persistence modes
Usage
const { data, error, status, refetch } = useQuery(
['user', userId],
(signal) => fetchUser(userId, signal),
{
staleTime: 60_000 // data is stale after 1 min,
pollInterval: 10_000 // refetch every 10 secs,
onSuccess: (data) => console.log('Data loaded:', data)
}
);API Reference
Parameters:
key: any[]- Unique identifier for cachingfetcher: (signal: AbortSignal) => Promise<T>- Data fetching functionoptions?: QueryOptions<T>- Configuration object
Options:
staleTime: number (default: 30000)- Milliseconds until data is stalepollInterval: number- Auto-refetch interval in msonSuccess: (data: T) => void- Success callbackonError: (error: Error) => void- Error callbackonSettled: (data: T|null, error: Error|null) => void- Completion callbackcacheMode: 'permanent'|'volatile'|'session'- Cache persistence mode
Return Value
data: T | null- Fetched data or nullerror: Error | null- Error object if request failedstatus: string- Current status ('STATIC', 'LOADING', 'FETCHING', 'ERROR')refetch: () => Promise<void>- Manual refetch function
Status States
STATIC- Initial state before fetchingLOADING- First fetch in progressFETCHING- Background refetch with stale data visibleERROR- Fetch failed with error
Caching Behavior
Data cached by serialized key array
Stale data served immediately while background refetch occurs
Automatic cache updates on successful fetches
Manual invalidation via key changes or external cache methods
Performance Optimizations
Request deduplication for identical keys
AbortController cancellation for unmounted components
Memoized key comparison to prevent unnecessary re-renders
Background polling without UI state changes
Stale-while-revalidate pattern for smooth UX
useMutation
Overview
A React hook for handling data mutations (create, update, delete operations) with built-in optimistic updates, retry logic, and cache management. Perfect for forms, actions, and any data-modifying operations.
Featues
- Optimistic updates for instant UI feedback
- Automatic retry mechanism with configurable delay
- Cache invalidation for related queries
- Error handling with rollback capability
- Multiple execution modes (sync and async)
- Request deduplication and cancellation
- TypeScript support with full type safety
Usage
function CreatePost() {
const { mutate, isLoading, error } = useMutation(
{
url: 'your-link/api/posts',
method: 'POST',
},
{
onSuccess: (data) => console.log('Post created:', data),
onError: (error) => console.error('Creation failed:', error),
optimisticUpdate: (cache, variables) => {
// Optimistically update cache
},
}
);
const handleSubmit = (formData) => {
mutate(formData);
};
return (
<form onSubmit={handleSubmit}>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Creating...' : 'Create Post'}
</button>
{error && <div>Error: {error.message}</div>}
</form>
);
}API Reference
useMutation<TResponse, TError, TVariables>(
config: Config<TResponse, TVariables>,
options?: Options<TResponse, TError, TVariables>
)Config Object
(NOTE: Provide either mutateFn or url + method.)
mutateFn?: (variables: TVariables) => Promise<TResponse>- Custom mutation functionurl?: string- Endpoint URL (alternative to mutateFn)method?: string- HTTP method (GET, POST, PUT, DELETE, etc.)headers?: HeadersInit- Custom request headers
Options Object
onSuccess?: (response: TResponse, variables: TVariables) => void- Success callback`onError?: (error: TError, variables: TVariables) => void - Error callback
onSettled?: (response: TResponse|null, error: TError|null, variables: TVariables) => void- Completion callbackinvalidateKeys?: any[]- Query keys to invalidate after mutationoptimisticUpdate?: (cache: Cache, variables: TVariables) => void- Pre-emptive cache updaterollback?: (cache: Cache, variables: TVariables) => void- Rollback function for failed optimistic updatesretries?: number- Number of retry attempts (default: 0)`retryDelay?: (attempt: number) => number - Custom retry delay function
cacheMode?: 'permanent'|'volatile'|'session'- Cache persistence mode
Status States
- IDLE – Initial state before mutation
- LOADING – Mutation in progress
- SUCCESS – Mutation completed successfully
- ERROR – Mutation failed with error
Optimistic Updates with Rollback
- Update cache immediately for a smoother UX
- Rollback changes automatically if mutation fails
const { mutate } = useMutation(
{ url: '/api/like', method: 'POST' },
{
optimisticUpdate: (cache, variables) => {
cache.set(['post', variables.postId], (old) => ({
...old,
likes: old.likes + 1,
}));
},
rollback: (cache, variables) => {
cache.set(['post', variables.postId], (old) => ({
...old,
likes: old.likes - 1,
}));
},
}
);Retry Mechanism
Configure automatic retries for failed requests
Supports custom delay strategies
useMutation(
{ url: '/api/upload', method: 'POST' },
{
retries: 3,
retryDelay: (attempt) => 1000 * attempt, // 1s, 2s, 3s delays
}
);Cache Invalidation
useMutation(
{ url: '/api/comments', method: 'POST' },
{
invalidateKeys: ['posts', 'comments'],
// Invalidates both posts and comments queries after mutation
}
);Error Handling with Callbacks
const { mutate } = useMutation(
{ url: '/api/user', method: 'POST' },
{
onSuccess: (data, variables) => {
showNotification('User created successfully');
},
onError: (error, variables) => {
showError(`Failed: ${error.message}`);
logError(error, variables);
},
onSettled: (data, error, variables) => {
trackAnalytics('user_creation', { success: !error, variables });
}
}
);Caching Behavior
Automatic cache updates through optimistic updates
Selective cache invalidation via invalidateKeys
Rollback capability for failed optimistic updates
Cache mode support for different persistence needs
useInfiniteQuery
useInfiniteQuery is a React hook for handling infinite scrolling or paginated data fetching. It abstracts away the complexity of managing page state, caching, and stale data so you can focus on rendering your list.
This hook lets you:
Fetch the next page or previous page on demand.
Cache pages to avoid unnecessary re-requests.
Automatically detect and refetch stale data.
Cancel ongoing requests to prevent race conditions.
Optionally start with an initial fetch (initialFetch).
Parameters
getPreviousPageParam?: (firstPageParam: string | number) => any- Function to get previous page parameterinitialFetch?: boolean- Whether to fetch initial page automatically (default: false)staleTime?: number- Time in ms before data is considered stale (default: 30000)onSuccess?: (data: PagedDataEntry<T>) => void- Success callbackonError?: (error: Error) => void- Error callbackonSettled?: (data: PagedDataEntry<T>|null, error: Error|null) => void- Completion callbackcacheMode?: 'permanent'|'volatile'|'session'- Cache persistence mode (default: 'volatile')
Return Value
Returns an object with:
data: { pages: T[][], pageParams: any[] }- All loaded pages and their parameterserror: Error | null- Error object if any request failedstatus: string- Current status ('IDLE', 'LOADING', 'SUCCESS', 'ERROR')fetchNextPage: () => void- Function to load the next pagefetchPreviousPage: () => void- Function to load the previous page
Example Usage
// start from page 1 (number)
const {
data,
status,
error,
fetchNextPage,
fetchPreviousPage,
} = useInfiniteQuery(
1, // <-- initial page param (number)
(page, signal) =>
fetch(`/api/messages?page=${page}`, { signal }).then((res) => res.json()),
(lastPageParam) => lastPageParam + 1, // next page number
{
getPreviousPageParam: (firstPageParam) =>
firstPageParam > 1 ? firstPageParam - 1 : undefined,
initialFetch: true,
}
);
Short rationale / tips:
The first argument here is the initial page param (a number in page-number APIs).
getNextPageParam receives the last used page param (a number) and returns the next one.
Make getPreviousPageParam return undefined when there is no previous page (so you can stop fetching backwards)
