Skip to content

Latest commit

 

History

History
313 lines (232 loc) · 8.37 KB

File metadata and controls

313 lines (232 loc) · 8.37 KB

🔑 Query Key Factory

A type-safe, declarative API for managing TanStack Query queryKeys at scale. Designed to prevent collisions, enforce consistency, and simplify query key management in large applications.

✨ Features

  • Declarative key hierarchies – define all keys in one place.
  • Fine-grained declaration – define query keys per feature module for better organization.
  • Static & dynamic segments – keys can be fixed or parameterized.
  • Casing transformation – transform keys to snake_case, kebab-case, or keep camelCase.
  • Type-safe – generics enforce correct usage at compile-time with precise tuple literals.
  • Collision-free – centralized definitions guarantee unique keys.
  • Developer-friendly – autocomplete and discoverability in IDEs.

📦 Installation

npm install @txtension/query-key-factory
# or
yarn add @txtension/query-key-factory
# or
bun add @txtension/query-key-factory

🚀 Getting Started

  1. Define your query key hierarchy
import { createKeyHierarchy, dynamic } from "@txtension/query-key-factory";

export const queryKeys = createKeyHierarchy({
  users: {
    list: null,
    detail: dynamic<number>(),
  },
});

  1. Use keys with simple ergonomics
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./queryKeys";

// ✅ Simple and clean API
const queryClient = useQueryClient();

// Full key with parameter
queryKeys.users.detail(1); // → ["users", "detail", 1]

// Prefix key using ._def property
queryKeys.users.detail._def; // → ["users", "detail"]

// Perfect for TanStack Query operations:

// Remove all user detail queries
queryClient.removeQueries({ queryKey: queryKeys.users.detail._def });

// Invalidate all user list queries
queryClient.invalidateQueries({ queryKey: queryKeys.users.list._def });

// Prefetch a specific user
queryClient.prefetchQueries({
  queryKey: queryKeys.users.detail(userId),
  queryFn: () => fetchUser(userId),
});

// ✅ Individual query usage
function UserDetail({ userId }: { userId: number }) {
  const { data } = useQuery({
    queryKey: queryKeys.users.detail(userId),
    queryFn: () => fetchUser(userId),
  });

  return <div>{data?.name}</div>;
}

Key Features:

  • Simple function calls: users.detail(1) for full keys
  • ._def property: users.detail._def for prefix keys
  • Type-safe: TypeScript enforces correct parameter types
  • Clean and intuitive: No confusing callable objects

🧩 API

createKeyHierarchy(definition, options?)

Creates a tree of query keys based on a declarative object with optional casing transformation.

  • Static keys → null values become () => readonly TupleLiteral functions with ._def property.
  • Dynamic keys → dynamic<T>() becomes (param: T) => readonly [...TupleLiteral, T] functions with ._def property.
  • Object nodes → Contain nested properties as usual.
  • NEW: Casing transformation applies to all key segments in the resulting tuples.

Parameters:

  • definition: The query key hierarchy definition object
  • options.casing: Optional casing transformation ("camelCase" | "snake_case" | "kebab-case")

Simple Usage Pattern:

// Default: camelCase (no transformation)
const queryKeys = createKeyHierarchy({
  users: {
    list: null,
    getUserById: dynamic<number>(),
  },
});

// ✅ Function calls for full keys
queryKeys.users.list(); // → ["users", "list"]
queryKeys.users.getUserById(1); // → ["users", "getUserById", 1]

// ✅ ._def property for prefix keys
queryKeys.users.list._def; // → ["users", "list"]
queryKeys.users.getUserById._def; // → ["users", "getUserById"]

Casing Transformation:

// snake_case transformation
const snakeKeys = createKeyHierarchy({
  users: {
    getUserById: dynamic<number>(),
    getAllUsers: null,
  },
}, { casing: "snake_case" });

snakeKeys.users.getUserById(1); // → ["users", "get_user_by_id", 1]
snakeKeys.users.getAllUsers(); // → ["users", "get_all_users"]
snakeKeys.users.getUserById._def; // → ["users", "get_user_by_id"]

