Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

@terrxo/query-key-factory

lukemorales300MIT1.3.7TypeScript support: included

A library for creating standardized query keys, useful for cache management in @tanstack/query

react, react-query, tanstack, tanstack-query, cache, query, query-keys

readme

Factory emoji

Query Key Factory

Latest build Latest published version Bundlephobia Tree shaking available Types included License Number of downloads GitHub Stars

Typesafe query key management for @tanstack/query with auto-completion features.

Focus on writing and invalidating queries without the hassle of remembering
how you've set up a key for a specific query! This lib will take care of the rest.

📦 Install

Query Key Factory is available as a package on NPM, install with your favorite package manager:

npm install @lukemorales/query-key-factory

⚡ Quick start

Start by defining the query keys for the features of your app:

Declare your store in a single file

import { createQueryKeyStore } from "@lukemorales/query-key-factory";

// if you prefer to declare everything in one file
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,
          }),
        }),
      },
    }),
  },
});

Fine-grained declaration colocated by features

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 }),
    contextQueries: {
      search: (query: string, limit = 15) => ({
        queryKey: [query, limit],
        queryFn: (ctx) => api.getSearchTodos({
          page: ctx.pageParam,
          filters,
          limit,
          query,
        }),
      }),
    },
  }),
});

// queries/index.ts
export const queries = mergeQueryKeys(users, todos);

Use throughout your codebase as the single source for writing the query keys, or even the complete queries for your cache management:

import { queries } from '../queries';

export function useUsers() {
  return useQuery({
    ...queries.users.all,
    queryFn: () => api.getUsers(),
  });
};

export function useUserDetail(id: string) {
  return useQuery(queries.users.detail(id));
};
import { queries } from '../queries';

export function useTodos(filters: TodoFilters) {
  return useQuery(queries.todos.list(filters));
};

export function useSearchTodos(filters: TodoFilters, query: string, limit = 15) {
  return useQuery({
    ...queries.todos.list(filters)._ctx.search(query, limit),
    enabled: Boolean(query),
  });
};

export function useUpdateTodo() {
  const queryClient = useQueryClient();

  return useMutation(updateTodo, {
    onSuccess(newTodo) {
      queryClient.setQueryData(queries.todos.detail(newTodo.id).queryKey, newTodo);

      // invalidate all the list queries
      queryClient.invalidateQueries({
        queryKey: queries.todos.list._def,
        refetchActive: false,
      });
    },
  });
};

📝 Features

Standardized keys

All keys generated follow the @tanstack/query convention of being an array at top level, including keys with serializable objects:

export const todos = createQueryKeys('todos', {
  detail: (todoId: string) => [todoId],
  list: (filters: TodoFilters) => ({
    queryKey: [{ filters }],
  }),
});

// => createQueryKeys output:
// {
//   _def: ['todos'],
//   detail: (todoId: string) => {
//     queryKey: ['todos', 'detail', todoId],
//   },
//   list: (filters: TodoFilters) => {
//     queryKey: ['todos', 'list', { filters }],
//   },
// }

queryKey can be optional when there's no need for a dynamic query:

export const users = createQueryKeys('users', {
  list: {
    queryKey: null,
    queryFn: () => api.getUsers(),
  }
});

Generate the query options you need to run useQuery

Declare your queryKey and your queryFn together, and have easy access to everything you need to run a query:

export const users = createQueryKeys('users', {
  detail: (userId: string) => ({
    queryKey: [userId],
    queryFn: () => api.getUser(userId),
  }),
});

// => createQueryKeys output:
// {
//   _def: ['users'],
//   detail: (userId: string) => {
//     queryKey: ['users', 'detail', userId],
//     queryFn: (ctx: QueryFunctionContext) => api.getUser(userId),
//   },
// }

export function useUserDetail(id: string) {
  return useQuery(users.detail(id));
};

Generate contextual queries

Declare queries that are dependent or related to a parent context (e.g.: all likes from a user):

export const users = createQueryKeys('users', {
  detail: (userId: string) => ({
    queryKey: [userId],
    queryFn: () => api.getUser(userId),
    contextQueries: {
      likes: {
        queryKey: null,
        queryFn: () => api.getUserLikes(userId),
      },
    },
  }),
});

