@lukemorales/query-key-factory

repository·main·Indexed 23 days ago

https://github.com/lukemorales/query-key-factory

A library for creating standardized, typesafe query and mutation keys for @tanstack/query. It provides tools like createQueryKeyStore and createQueryKeys to define keys, query functions, and contextual sub-queries in a centralized structure with full TypeScript autocomplete. Features include support for dynamic keys, mergeQueryKeys for feature-based organization, and utility types like inferQueryKeyStore and TypedUseQueryOptions for maintaining type safety across cache management and hook options.

Tokens
4.1K
Snippets
8
Records
22
Agent score
81%

What's inside @lukemorales/query-key-factory

  1. Accessing serializable key scopes with _def and queryKey

    main

    The library provides two ways to access the underlying key arrays for cache management (like invalidation):

    • queryKey: Returns the full, specific array for a particular query instance (including all dynamic parameters).
    • _def: Returns the base definition array for a query type, useful for invalidating all queries of that type (e.g., invalidating all 'list' queries regardless of filters).
  2. How contextual queries work with contextQueries

    main

    You can declare queries that are dependent on a parent context using the contextQueries property. These sub-queries are accessible via the _ctx property on the parent query object. The library automatically handles the key nesting (e.g., appending the sub-query key to the parent key).

    export const users = createQueryKeys('users', {
      detail: (userId: string) => ({
        queryKey: [userId],
        queryFn: () => api.getUser(userId),
        contextQueries: {
          likes: {
            queryKey: null,
            queryFn: () => api.getUserLikes(userId),
          },
        },
      }),
    });
    
    // Accessing the contextual query:
    // users.detail(userId)._ctx.likes
  3. Declare and merge query keys by feature using createQueryKeys

    main

    For larger applications, use createQueryKeys to define keys for specific features in separate files. You can then combine them into a single access point using mergeQueryKeys.

    import { createQueryKeys, mergeQueryKeys } from "@lukemorales/query-key-factory";
    
    // queries/users.ts
    export const users = createQueryKeys('users', {
      all: null,
      detail: (userId: string) => ({
        queryKey: [userId],
        queryFn: () => api.getUser(userId),
      }),
    });
    
    // queries/todos.ts
    export const todos = createQueryKeys('todos', {
      detail: (todoId: string) => [todoId],
      list: (filters: TodoFilters) => ({
        queryKey: [{ filters }],
        queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
      }),
    });
    
    // queries/index.ts
    export const queries = mergeQueryKeys(users, todos);
  4. Use QueryFactorySchema to define query keys and functions

    main

    The schema allows you to define how query keys are composed and how queryFns are attached.

    • queryKey: Defines the base key for the property.
    • queryFn: Defines the fetcher function. When provided, the factory generates objects compatible with TanStack Query's useQuery (containing both queryKey and queryFn).
    • contextQueries: Enables a hierarchical structure where sub-queries are accessible via a _ctx property on the parent object.
  5. Define a MutationFactorySchema

    main

    A MutationFactorySchema is a record used to define the structure of your mutation keys. Each property in the schema can be one of several types that determine how the resulting keys are composed:

    • null: Represents a terminal or empty property.
    • KeyTuple: A static array of keys (e.g., ['update-user']) that will be appended to the base key.
    • NullableMutationKeyRecord: An object containing a mutationKey which may be null.
    • MutationKeySchemaWithContextualMutations: A schema that defines a mutationKey and provides a contextMutations object for nested/contextual mutations.
    • $MutationFactorySchema: A schema that defines a mutationFn (the mutation function) and a mutationKey.
    • MutationFactoryWithContextualMutationsSchema: A schema that defines both a mutationFn and contextMutations.
    • MutationDynamicKey: A function that, when called, returns a schema or a key tuple, allowing for dynamic key generation based on arguments.
  6. Understand MutationFactorySchema property types

    main

    When building a MutationFactorySchema, the type of value assigned to a key determines the output shape:

    Static Properties

    If a property is a KeyTuple or a MutationKeyRecord, the factory generates a static object containing the composed mutationKey.

    Functional (Dynamic) Properties

    If a property is a MutationDynamicKey (a function), the factory generates a function. When you call this function with arguments, it returns the mutation options or keys based on the function's return value.

    Contextual Mutations

    If a property includes contextMutations, the resulting output will include a _ctx object. This object provides access to nested mutation factories that are scoped to the current key context.

  7. Understand the QueryFactorySchema structure

    main

    A QueryFactorySchema is the core configuration object used to define your query keys and their associated logic. It is a Record<string, FactoryProperty | DynamicKey> where each key represents a named query or a group of queries.

    Properties in the schema can be:

    • Static keys: A KeyTuple (e.g., ['users']) that defines a fixed part of the query key.
    • Query functions: A $QueryFactorySchema which includes a queryFn to automatically generate queryOptions (containing both queryKey and queryFn).
    • Contextual queries: A KeySchemaWithContextualQueries which provides a contextQueries object, allowing you to nest related queries under a _ctx property.
    • Dynamic keys: A function (DynamicKey) that accepts arguments and returns a new schema or key tuple, allowing for parameterized queries (e.g., user(id => ({ queryKey: ['user', id] }))).
  8. Declare query keys in a single file using createQueryKeyStore

    main

    For smaller applications or simpler setups, you can define your entire query key store in one place using createQueryKeyStore. This allows you to group related queries under top-level keys.

    import { createQueryKeyStore } from "@lukemorales/query-key-factory";
    
    export const queries = createQueryKeyStore({
      users: {
        all: null,
        detail: (userId: string) => ({
          queryKey: [userId],
          queryFn: () => api.getUser(userId),
        }),
      },
      todos: {
        detail: (todoId: string) => [todoId],
        list: (filters: TodoFilters) => ({
          queryKey: [{ filters }],
          queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
          contextQueries: {
            search: (query: string, limit = 15) => ({
              queryKey: [query, limit],
              queryFn: (ctx) => api.getSearchTodos({
                page: ctx.pageParam,
                filters,
                limit,
                query,
              }),
            }),
          },
        }),
      },
    });
  9. Infer query key types for TypeScript safety

    main

    Use inferQueryKeyStore (for stores created with createQueryKeyStore) or inferQueryKeys (for features created with createQueryKeys) to extract types from your definitions. This enables full autocomplete and type safety when using the keys in your application.

    // For a merged store
    import { mergeQueryKeys, inferQueryKeyStore } from "@lukemorales/query-key-factory";
    import { users } from './users';
    import { todos } from './todos';
    
    export const queries = mergeQueryKeys(users, todos);
    export type QueryKeys = inferQueryKeyStore<typeof queries>;
    
    // For a single feature
    import { createQueryKeys, inferQueryKeys } from "@lukemorales/query-key-factory";
    export const todos = createQueryKeys('todos', { /* ... */ });
    export type TodosKeys = inferQueryKeys<typeof todos>;
  10. Type the QueryFunctionContext in queryFn

    main

    To get accurate types for the ctx argument inside your queryFn, you can extract the type of the queryKey from your inferred keys and pass it to QueryFunctionContext.

    import type { QueryFunctionContext } from '@tanstack/react-query';
    import type { QueryKeys } from "../queries";
    
    type TodosList = QueryKeys['todos']['list'];
    
    const fetchTodos = async (ctx: QueryFunctionContext<TodosList['queryKey']>) => {
      // ctx.queryKey is now typed based on the definition
      const [, , { filters }] = ctx.queryKey;
      return api.getTodos({ filters, page: ctx.pageParam });
    };
  11. Infer type-safe UseQueryOptions with TypedUseQueryOptions

    main

    The TypedUseQueryOptions utility type allows you to automatically derive the correct UseQueryOptions from a query options object or a generator function. This ensures that the queryKey and the data returned by the queryFn are correctly typed when passed to TanStack Query's useQuery hook.

    It supports two input formats:

    1. A direct options object: An object containing queryKey and queryFn.
    2. A generator function: A function that, when called, returns an options object.

    This is particularly useful when building custom query factories to maintain end-to-end type safety from your key definitions to your component hooks.

    export type TypedUseQueryOptions<
      Options extends LooseQueryOptionsStruct | LooseQueryOptionsStructGenerator,
      Data = Options extends LooseQueryOptionsStructGenerator ? Awaited<ReturnType<ReturnType<Options>['queryFn']>>
      : Options extends LooseQueryOptionsStruct ? Awaited<ReturnType<Options['queryFn']>>
      : never,
    > =
      Options extends LooseQueryOptionsStructGenerator ?
        UseQueryOptions<Awaited<ReturnType<ReturnType<Options>['queryFn']>>, unknown, Data, ReturnType<Options>['queryKey']>
      : Options extends LooseQueryOptionsStruct ?
        UseQueryOptions<Awaited<ReturnType<Options['queryFn']>>, unknown, Data, Options['queryKey']>
      : never;