A flexible state management solution for handling server state in JavaScript applications, built on top of TanStack Query (formerly React Query) with framework-agnostic reactivity support.
Query Manager is a wrapper around TanStack Query that provides a framework-agnostic way to handle server state in your applications. It abstracts away the complexity of state management while providing a consistent API across different frontend frameworks.
- 🔄 Framework-agnostic server state management
- 🎯 Built-in caching and request deduplication
- 🔌 Pluggable reactivity system (Vue, React, or custom)
- 🚀 Automatic background updates
- 💪 Type-safe query and mutation definitions
- 🎨 Flexible configuration options
- 🔄 Automatic query key normalization
- 📡 Query subscription support
Traditional state management solutions often don't handle server state well because server state:
- Is asynchronous
- Requires caching
- Needs background updates
- Can become stale
- Requires deduplication of requests
Query Manager solves these problems by:
- Providing a unified API for data fetching and mutations
- Implementing intelligent caching strategies
- Offering framework-agnostic reactivity
- Maintaining consistency across multiple components
import { QueryManager } from "query-manager";
const queryConfig = {
// Define your queries and mutations
users: {
type: "query",
define: () => ({
queryKey: ["users"],
queryFn: () => usersService(),
options: (queryClient) => ({
staleTime: 5000,
onSuccess: (data) => {
// Access to queryClient for cache manipulation
queryClient.setQueryData(getExactQueryKey(["otherQuery"]), (currentCache) => ({
...currentCache,
// update logic
}));
},
}),
}),
},
createUser: {
type: "mutation",
define: () => ({
mutationKey: ["createUser"],
mutationFn: (newUser) => createUserService(),
}),
options: {
// tanstack mutation options
},
},
};
const queryManager = new QueryManager(queryConfig);// Define a query with parameters
const userQuery = {
type: "query",
define: (userId) => ({
queryKey: ["user", userId],
queryFn: () => usersService(userId),
options: (queryClient, getExactQueryKey) => ({
staleTime: 5000,
enabled: true,
onSuccess: (userData) => {
// Update related queries
queryClient.setQueryData(getExactQueryKey(["otherQuery"], (current) => current);
},
}),
}),
};
// Using the query
const { state, fetch } = queryManager.users;
// Access query state
console.log(state.data); // Query data
console.log(state.isLoading); // Loading state
console.log(state.error); // Error state
// Subscribe to state changes
queryManager.users.subscribe((state) => {
console.log("Query state:", state);
});
// Manually trigger fetch
await fetch("userId");// Define a mutation
const createUserMutation = {
type: "mutation",
define: () => ({
mutationKey: ["createUser"],
mutationFn: (userData) => createUserService(userData),
options: (queryClient) => ({
onSuccess: (newRecord) => {
// Update users list query
queryClient.setQueryData(getExactQueryKey(["otherQuery"],
(records) => ( [newRecord, ...records]
));
},
}),
}),
};
// Using the mutation
const { state, mutate } = queryManager.createUser;
// Execute mutation
await mutate({ name: "John Doe", email: "john@example.com" });Query Manager automatically normalizes query keys to ensure consistent caching:
// These query keys will be normalized to the same cache key
["users", { page: 1, filters: { status: "active" } }][("users", { filters: { status: "active" }, page: 1 })][
// Normalized to:
("users", "filters:status:active|page:1")
];This ensures:
- Consistent cache keys regardless of object property order
- Proper handling of nested objects and arrays
- Reliable cache invalidation and updates
Create a custom reactivity adapter by implementing the Reactivity interface. By default, Query Manager uses Vue integration:
class CustomReactivity extends Reactivity {
create(initialState) {
// Initialize reactive state
}
set(reactiveObject, newState) {
// Update reactive state
}
get(reactiveObject) {
// Get reactive state
}
}
const queryManager = new QueryManager(
{
// Your query config
},
new CustomReactivity(),
);constructor(config, reactivity?): Creates a new QueryManager instance
{
type: 'query',
define: (params?) => ({
queryKey: string[],
queryFn: () => Promise<any>,
options?: (
queryClient: QueryClient,
getExactQueryKey: (key: string) => string[]
) => ({
enabled?: boolean,
staleTime?: number,
onSuccess?: (data: any) => void,
onError?: (error: any) => void,
// ... other TanStack Query options
})
})
}{
type: 'mutation',
define: () => ({
mutationKey: string[],
mutationFn: (variables: any) => Promise<any>,
options?: (
queryClient: QueryClient,
getExactQueryKey: (key: string) => string[]
) => ({
onSuccess?: (data: any, variables: any) => void,
onError?: (error: any, variables: any) => void,
// ... other TanStack Mutation options
})
})
}