Skip to content

Managing React Query Keys: Challenges and Solutions

React Query (TanStack Query) has become the de facto standard for data fetching in React applications. While it's incredibly powerful, managing query keys can quickly become a pain point as your application grows. In this blog post, we'll explore the common challenges developers face and how awesome-key-factory provides an elegant solution.

The Challenges

1. Inconsistent Key Formats

Without a structured approach, query keys can become inconsistent across your codebase:

typescript
// Different formats scattered throughout your app
useQuery({ queryKey: ['users'] })
useQuery({ queryKey: ['user', userId] })
useQuery({ queryKey: ['users', 'list'] })
useQuery({ queryKey: ['user-detail', id] })
useQuery({ queryKey: ['users', userId, 'posts'] })

This inconsistency makes it difficult to:

  • Understand the key structure at a glance
  • Invalidate related queries
  • Maintain and refactor code

2. Typos and Runtime Errors

Query keys are just arrays of strings, which means typos won't be caught until runtime:

typescript
// Oops! Typo in the key
useQuery({ queryKey: ['usres', userId] }) // ❌ Cache miss!

// Later, trying to invalidate
queryClient.invalidateQueries({ 
  queryKey: ['users', userId] // ❌ Won't invalidate the query above
})

These typos lead to:

  • Cache misses and unnecessary refetches
  • Stale data that doesn't get invalidated
  • Hard-to-debug issues

3. No Type Safety

TypeScript can't help you with plain string arrays:

typescript
// No autocomplete, no type checking
const key = ['users', userId, 'posts', postId] // 😕

// What keys exist? What parameters do they need?
// You have to remember or look it up every time

4. Difficult Refactoring

When you need to change a key structure, you have to:

  • Find all occurrences manually (grep/search)
  • Update each one individually
  • Hope you didn't miss any
  • Risk breaking the cache

5. Complex Nested Structures

As your app grows, you might need deeply nested keys:

typescript
// Managing API versioning
['api', 'v1', 'users', userId, 'posts', postId, 'comments']

// Managing different views
['app', 'dashboard', 'analytics', 'revenue', dateRange]

// Managing filters and pagination
['products', 'list', { category: 'electronics', page: 1 }]

Keeping track of these manually is error-prone and tedious.

The Solution: awesome-key-factory

awesome-key-factory solves all these challenges by providing a type-safe, hierarchical key factory for React Query keys.

Key Benefits

Type Safety - Get compile-time errors for typos and incorrect usage
Autocomplete - Full IDE support with intelligent suggestions
Consistency - Enforced structure keeps keys organized
Easy Refactoring - Change keys in one place, TypeScript catches all usages
Flexibility - Support for nested structures, parameters, and array shorthand

How to Use It

Basic Setup

First, install the library:

bash
yarn add awesome-key-factory

Then, create your key factory:

typescript
import { createKeyFactory } from 'awesome-key-factory';

const queryKeys = createKeyFactory('app', {
  users: {
    all: () => [],
    detail: (params: { id: string }) => [params.id],
    posts: (params: { userId: string }) => [params.userId, 'posts'],
  },
  posts: {
    all: () => [],
    detail: (params: { id: string }) => [params.id],
  },
});

Using in Queries

Now you can use these keys with full type safety:

typescript
import { useQuery } from '@tanstack/react-query';

function UserDetail({ userId }: { userId: string }) {
  // ✅ Type-safe, autocomplete works!
  const { data } = useQuery({
    queryKey: queryKeys.users.detail({ id: userId }),
    queryFn: () => fetchUser(userId),
  });

  // ✅ TypeScript knows this key exists
  const { data: posts } = useQuery({
    queryKey: queryKeys.users.posts({ userId }),
    queryFn: () => fetchUserPosts(userId),
  });

  return <div>{/* ... */}</div>;
}

Using in Mutations

Invalidate queries with confidence:

typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';

function useCreatePost() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: createPost,
    onSuccess: (data) => {
      // ✅ Type-safe invalidation
      queryClient.invalidateQueries({
        queryKey: queryKeys.posts.all(),
      });
      
      // ✅ TypeScript ensures the parameters match
      queryClient.invalidateQueries({
        queryKey: queryKeys.users.posts({ userId: data.userId }),
      });
    },
  });
}

