Type-Safe Query Keys for TanStack Query – Query Key Manager

Type-safe query key management for TanStack Query. Centralized namespaces, collision detection, and enhanced developer experience.

Query Key Manager is a lightweight, type-safe, and scalable utility for managing query keys in applications that use TanStack Query.

It provides a centralized, structured way to define and organize query keys while preventing common issues like key collisions and inconsistent parameter types.

Query Key Manager works by establishing a single source of truth for all query keys in your application.

You define key builders using the QueryKeyManager.create() method, specifying the namespace and key functions with their required parameters.

The library then provides compile-time type checking for arguments and runtime duplicate detection in development mode, all while maintaining zero performance overhead in production builds since the type system disappears after TypeScript compilation.

Features

🛡️ Type-safe key generation prevents runtime errors through compile-time argument validation.

🏗️ Centralized key management consolidates all query keys into organized, discoverable namespaces.

Zero runtime overhead leverages TypeScript’s type system without adding production bundle weight.

🔒 Collision detection identifies duplicate key names at both compile time and development runtime.

📦 Nested namespace support handles complex application structures with hierarchical key organization.

🔍 Enhanced discoverability provides getQueryKeys() method to inspect all registered keys.

🔄 Migration utilities assist with transitioning from legacy string-based key systems.

📝 IntelliSense integration offers autocomplete and type hints for all key functions.

Use Cases

  • Large-scale React applications where multiple developers work with numerous query keys across different features and modules.
  • Multi-team projects requiring consistent query key naming conventions and preventing accidental key conflicts between different development teams.
  • Migration scenarios when transitioning from legacy query key management systems to a more structured approach.
  • Complex data relationships involving dependent queries where key composition and inheritance patterns are necessary.
  • Development environments needing better debugging capabilities and visibility into query key usage patterns.

How to Use It

1. Install React Query Key Manager:

npm install react-query-key-manager
# or
yarn add react-query-key-manager
# or
pnpm add react-query-key-manager

2. Create a dedicated file for your query key definitions. This file will serve as the central registry for all query keys in your application:

// src/lib/queryKeys.ts
import { QueryKeyManager } from "react-query-key-manager";
export const userKeys = QueryKeyManager.create("user", {
  profile: (userId: string) => ["user", "profile", userId],
  settings: (userId: string, section?: string) => 
    section ? ["user", "settings", userId, section] : ["user", "settings", userId],
  notifications: (userId: string, page: number = 1) => 
    ["user", "notifications", userId, page],
});
export const postKeys = QueryKeyManager.create("post", {
  list: (filters: { category?: string; tags?: string[]; limit?: number }) => 
    ["posts", "list", filters],
  detail: (postId: string) => ["post", "detail", postId],
  comments: (postId: string, sortBy: "newest" | "oldest" = "newest") => 
    ["post", "comments", postId, sortBy],
});

3. For applications with complex hierarchical structures, utilize nested namespaces to maintain organization:

export const adminKeys = QueryKeyManager.create("admin", {
  users: {
    list: (page: number, filters: { role?: string; status?: string }) => 
      ["admin", "users", "list", page, filters],
    detail: (userId: string) => ["admin", "users", "detail", userId],
    permissions: (userId: string) => ["admin", "users", "permissions", userId],
  },
  reports: {
    analytics: (dateRange: { start: Date; end: Date }) => 
      ["admin", "reports", "analytics", dateRange],
    usage: (metric: string, period: "daily" | "weekly" | "monthly") => 
      ["admin", "reports", "usage", metric, period],
  },
});

4. Implement the query keys in your React components by importing the appropriate key builders and using them with TanStack Query hooks:

// src/components/UserProfile.tsx
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { userKeys } from "../lib/queryKeys";
interface UserProfileProps {
  userId: string;
}
export function UserProfile({ userId }: UserProfileProps) {
  const queryClient = useQueryClient();
  const { data: profile, isLoading } = useQuery({
    queryKey: userKeys.profile(userId),
    queryFn: () => fetchUserProfile(userId),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
  const { data: settings } = useQuery({
    queryKey: userKeys.settings(userId, "privacy"),
    queryFn: () => fetchUserSettings(userId, "privacy"),
    enabled: !!profile,
  });
  const updateProfileMutation = useMutation({
    mutationFn: updateUserProfile,
    onSuccess: () => {
      // Invalidate related queries
      queryClient.invalidateQueries({ queryKey: userKeys.profile(userId) });
      queryClient.invalidateQueries({ queryKey: userKeys.settings(userId) });
    },
  });
  if (isLoading) return <div>Loading...</div>;
  return (
    <div>
      <h1>{profile?.name}</h1>
      <button onClick={() => updateProfileMutation.mutate(newData)}>
        Update Profile
      </button>
    </div>
  );
}

5. For complex scenarios involving key composition and dependent queries, create composite keys that combine multiple namespaces:

export const dashboardKeys = QueryKeyManager.create("dashboard", {
  userSummary: (userId: string) => [
    "dashboard", 
    "summary", 
    ...userKeys.profile(userId),
    ...userKeys.notifications(userId, 1),
  ],
  adminOverview: (filters: { dateRange: string; department: string }) => [
    "dashboard",
    "admin-overview",
    ...adminKeys.users.list(1, { status: "active" }),
    ...adminKeys.reports.analytics({ start: new Date(), end: new Date() }),
    filters,
  ],
});

6. When migrating from existing string-based keys, use the provided migration utilities to ensure backward compatibility:

import { migrateLegacyKeys } from "react-query-key-manager";
// Legacy key function
const legacyUserProfileKey = (userId: string) => `user-profile-${userId}`;
// Migration wrapper
const migratedUserKey = migrateLegacyKeys(
  "legacy-user-profile",
  (userId: string) => userKeys.profile(userId)
);

7. Take advantage of the debugging capabilities by inspecting all registered keys during development:

// During development, log all registered keys
if (process.env.NODE_ENV === "development") {
  const allKeys = QueryKeyManager.getQueryKeys();
  console.table(allKeys);
}

Related Resources

  • TanStack Query Documentation – Official documentation for React Query, covering all query management patterns and best practices.
  • React Query DevTools – Development tools for debugging and inspecting query cache state in React applications.
  • React Query Kit – Alternative toolkit for managing React Query operations with enhanced type safety.

FAQs

Q: Does React Query Key Manager affect runtime performance?
A: No, the library uses TypeScript’s type system exclusively, which disappears during compilation. The runtime footprint is minimal, consisting only of simple object structures for key generation.

Q: Can I use this library with existing React Query implementations?
A: Yes, React Query Key Manager is designed for gradual adoption. You can migrate individual query keys incrementally while maintaining compatibility with existing string-based keys throughout your application.

Q: How does duplicate key detection work across different namespaces?
A: The library checks for duplicate key names within each namespace at compile time through TypeScript errors. Runtime detection in development mode identifies actual key collisions by comparing generated key arrays.

Q: Is it possible to extend or modify key builders after creation?
A: Key builders are immutable after creation to maintain type safety. However, you can create new builders that compose or extend existing ones using key composition patterns.

Q: What happens if I need dynamic key generation based on runtime conditions?
A: The library supports dynamic key generation through function parameters. You can pass runtime values to key functions, and the TypeScript compiler will enforce parameter types while allowing flexible key creation.

Emmanuel Alozie

Emmanuel Alozie

Leave a Reply

Your email address will not be published. Required fields are marked *