├── public └── banner.png ├── tsconfig.json ├── .npmignore ├── .gitignore ├── package.json ├── pnpm-lock.yaml ├── src └── index.ts └── README.md /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techwithmanuel/react-query-key-manager/HEAD/public/banner.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "CommonJS", 5 | "declaration": true, 6 | "outDir": "dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["src"], 12 | "exclude": ["node_modules", "dist", "node"] 13 | } 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Source files (only publish compiled dist/) 2 | src/ 3 | 4 | # Config files 5 | tsconfig.json 6 | tsconfig.*.json 7 | 8 | # Ignore git and editor configs 9 | .gitignore 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | .vscode/ 15 | .idea/ 16 | 17 | # Test files 18 | __tests__/ 19 | *.test.ts 20 | *.spec.ts 21 | 22 | # Misc 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node modules 2 | node_modules/ 3 | 4 | # Build output 5 | dist/ 6 | 7 | # TypeScript cache 8 | *.tsbuildinfo 9 | 10 | # Logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Environment files 17 | .env 18 | .env.local 19 | .env.*.local 20 | 21 | # Mac system files 22 | .DS_Store 23 | 24 | # Editor settings 25 | .idea/ 26 | .vscode/ 27 | *.swp 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-query-key-manager", 3 | "version": "0.0.113", 4 | "description": "A lightweight TypeScript library for managing and organizing query keys for React Query projects, enabling consistent, type-safe key usage without extra dependencies.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "keywords": [ 11 | "react-query", 12 | "query keys", 13 | "key manager", 14 | "cache keys", 15 | "react", 16 | "typescript", 17 | "data-fetching", 18 | "state management", 19 | "type-safe" 20 | ], 21 | "scripts": { 22 | "build": "tsc", 23 | "clean": "rm -rf dist", 24 | "dev": "tsc --watch" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^24.3.0", 28 | "typescript": "^5.9.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@types/node': 12 | specifier: ^24.3.0 13 | version: 24.3.0 14 | typescript: 15 | specifier: ^5.9.2 16 | version: 5.9.2 17 | 18 | packages: 19 | 20 | '@types/node@24.3.0': 21 | resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} 22 | 23 | typescript@5.9.2: 24 | resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} 25 | engines: {node: '>=14.17'} 26 | hasBin: true 27 | 28 | undici-types@7.10.0: 29 | resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} 30 | 31 | snapshots: 32 | 33 | '@types/node@24.3.0': 34 | dependencies: 35 | undici-types: 7.10.0 36 | 37 | typescript@5.9.2: {} 38 | 39 | undici-types@7.10.0: {} 40 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type QueryKeyPart = string | number | boolean | object | undefined; 2 | export type QueryKey = readonly QueryKeyPart[]; 3 | 4 | function createQueryKey( 5 | ...parts: T 6 | ): T { 7 | return parts; 8 | } 9 | 10 | export function defineKey< 11 | const Parts extends readonly QueryKeyPart[], 12 | Args extends unknown[] = [] 13 | >(fn: (...args: Args) => Parts): (...args: Args) => Parts { 14 | return ((...args: Args) => { 15 | const parts = fn(...args); 16 | return createQueryKey(...parts); 17 | }) as any; 18 | } 19 | 20 | export type QueryKeyBuilder< 21 | Args extends unknown[] = [], 22 | Return extends readonly QueryKeyPart[] = readonly QueryKeyPart[] 23 | > = (...args: Args) => Return; 24 | 25 | export type QueryKeyRegistry = Record>; 26 | 27 | type ValidFunction = T extends (...args: infer A) => infer R 28 | ? R extends readonly QueryKeyPart[] 29 | ? T 30 | : never 31 | : never; 32 | 33 | type ValidateKeyMap = { 34 | [K in keyof T]: T[K] extends (...args: any[]) => any 35 | ? ValidFunction 36 | : T[K] extends object 37 | ? ValidateKeyMap 38 | : never; 39 | }; 40 | 41 | export class QueryKeyManager { 42 | private static registry: QueryKeyRegistry = {}; 43 | private static keyNames: Set = new Set(); 44 | 45 | static create< 46 | const KeyMap extends Record | object> 47 | >(name: string, keyMap: KeyMap & ValidateKeyMap): KeyMap { 48 | if (this.keyNames.has(name)) { 49 | if (process.env.NODE_ENV !== "production") { 50 | throw new Error(`QueryKeyManager: Key name "${name}" already exists`); 51 | } 52 | return keyMap; 53 | } 54 | 55 | this.keyNames.add(name); 56 | 57 | const register = (prefix: string, node: Record) => { 58 | for (const [key, value] of Object.entries(node)) { 59 | const fullKey = `${prefix}.${key}`; 60 | if (typeof value === "function") { 61 | this.registry[fullKey] = value as QueryKeyBuilder; 62 | } else if (value && typeof value === "object") { 63 | register(fullKey, value); 64 | } 65 | } 66 | }; 67 | 68 | register(name, keyMap); 69 | 70 | return keyMap; 71 | } 72 | 73 | static getQueryKeys(): Readonly { 74 | return Object.freeze({ ...this.registry }); 75 | } 76 | 77 | static clearRegistry(): void { 78 | this.registry = {}; 79 | this.keyNames.clear(); 80 | } 81 | 82 | static registerLegacy( 83 | legacyKey: string, 84 | builder: QueryKeyBuilder 85 | ): void { 86 | if (this.registry[legacyKey]) { 87 | throw new Error(`Legacy key ${legacyKey} conflicts with existing keys`); 88 | } 89 | this.registry[legacyKey] = builder; 90 | } 91 | 92 | private constructor() {} 93 | } 94 | 95 | export function migrateLegacyKeys>( 96 | legacyKeyOrKeys: string | string[], 97 | newBuilder: T 98 | ): T { 99 | if (process.env.NODE_ENV !== "production") { 100 | const list = Array.isArray(legacyKeyOrKeys) 101 | ? legacyKeyOrKeys.join(", ") 102 | : legacyKeyOrKeys; 103 | console.warn(`Migrating legacy key(s): ${list}`); 104 | } 105 | 106 | const keys = Array.isArray(legacyKeyOrKeys) 107 | ? legacyKeyOrKeys 108 | : [legacyKeyOrKeys]; 109 | 110 | for (const key of keys) { 111 | if (QueryKeyManager.getQueryKeys()[key]) { 112 | throw new Error(`Legacy key ${key} conflicts with new keys`); 113 | } 114 | QueryKeyManager.registerLegacy(key, newBuilder); 115 | } 116 | 117 | return newBuilder; 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Banner](/public/banner.png) 2 | 3 | # React Query Key Manager 4 | 5 | A **lightweight**, **type-safe**, and **scalable** way to manage query keys for [`@tanstack/react-query`](https://tanstack.com/query). 6 | Designed to eliminate query key collisions, improve discoverability, and make key composition effortless. 7 | 8 | Install via your preferred package manager: 9 | 10 | ```sh 11 | # pnpm 12 | pnpm add react-query-key-manager 13 | 14 | # yarn 15 | yarn add react-query-key-manager 16 | 17 | # npm 18 | npm install react-query-key-manager 19 | ``` 20 | 21 | ## 🔑 New `defineKey` Helper (v0.0.113) 22 | 23 | A new `defineKey` helper is available to make query key definitions more expressive and type-safe. 24 | It’s fully **optional** and works alongside existing patterns — you can adopt it gradually. 25 | 26 | ### Example 27 | 28 | ```ts 29 | import { QueryKeyManager, defineKey } from "react-query-key-manager"; 30 | 31 | const userKeys = QueryKeyManager.create("user", { 32 | profile: defineKey((userId: string) => ["user", "profile", userId]), 33 | settings: defineKey((userId: string, section?: string) => [ 34 | "user", 35 | "settings", 36 | userId, 37 | section, 38 | ]), 39 | list: defineKey(() => ["user", "list"]), 40 | }); 41 | 42 | // ✅ Strongly typed inference: 43 | const p = userKeys.profile("123"); 44 | // type: readonly ["user", "profile", string] 45 | ``` 46 | 47 | ## 🎯 Problem Statement 48 | 49 | When working on medium-to-large projects with React Query, query keys quickly become a mess: 50 | 51 | **Magic strings everywhere** — prone to typos and silent bugs. 52 | 53 | **Key collisions** — multiple developers accidentally using the same key for different data. 54 | 55 | **Poor discoverability** — no clear place to see all keys at once. 56 | 57 | **Inconsistent arguments** — no type safety for parameters passed to keys. 58 | 59 | These issues often surface late — during debugging or in production — instead of being caught at compile-time. 60 | 61 | ## 💡 Core Idea 62 | 63 | Use a namespaced key builder that: 64 | 65 | 1. Centralizes key definitions in a single source of truth. 66 | 67 | 2. Enforces type safety for key arguments. 68 | 69 | 3. Prevents duplicates both at compile time and at runtime (in dev). 70 | 71 | 4. Supports nested namespaces for large-scale projects. 72 | 73 | 5. Maintains zero runtime cost — types disappear after compilation. 74 | 75 | This approach is lightweight enough to copy-paste directly into your project but also available via npm/yarn/pnpm. 76 | 77 | ## 🛠 Usage Example 78 | 79 | `queryKeys.ts` 80 | 81 | ```tsx 82 | import { QueryKeyManager } from "react-query-key-manager"; 83 | 84 | export const userKeys = QueryKeyManager.create("user", { 85 | profile: (userId: string) => ["user", "profile", userId], 86 | settings: (userId: string) => ["user", "settings", userId], 87 | }); 88 | 89 | export const postKeys = QueryKeyManager.create("post", { 90 | list: (filters: { category: string }) => ["posts", filters], 91 | detail: (postId: string) => ["post", "detail", postId], 92 | }); 93 | 94 | // Nested namespaces supported 95 | export const adminKeys = QueryKeyManager.create("admin", { 96 | users: { 97 | list: (page: number) => ["admin", "users", "list", page], 98 | }, 99 | }); 100 | 101 | // Debugging — list all registered keys 102 | export const allQueryKeys = QueryKeyManager.getQueryKeys(); 103 | ``` 104 | 105 | `UserProfile.tsx` 106 | 107 | ```tsx 108 | import { useQuery } from "@tanstack/react-query"; 109 | import { userKeys } from "./queryKeys"; 110 | 111 | function UserProfile({ userId }: { userId: string }) { 112 | const { data, isLoading } = useQuery({ 113 | queryKey: userKeys.profile(userId), 114 | queryFn: () => fetchUserProfile(userId), 115 | }); 116 | 117 | // ... 118 | } 119 | ``` 120 | 121 | ## 🚀 Advanced Patterns 122 | 123 | #### Key Composition 124 | 125 | ```ts 126 | export const extendedPostKeys = QueryKeyManager.create("post.extended", { 127 | withAuthor: (postId: string, authorId: string) => [ 128 | ...postKeys.detail(postId)(), 129 | "author", 130 | ...userKeys.profile(authorId)(), 131 | ], 132 | }); 133 | ``` 134 | 135 | #### Dependent Keys 136 | 137 | ```ts 138 | export const dashboardKeys = QueryKeyManager.create("dashboard", { 139 | summary: (userId: string) => [ 140 | "dashboard", 141 | "summary", 142 | ...userKeys.profile(userId)(), 143 | ...postKeys.list({ category: "featured" })(), 144 | ], 145 | }); 146 | ``` 147 | 148 | ## 🔑 Key Features 149 | 150 | 1. **Duplicate Key Prevention** 151 | 152 | - Compile-time: TypeScript errors if you try to redeclare a key name. 153 | 154 | - Runtime (dev): Throws if duplicate keys are detected. 155 | 156 | 2. **Full Type Inference** 157 | 158 | - Function arguments are strictly typed. 159 | 160 | - Nested namespaces preserve their type signatures. 161 | 162 | 3. **Performance** 163 | 164 | - Zero-cost abstractions — all type checks vanish after compilation. 165 | 166 | - Minimal object structure for fast inference. 167 | 168 | ## 🧭 Migration Utility 169 | 170 | For migrating legacy keys safely: 171 | 172 | ```ts 173 | import { migrateLegacyKeys } from "react-query-key-manager"; 174 | import { userKeys } from "./queryKeys"; 175 | 176 | const legacyUserKey = migrateLegacyKeys("oldUserKey", (userId: string) => 177 | userKeys.profile(userId)() 178 | ); 179 | ``` 180 | 181 | ## 📌 Benefits Recap 182 | 183 | - **Zero Runtime Overhead** — Pure TypeScript types. 184 | 185 | - **Instant Editor Feedback** — Type errors as you type. 186 | 187 | - **Scalable Organization** — Namespaced, nested keys. 188 | 189 | - **Collision Protection** — Compile + runtime safety. 190 | 191 | - **Discoverability** — getQueryKeys() shows all keys. 192 | 193 | ## 🪶 Lightweight by Design 194 | 195 | You can: 196 | 197 | - **Install**: pnpm add react-query-key-manager 198 | 199 | - **Or copy-paste** the implementation into your project directly from `src/index.ts` 200 | --------------------------------------------------------------------------------