🔑 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
- Define your query key hierarchy
import { createKeyHierarchy, dynamic } from "@txtension/query-key-factory";
export const queryKeys = createKeyHierarchy({
users: {
list: null,
detail: dynamic<number>(),
},
});⸻
- 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._deffor prefix keys - Type-safe: TypeScript enforces correct parameter types
- Clean and intuitive: No confusing callable objects
⸻
🧩 API
Creates a tree of query keys based on a declarative object with optional casing transformation.
- Static keys →
nullvalues become() => readonly TupleLiteralfunctions with._defproperty. - Dynamic keys →
dynamic<T>()becomes(param: T) => readonly [...TupleLiteral, T]functions with._defproperty. - 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 objectoptions.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"]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 featureoptions.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"]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);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
._defproperty 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:
// 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>(),
},
});// 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,
});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