@forgedevstack/forge-query
v1.0.2
Published
Powerful data fetching and caching library for React with DevTools support
Maintainers
Readme
Forge Query
Why Forge Query?
| Feature | Forge Query | Others | |---------|-------------|--------| | Bundle Size | ~12KB | 30KB+ | | DevTools | Built-in + Chrome Extension | Separate package | | Setup | 2 lines | 10+ lines | | Learning Curve | Minimal | Steep | | TypeScript | First-class | Added later |
Features
- Simple API — Just
useQueryanduseMutation - Infinite queries —
useInfiniteQuerywith page accumulation - Prefetch —
prefetchQuery,ensureQueryData,prefetchQueries - Persistence — optional cache restore via
persistQueryClient(JSON-serializable MVP) - Smart Caching — Automatic with configurable stale/cache times
- Background Sync — Auto-refetch on focus, reconnect, and intervals
- Live Queries —
useSubscriptionstreams WebSocket/SSE events into the cache - Optimistic Updates — Declarative
optimisticUpdatewith automatic rollback - DevTools — In-app panel + Chrome extension (optional Bear peer)
- Tiny core — No UI kit required for the query client
- Flexible — Works with fetch, axios, graphql, or any async function
Quick Start
1. Install
npm install @forgedevstack/forge-queryCore depends only on React. For in-app DevTools:
npm install @forgedevstack/bear2. Setup
import { QueryClient, QueryClientContext } from '@forgedevstack/forge-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientContext.Provider value={queryClient}>
<YourApp />
</QueryClientContext.Provider>
);
}3. Fetch Data
import { useQuery } from '@forgedevstack/forge-query';
function UserProfile({ userId }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <Profile user={data} />;
}4. Mutate Data
import { useMutation, useQueryClient } from '@forgedevstack/forge-query';
function CreateTodo() {
const queryClient = useQueryClient();
const { mutate, isLoading } = useMutation({
mutationFn: (todo) => fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(todo),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<button onClick={() => mutate({ title: 'New Todo' })} disabled={isLoading}>
Add Todo
</button>
);
}5. Go Live
import { useSubscription } from '@forgedevstack/forge-query';
function Channel({ channelId }) {
const { data: messages, isSubscribed } = useSubscription({
queryKey: ['messages', channelId],
queryFn: () => fetchMessages(channelId),
source: {
subscribe: (listener) => {
const ws = new WebSocket(`wss://chat.example.com/${channelId}`);
ws.onmessage = (e) => listener(JSON.parse(e.data));
return () => ws.close();
},
},
onEvent: (message, messages = []) => [...messages, message],
});
return <MessageList messages={messages} live={isSubscribed} />;
}DevTools
In-App Panel
Add the DevTools component to see all queries, logs, and cache in real-time.
Requires optional peer @forgedevstack/bear:
import { ForgeQueryDevTools } from '@forgedevstack/forge-query/devtools';
function App() {
return (
<QueryClientContext.Provider value={queryClient}>
<YourApp />
<ForgeQueryDevTools />
</QueryClientContext.Provider>
);
}Chrome Extension
Get a dedicated DevTools panel in Chrome:
- Download from Chrome Web Store (coming soon)
- Or build from source:
cd devtools-extension npm install npm run build - Load in Chrome: Extensions → Developer Mode → Load Unpacked
Features:
- View all queries and their status
- Activity logs with timestamps
- Cache statistics and data preview
- Refetch, invalidate, or remove queries
API Reference
useQuery
const {
data, // The fetched data
error, // Error if failed
isLoading, // Initial loading state
isFetching, // Any fetching (including background)
isError, // Error state
isSuccess, // Success state
refetch, // Manual refetch function
} = useQuery({
queryKey: ['users'], // Unique cache key
queryFn: fetchUsers, // Async function
staleTime: 60000, // Time until stale (ms)
cacheTime: 300000, // Cache retention (ms)
retry: 3, // Retry attempts
enabled: true, // Enable/disable
refetchOnMount: true, // Refetch on mount
refetchOnWindowFocus: true, // Refetch on focus
refetchInterval: false, // Auto-refetch interval
});useMutation
const {
mutate, // Trigger mutation
mutateAsync, // Returns promise
data, // Result data
error, // Error if failed
isLoading, // Loading state
reset, // Reset state
} = useMutation({
mutationFn: (data) => createItem(data),
onMutate: (variables) => { /* Before mutation */ },
onSuccess: (data) => { /* On success */ },
onError: (error) => { /* On error */ },
onSettled: () => { /* Always runs */ },
optimisticUpdate: { // Optional declarative optimistic update
queryKey: ['items'], // Cached query to update
updater: (old, vars) => [...], // Produce optimistic value
reconcile: (old, data, vars) => [...], // Merge server data on success (optional)
invalidateOnSettled: true, // Refetch after settle (optional)
},
});useSubscription
const {
data, // Live cached data
isSubscribed, // Source is being consumed
subscriptionStatus, // 'idle' | 'subscribed' | 'closed'
...queryResult // Everything useQuery returns
} = useSubscription({
queryKey: ['messages', channelId], // Cache entry fed by events
queryFn: fetchMessages, // Optional initial fetch
source: { // Any push source adapter
subscribe: (listener) => {
// Wire up WebSocket/SSE/emitter, call listener(event) per event.
// The source owns reconnection — the library only consumes.
return () => { /* cleanup on unmount */ };
},
},
onEvent: (event, currentData) => nextData, // Fold event into cache
onEventError: (error) => { /* Reducer threw */ },
});Outside React, pipe a source into the cache with the client:
const stop = queryClient.consumeSubscription({
queryKey: ['messages', channelId],
source: mySource,
onEvent: (message, messages = []) => [...messages, message],
});
stop(); // Stop consuminguseInfiniteQuery
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['projects'],
initialPageParam: 0,
queryFn: ({ pageParam }) => fetchProjects(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
});
// data.pages — array of page results
// data.pageParams — params used for each pagePrefetch helpers
await queryClient.prefetchQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
});
const user = await queryClient.ensureQueryData({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 60_000,
});
await queryClient.prefetchQueries([
{ queryKey: ['a'], queryFn: fetchA },
{ queryKey: ['b'], queryFn: fetchB },
]);Cache persistence (MVP)
import {
QueryClient,
persistQueryClient,
createLocalStoragePersister,
} from '@forgedevstack/forge-query';
const queryClient = new QueryClient();
const persistence = persistQueryClient(queryClient, {
storage: createLocalStoragePersister(),
persistKey: 'my-app-query-cache',
buster: 'v1', // bump to invalidate old caches
maxAge: 24 * 60 * 60 * 1000,
});
await persistence.restore();
// subscribe auto-persists on client updates; call persistence.unsubscribe() on teardownLimits: JSON-serializable data only; errors / AbortSignals / functions are not restored; no encryption; not a full offline mutation queue.
QueryClient
const queryClient = new QueryClient({
defaultOptions: {
staleTime: 0,
cacheTime: 5 * 60 * 1000,
retry: 3,
},
});
// Methods
queryClient.getQueryData(['users']); // Get cached data
queryClient.setQueryData(['users'], newData); // Set cache
queryClient.ensureQueryData({ queryKey: ['users'], queryFn: fetchUsers });
queryClient.prefetchQuery({ queryKey: ['users'], queryFn: fetchUsers });
queryClient.invalidateQueries(['users']); // Mark stale & refetch
queryClient.refetchQueries(['users']); // Force refetch
queryClient.removeQueries(['users']); // Remove from cache
queryClient.clear(); // Clear allExamples
Dependent Queries
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
});
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchPosts(user.id),
enabled: !!user, // Wait for user
});Optimistic Updates
Declarative — snapshot and rollback are automatic:
const { mutate } = useMutation({
mutationFn: createTodo,
optimisticUpdate: {
queryKey: ['todos'],
updater: (todos = [], newTodo) => [...todos, { ...newTodo, pending: true }],
reconcile: (todos = [], saved) =>
todos.map((todo) => (todo.pending ? saved : todo)),
},
});
// mutate(newTodo) updates ['todos'] instantly;
// on error the cache rolls back to the pre-mutation snapshot.Or manual, via the onMutate/onError lifecycle:
const { mutate } = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries(['todos']);
const previous = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
return { previous };
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previous);
},
});Live Queries (WebSocket / SSE)
const sseSource = {
subscribe: (listener) => {
const events = new EventSource(`/api/channels/${channelId}/stream`);
events.onmessage = (e) => listener(JSON.parse(e.data));
return () => events.close();
},
};
const { data: messages } = useSubscription({
queryKey: ['messages', channelId],
queryFn: () => fetchMessages(channelId),
source: sseSource,
onEvent: (message, messages = []) => [...messages, message],
});With TypeScript
interface User {
id: number;
name: string;
}
const { data } = useQuery<User, Error>({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
});
// data is User | undefinedConfiguration
const queryClient = new QueryClient({
defaultOptions: {
staleTime: 0, // Data is stale immediately
cacheTime: 5 * 60 * 1000, // 5 minutes
retry: 3,
retryDelay: 1000,
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
},
devtools: {
enabled: process.env.NODE_ENV === 'development',
maxLogs: 100,
},
});Browser Support
| Browser | Version | |---------|---------| | Chrome | 90+ | | Firefox | 90+ | | Safari | 14+ | | Edge | 90+ |
Contributing
We welcome contributions! See CONTRIBUTING.md.
License
MIT © ForgeDevStack
