@kitsune.io/query-manager
v1.0.0-beta
Published
A TypeScript-first query manager for React and React Native. Includes caching, pagination, retries, optimistic UI, realtime updates, background refetch, garbage collection, and seamless Suspense integration.
Maintainers
Readme
🚀 Query Manager
A modern, lightweight alternative to TanStack Query for React.
Built from scratch with hooks, Suspense support, realtime updates, pagination, retries, GC, optimistic updates — everything you need for production.
✨ Features
- ⚡ Declarative hooks (
useQuery) and Suspense API (createSuspenseQuery) - 🔄 Stale-while-revalidate fetching
- 📑 Pagination: page-based & cursor-based
- ⏱ Retry with exponential backoff
- 📡 Realtime updates (polling & WebSocket)
- 🕵️ DevTools-friendly (
manager.inspect()) - 🎯 Optimistic updates with rollback
- 🗑 Drop vs Reset distinction
- 🧹 Garbage collection for unused queries
- ✅ Full TypeScript types
📦 Installation
npm install query-manager
# or
yarn add query-manager🧠 Core Concepts
QueryManager
The central cache & orchestrator.
Responsible for creating queries, caching, retries, pagination, realtime, and GC.
useQuery (React Hook)
React hook for consuming queries with full lifecycle (loading/error/stale states).
createSuspenseQuery (Suspense API)
Suspense-compatible query consumer for error boundaries + <Suspense>.
Drop vs Reset
| Method | Effect | Use Case |
| --------- | -------------------------------------------------------------------- | ---------------------------- |
| reset() | Clears data + error, keeps config alive | Reset forms / clear UI state |
| drop() | Clears data & cancels in-flight requests. If no subscribers → GC | Query no longer needed |
🚀 Usage
Basic useQuery
import { useQuery } from "./useQuery";
function Users() {
const { data, loading, error, update } = useQuery("users", {
url: "/api/users",
staleTime: 60_000,
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Failed</p>;
return (
<div>
<button onClick={update}>Refresh</button>
<ul>
{data?.map((u: any) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
</div>
);
}Suspense API
import { createSuspenseQuery } from "./suspenseQuery";
function Profile() {
const { data } = createSuspenseQuery({ key: "profile", url: "/api/profile" });
return <div>{data.name}</div>;
}
<Suspense fallback={<p>Loading...</p>}>
<ErrorBoundary fallback={<p>Failed</p>}>
<Profile />
</ErrorBoundary>
</Suspense>;🔄 Pagination
Page Mode
useQuery("posts", {
url: "/api/posts",
pagination: { enabled: true, type: "page", pageParam: 1, pageSize: 10 },
});Backend must respond with:
{
"data": [{ "id": 1, "title": "..." }],
"totalPages": 42
}Cursor Mode
useQuery("messages", {
url: "/api/messages",
pagination: { enabled: true, type: "cursor", pageSize: 20, accumulate: true },
});Backend must respond with:
{
"data": [{ "id": "abc", "text": "..." }],
"nextCursor": "cursor-token"
}🌀 Realtime
Polling
useQuery("stocks", {
url: "/api/stocks",
refetchInterval: 5000, // every 5s
});WebSocket
manager.createQuery("chat", {
params: { url: "/api/chat" },
realtime: { enabled: true, type: "websocket", url: "wss://example.com/chat" },
});Backend should send messages shaped like your query data.
⚡ Optimistic Updates
const { optimisticUpdate } = useQuery("todos", { url: "/api/todos" });
function addTodo() {
const rollback = optimisticUpdate(() => [
...todos,
{ id: "temp", text: "New Todo" },
]);
fetch("/api/todos", {
method: "POST",
body: JSON.stringify({ text: "New Todo" }),
}).catch(() => rollback()); // rollback if server fails
}🧹 Garbage Collection (GC)
- Queries with no subscribers and no realtime are GC’d after
gcTime(default 5 min). drop()accelerates cleanup.reset()preserves config, does not GC.
🧑💻 DevTools
Use inspect() to get a live snapshot:
console.log(manager.inspect());Pro Tip 💡
Expose it in DevTools:
(window as any).__QUERIES__ = () => manager.inspect();Then in console:
__QUERIES__();🛠 Backend Requirements
✅ Page Mode: { data: [...], totalPages: number }
✅ Cursor Mode: { data: [...], nextCursor: string | null }
✅ Error Shape (recommended): { error: string, code?: number }
✅ WebSocket Messages: JSON stringified query payloads
📑 Cheatsheet
update()→ Refetch nowcancel()→ Cancel in-flight requestdrop()→ Clear + cancel, may GCreset()→ Clear but keep configfetchNextPage()/fetchPreviousPage()/fetchLastPage()optimisticUpdate(fn)→ Mutate + rollback supportmarkStale()→ Force next fetch
⚔️ Comparison with TanStack Query
| Feature | Query Manager | TanStack Query | | ------------------ | --------------- | ----------------- | | Size / Simplicity | ✅ Lightweight | ❌ Heavy, complex | | Suspense-first | ✅ Built-in | ⚠️ Partial | | Realtime support | ✅ Polling + WS | ❌ External | | Optimistic updates | ✅ Built-in | ✅ Yes | | Garbage Collection | ✅ Simple | ⚠️ Advanced | | Learning Curve | ✅ Easy | ❌ Steep |
✅ Summary
- 🔥 Easy alternative to TanStack Query
- ✅ Caching, retries, pagination, realtime, optimistic updates
- 🛠 Works with simple backend contracts
- 🧑💻 Debuggable with
inspect()