Array Shorthand

For static keys, use the array shorthand:

typescript
const queryKeys = createKeyFactory('shop', {
  products: {
    list: ['all'], // ✅ Equivalent to () => ['all']
    featured: ['featured', 'items'],
  },
});

// Usage
queryKeys.products.list() // => ['shop', 'products', 'list', 'all']

Deep Nesting

Handle complex nested structures easily:

typescript
const queryKeys = createKeyFactory('api', {
  v1: {
    users: {
      posts: {
        comments: (params: { postId: string }) => [params.postId],
      },
    },
  },
});

// ✅ Access any level
queryKeys.v1.users.posts.comments({ postId: '123' })
// => ['api', 'v1', 'users', 'posts', 'comments', '123']

// ✅ Or get intermediate paths
queryKeys.v1.users.posts() // => ['api', 'v1', 'users', 'posts']
queryKeys.v1.users() // => ['api', 'v1', 'users']

Real-World Example

Here's a complete example for an e-commerce application:

typescript
const queryKeys = createKeyFactory('shop', {
  products: {
    all: () => [],
    list: (params: { 
      category?: string
      page?: number
      sort?: 'price' | 'rating'
    }) => {
      const keys: string[] = [];
      if (params.category) keys.push('category', params.category);
      if (params.page) keys.push('page', params.page.toString());
      if (params.sort) keys.push('sort', params.sort);
      return keys;
    },
    detail: (params: { id: string }) => [params.id],
    featured: ['featured'],
  },
  cart: {
    items: () => [],
    count: () => ['count'],
  },
  orders: {
    all: () => [],
    detail: (params: { id: string }) => [params.id],
    byUser: (params: { userId: string }) => [params.userId],
  },
});

// Usage
useQuery({
  queryKey: queryKeys.products.list({ 
    category: 'electronics',
    page: 1,
    sort: 'price'
  }),
  queryFn: () => fetchProducts({ category: 'electronics', page: 1, sort: 'price' }),
});

// Invalidate all product queries
queryClient.invalidateQueries({
  queryKey: queryKeys.products(),
});

Migration Guide

If you're already using React Query, migrating to awesome-key-factory is straightforward:

Step 1: Create Your Key Factory

Identify all your query keys and organize them into a factory:

typescript
// Before
const userKey = ['users', userId];
const postsKey = ['users', userId, 'posts'];

// After
const queryKeys = createKeyFactory('app', {
  users: {
    detail: (params: { id: string }) => [params.id],
    posts: (params: { userId: string }) => [params.userId, 'posts'],
  },
});

Step 2: Replace Key Usage

Use find-and-replace to update your queries:

typescript
// Before
useQuery({ queryKey: ['users', userId] })

// After
useQuery({ queryKey: queryKeys.users.detail({ id: userId }) })

Step 3: Update Invalidations

Update your query invalidations:

typescript
// Before
queryClient.invalidateQueries({ queryKey: ['users'] })

// After
queryClient.invalidateQueries({ queryKey: queryKeys.users() })

TypeScript will help you catch any missed updates!

Best Practices

  1. Centralize Your Factory - Create one key factory per application or feature module
  2. Use Descriptive Names - Make your key structure self-documenting
  3. Group Related Keys - Use nesting to group related queries together
  4. Type Your Parameters - Always type your function parameters for better safety
  5. Use Array Shorthand Sparingly - Prefer functions for flexibility, use arrays only for truly static keys

Conclusion

Managing React Query keys doesn't have to be a pain. With awesome-key-factory, you get:

  • Type safety that catches errors at compile time
  • Autocomplete that makes development faster
  • Consistency that makes your codebase easier to understand
  • Maintainability that makes refactoring safe and easy

Start using awesome-key-factory today and experience the difference type-safe key management can make!

Next Steps


Ready to get started? Install awesome-key-factory today:

bash
yarn add awesome-key-factory

Released under the MIT License.