// => createQueryKeys output:
// {
//   _def: ['users'],
//   detail: (userId: string) => {
//     queryKey: ['users', 'detail', userId],
//     queryFn: (ctx: QueryFunctionContext) => api.getUser(userId),
//     _ctx: {
//       likes: {
//         queryKey: ['users', 'detail', userId, 'likes'],
//         queryFn: (ctx: QueryFunctionContext) => api.getUserLikes(userId),
//       },
//     },
//   },
// }

export function useUserLikes(userId: string) {
  return useQuery(users.detail(userId)._ctx.likes);
};

Access to serializable keys scope definition

Easy way to access the serializable key scope and invalidate all cache for that context:

users.detail(userId).queryKey; // => ['users', 'detail', userId]
users.detail._def; // => ['users', 'detail']

Create a single point of access for all your query keys

Declare your query keys store in a single file

Just one place to edit and maintain your store:

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 }),
    }),
  },
});

Declare your query keys by feature

Have fine-grained control over your features' keys and merge them into a single object to have access to all your query keys in your codebase:

// 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);

Type safety and smart autocomplete

Typescript is a first class citizen of Query Key Factory, providing easy of use and autocomplete for all query keys available and their outputs. Don't remember if a key is serializable or the shape of a key? Just let your IDE show you all information you need.

Infer the type of the store's query keys

import { createQueryKeyStore, inferQueryKeyStore } from "@lukemorales/query-key-factory";

export const queries = createQueryKeyStore({
  /* ... */
});

export type QueryKeys = inferQueryKeyStore<typeof queries>;
// queries/index.ts
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>;

Infer the type of a feature's query keys

import { createQueryKeys, inferQueryKeys } from "@lukemorales/query-key-factory";

export const todos = createQueryKeys('todos', {
  detail: (todoId: string) => [todoId],
  list: (filters: TodoFilters) => ({
    queryKey: [{ filters }],
    queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
  }),
});

export type TodosKeys = inferQueryKeys<typeof todos>;

Type your QueryFunctionContext with ease

Get accurate types of your query keys passed to the queryFn context:

import type { QueryKeys } from "../queries";
// import type { TodosKeys } from "../queries/todos";

type TodosList = QueryKeys['todos']['list'];
// type TodosList = TodosKeys['list'];

const fetchTodos = async (ctx: QueryFunctionContext<TodosList['queryKey']>) => {
  const [, , { filters }] = ctx.queryKey;

  return api.getTodos({ filters, page: ctx.pageParam });
}

export function useTodos(filters: TodoFilters) {
  return useQuery({
    ...queries.todos.list(filters),
    queryFn: fetchTodos,
  });
};

changelog

@lukemorales/query-key-factory

1.3.4

Patch Changes

1.3.3

Patch Changes

  • 9b5401c Thanks @lukemorales! - Improve mergeQueryKeys type inference and improve type-safety for dynamic query keys

1.3.2

Patch Changes

1.3.1

Patch Changes

  • #67 e6b8389 Thanks @lukemorales! - Loosen types for TypedUseQueryOptions and allow inference of dynamic query options generation

1.3.0

Minor Changes

1.2.0

Minor Changes

Patch Changes

1.1.0

Minor Changes

Patch Changes

1.0.2

Patch Changes

1.0.1

Patch Changes

1.0.0