// kebab-case transformation
const kebabKeys = createKeyHierarchy({
  users: {
    getUserById: dynamic<number>(),
    getAllUsers: null,
  },
}, { casing: "kebab-case" });

kebabKeys.users.getUserById(1); // → ["users", "get-user-by-id", 1]
kebabKeys.users.getAllUsers(); // → ["users", "get-all-users"]
kebabKeys.users.getUserById._def; // → ["users", "get-user-by-id"]

createQueryKeys(namespace, definition, options?)

Creates a namespaced query key factory for a specific feature module.

Parameters:

  • namespace: The feature namespace (e.g., "users", "products")
  • definition: The query key hierarchy definition object for this feature
  • options.casing: Optional casing transformation ("camelCase" | "snake_case" | "kebab-case")

Returns: A query key hierarchy with the namespace as the root.

const usersKeys = createQueryKeys("users", {
  list: null,
  detail: dynamic<string>(),
});

usersKeys.list(); // → ["users", "list"]
usersKeys.detail("123"); // → ["users", "detail", "123"]

mergeQueryKeys(factories)

Merges multiple feature query key factories into a single root factory.

Parameters:

  • factories: An object mapping feature names to their query key factories

Returns: A combined query key hierarchy with full type safety.

const queryKeys = mergeQueryKeys({
  users: usersKeys,
  products: productsKeys,
});

// Access all features from one place
queryKeys.users.list();
queryKeys.products.detail(1);

dynamic()

Defines a dynamic segment that accepts a parameter of type T.

  • Parameters are NOT serialized - objects maintain reference equality.
  • TypeScript enforces parameter type at compile-time.
  • Returns readonly unknown[] for immutability.
  • Includes ._def property for prefix access.

✅ Benefits

  • No more hardcoded strings or accidental mismatches.
  • Flexible casing options for different naming conventions.
  • Precise tuple literal types (no more readonly unknown[]).
  • Safer collaboration in large teams.
  • Works seamlessly with TanStack Query.

──—

📚 Fine-grained Declaration

For large applications, you can define query keys per feature module instead of a monolithic file:

1. Define keys per feature

// features/users/users.query-keys.ts
import { createQueryKeys, dynamic } from "@txtension/query-key-factory";

export const usersKeys = createQueryKeys("users", {
  list: null,
  detail: dynamic<string>(),
  posts: {
    list: dynamic<{ userId: string }>(),
  },
});
// features/products/products.query-keys.ts
import { createQueryKeys, dynamic } from "@txtension/query-key-factory";

export const productsKeys = createQueryKeys("products", {
  list: null,
  detail: dynamic<number>(),
  categories: {
    all: null,
    byId: dynamic<string>(),
  },
});

2. Merge into a root factory

// query-keys/index.ts
import { mergeQueryKeys } from "@txtension/query-key-factory";
import { usersKeys } from "../features/users/users.query-keys";
import { productsKeys } from "../features/products/products.query-keys";

export const queryKeys = mergeQueryKeys({
  users: usersKeys,
  products: productsKeys,
});

3. Use it anywhere

import { queryKeys } from "./query-keys";

// Type-safe and organized!
queryKeys.users.list();                           // → ["users", "list"]
queryKeys.users.detail("123");                    // → ["users", "detail", "123"]
queryKeys.products.categories.byId("electronics"); // → ["products", "categories", "byId", "electronics"]

Benefits:

  • 📦 Modular: Each feature owns its query keys
  • 🔍 Scalable: Easy to add/remove features
  • 🛡️ Type-safe: Full TypeScript support
  • 🧩 Clean: No monolithic key files

──—

💖 Example

const queryKeys = createKeyHierarchy({
  todos: {
    all: null,
    byId: dynamic<number>(),
    comments: {
      list: dynamic<{ todoId: number }>(),
    },
  },
});

// queryKeys.todos.all()                        → ["todos", "all"]
// queryKeys.todos.byId(1)                      → ["todos", "byId", 1]
// queryKeys.todos.comments.list({ todoId: 1 }) → ["todos", "comments", "list", { todoId: 1 }]

🔠 Roadmap

  • Key serialization customization
  • Devtools integration for debugging queryKeys
  • ESLint plugin for linting hardcoded query keys