Skip to content

Latest commit

 

History

History
254 lines (208 loc) · 6.28 KB

File metadata and controls

254 lines (208 loc) · 6.28 KB

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