Major Changes

  • #20 ba47907 Thanks @lukemorales! - ## Generate query options and add support for nested keys

    New in @lukemorales/query-key-factory is support for nested keys and generation of query options, adopting the query options overload as first class citizen, in preparation for React Query v5 roadmap.

    const people = createQueryKeys("people", {
      person: (id: number) => ({
        queryKey: [id],
        queryFn: () => api.getPerson({ params: { id } }),
        contextQueries: {
          ships: {
            queryKey: null,
            queryFn: () =>
              api.getShipsByPerson({
                params: { personId: id },
              }),
          },
          film: (filmId: string) => ({
            queryKey: [filmId],
            queryFn: () =>
              api.getFilm({
                params: { id: filmId },
              }),
          }),
        },
      }),
    });

    Each entry outputs an object that can be used in the query options overload in React Query:

    console.log(people.person("person_01"));
    
    // => output:
    // {
    //   queryKey: ['people', 'person', 'person_01'],
    //   queryFn: () => api.getPerson({ params: { id: 'person_01' } }),
    //   _ctx: { ...queries declared inside "contextQueries" }
    // }

    Then you can easily just use the object in useQuery or spread it and add more query options to that observer:

    export const Person = ({ id }) => {
      const personQuery = useQuery(people.person(id));
    
      return {
        /* render person data */
      };
    };
    
    export const Ships = ({ personId }) => {
      const shipsQuery = useQuery({
        ...people.person(personId)._ctx.ships,
        enabled: !!personId,
      });
    
      return {
        /* render ships data */
      };
    };
    
    export const Film = ({ id, personId }) => {
      const filmQuery = useQuery(people.person(personId)._ctx.film(id));
    
      return {
        /* render film data */
      };
    };

    BREAKING CHANGES

    Standardized query key values

    All query key values should now be an array. Only the first level keys (those not dynamically generated) can still be declared as null, but if you want to pass a value, you will need to make it an array.

    export const todosKeys = createQueryKeys('todos', {
      mine: null,
    - all: 'all-todos',
    + all: ['all-todos'],
    - list: (filters: TodoFilters) => ({ filters }),
    + list: (filters: TodoFilters) => [{ filters }],
    - todo: (todoId: string) => todoId,
    + todo: (todoId: string) => [todoId],
    });

    Objects are now used to declare query options

    You can still use objects to compose a query key, but now they must be inside an array because plain objects are now used for the declaration of the query options:

    export const todosKeys = createQueryKeys('todos', {
    - list: (filters: TodoFilters) => ({ filters }),
    + list: (filters: TodoFilters) => ({
    +   queryKey: [{ filters }],
    + }),
    });

    Generated output for a query key is always an object

    With the new API, the output of an entry will always be an object according to what options you've declared in the factory (e.g.: if you returned an array or declared an object with only queryKey, your output will be { queryKey: [...values] }, if you also declared queryFn it will be added to that object, and contextQueries will be available inside _ctx):

    export const todosKeys = createQueryKeys('todos', {
      todo: (todoId: string) => [todoId],
      list: (filters: TodoFilters) => {
        queryKey: [{ filters }],
        queryFn: () => fetchTodosList(filters),
      },
    });
    
    - useQuery(todosKeys.todo(todoId), fetchTodo);
    + useQuery(todosKeys.todo(todoId).queryKey, fetchTodo);
    
    - useQuery(todosKeys.list(filters), fetchTodosList);
    + useQuery(todosKeys.list(filters).queryKey, todosKeys.list(filters).queryFn);
    
    // even better refactor, preparing for React Query v5
    + useQuery({
    +   ...todosKeys.todo(todoId),
    +   queryFn: fetchTodo,
    + });
    
    + useQuery(todosKeys.list(filters));

    Helper types to infer query keys in the store reflect the new output

    The helper types to infer query keys in the created store reflect the new output, to account for all possible use cases:

    type TodosKeys = inferQueryKeys<typeof todosKeys>;
    
    - type SingleTodoQueryKey = TodosKeys['todo'];
    + type SingleTodoQueryKey = TodosKeys['todo']['queryKey'];
  • #20 ba47907 Thanks @lukemorales! - ## Remove deprecated methods Since v0.6.0, the default key and and toScope method have been deprecated from the package.

    BREAKING CHANGES

    default and toScope removed from implementation

    default key and toScope method have been officially removed from the code, which means that if you try to access them, you will either receive undefined or your code will throw for trying to invoke a function on toScope that does not exist anymore.

0.6.1

Patch Changes

  • Fix query key breaking devtools because of Proxy

0.6.0

Minor Changes

  • Introduce internal _def key and deprecate default and toScope

0.5.0

Minor Changes

  • Create createQueryKeyStore to allow declaring all feature keys in one place

0.4.0

Minor Changes

  • Create mergeQueryKeys to create single access point to all query keys

0.3.2

Patch Changes

  • Add inferQueryKeys type to create interface from result of createQueryKeys

0.3.1

Patch Changes

  • Fix new types not published

0.3.0

Minor Changes

  • Allow tuples of any size in dynamic keys

0.2.1

Patch Changes

  • Removes yarn engine restriction

0.2.0

Minor Changes

  • Make serializable keys less strict