Skip to content

All the power of TanStack Query, filtered through our charmingly bad ideas, giving you a wrapper that’s basically a functioning monument to questionable design decisions, yet somehow it still manages to keep your server state in line while whispering, ‘I can’t believe this actually works.

Notifications You must be signed in to change notification settings

IMAT13/Query-Manager

Repository files navigation

Query Manager

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.

Table of Contents

Introduction

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.

Features

  • 🔄 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

Why Query Manager?

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:

  1. Providing a unified API for data fetching and mutations
  2. Implementing intelligent caching strategies
  3. Offering framework-agnostic reactivity
  4. Maintaining consistency across multiple components

Usage

Basic Setup

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);

Queries

// 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");

Mutations

// 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 Key Normalization

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

Framework Integration

Custom Framework Integration

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(),
);

API Reference

QueryManager

  • constructor(config, reactivity?): Creates a new QueryManager instance

Query Definition

{
  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
    })
  })
}

Mutation Definition

{
  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
    })
  })
}

About

All the power of TanStack Query, filtered through our charmingly bad ideas, giving you a wrapper that’s basically a functioning monument to questionable design decisions, yet somehow it still manages to keep your server state in line while whispering, ‘I can’t believe this actually works.

Resources

Stars

Watchers

Forks