Query Key Factory
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 your prefer to declare everything in one file
export const queryKeys = createQueryKeyStore({
users: null,
todos: {
list: (filters: TodoFilters) => ({ filters }),
search: (query: string, limit = 15) => [query, limit],
todo: (todoId: string) => todoId,
},
});
Fine-grained declaration colocated by features
import { createQueryKeys, mergeQueryKeys } from "@lukemorales/query-key-factory";
// queries/users.ts
export const usersKeys = createQueryKeys('users');
// queries/todos.ts
export const todosKeys = createQueryKeys('todos', {
list: (filters: TodoFilters) => ({ filters }),
search: (query: string, limit = 15) => [query, limit],
todo: (todoId: string) => todoId,
});
// queries/index.ts
export const queryKeys = mergeQueryKeys(usersKeys, todosKeys);
Use throughout your codebase as the single source for writing the query keys for your cache management:
import { queryKeys } from '../queries';
export function useUsers() {
return useQuery(queryKeys.users._def, fetchUsers);
};
import { queryKeys } from '../queries';
export function useTodos(filters: TodoFilters) {
return useQuery(queryKeys.todos.list(filters), fetchTodos);
};
export function useSearchTodos(query: string, limit = 15) {
return useQuery(queryKeys.todos.search(query, limit), fetchSearchTodos, {
enabled: Boolean(query),
});
};
export function useUpdateTodo() {
const queryClient = useQueryClient();
return useMutation(updateTodo, {
onSuccess(newTodo) {
queryClient.setQueryData(queryKeys.todos.todo(newTodo.id), newTodo);
// invalidate all the list queries
queryClient.invalidateQueries({
queryKey: queryKeys.todos.list._def,
refetchActive: false,
});
},
});
};
๐
Features
Standardized keys
All keys generated follow the @tanstack/query standard of being an array at top level, including keys with serializable objects:
export const todosKeys = createQueryKeys('todos', {
list: (filters: TodoFilters) => ({ filters }),
search: (query: string, limit = 15) => [query, limit],
todo: (todoId: string) => todoId,
});
// => createQueryKeys output:
// {
// _def: ['todos'],
// list: (filters: TodoFilters) => ['todos', 'list', { filters }],
// search: (query: string, limit = 15) => ['todos', 'search', query, limit],
// todo: (todoId: string) => ['todos', 'todo', todoId],
// }
Access to serializable keys scope definition
Easy way to access the serializable key scope and invalidade all cache for that context:
todosKeys.list({ status: 'completed' }) // => ['todos', 'list', { status: 'completed' }]
todosKeys.list._def; // => ['todos', 'list']
todosKeys.search('tanstack query', 15); // => ['todos', 'search', 'tanstack query', 15]
todosKeys.search._def; // => ['todos', 'search']
todosKeys.todo('todo-id'); // => ['todos', 'todo', 'todo-id']
todosKeys.todo._def; // => ['todos', 'todo']
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 queryKeys = createQueryKeyStore({
users: null,
todos: {
list: (filters: TodoFilters) => ({ filters }),
search: (query: string, limit = 15) => [query, limit],
todo: (todoId: string) => todoId,
},
});
// => createQueryKeyStore output:
// {
// users: {
// _def: ['users'],
// },
// todos: {
// _def: ['todos'],
// list: (filters: TodoFilters) => ['todos', 'list', { filters }],
// search: (query: string, limit = 15) => ['todos', 'search', query, limit],
// todo: (todoId: string) => ['todos', 'todo', todoId],
// },
// };
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
const usersKeys = createQueryKeys('users');
// queries/todos.ts
const todosKeys = createQueryKeys('todos', {
list: (filters: TodoFilters) => ({ filters }),
search: (query: string, limit = 15) => [query, limit],
todo: (todoId: string) => todoId,
});
// queries/index.ts
export const queryKeys = mergeQueryKeys(usersKeys, todosKeys);
// => mergeQueryKeys output:
// {
// users: {
// _def: ['users'],
// },
// todos: {
// _def: ['todos'],
// list: (filters: TodoFilters) => ['todos', 'list', { filters }],
// search: (query: string, limit = 15) => ['todos', 'search', query, limit],
// todo: (todoId: string) => ['todos', 'todo', todoId],
// },
// };
Type safety and smart autocomplete
Typescript is a first class citizen of the Query Key Factory lib, 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 mouseover and your IDE will show you all information you need to know.
Infer the type of the store's query keys
import { createQueryKeyStore, inferQueryKeyStore } from "@lukemorales/query-key-factory";
export const queryKeys = createQueryKeyStore({
users: null,
todos: {
list: (filters: TodoFilters) => ({ filters }),
search: (query: string, limit = 15) => [query, limit],
todo: (todoId: string) => todoId,
},
});
export type QueryKeys = inferQueryKeyStore<typeof queryKeys>;
// => QueryKeys type:
// {
// users: {
// _def: readonly ['users'];
// };
// todos: {
// _def: readonly ['todos'];
// list: readonly ['todos', 'list', { filters: TodoFilters }];
// search: readonly ['todos', 'search', string, number];
// todo: readonly ['todos', 'todo', string];
// };
// }
import { createQueryKeys, inferQueryKeyStore } from "@lukemorales/query-key-factory";
// queries/users.ts
const usersKeys = createQueryKeys('users');
// queries/todos.ts
const todosKeys = createQueryKeys('todos', {
list: (filters: TodoFilters) => ({ filters }),
search: (query: string, limit = 15) => [query, limit],
todo: (todoId: string) => todoId,
});
// queries/index.ts
export const queryKeys = mergeQueryKeys(usersKeys, todosKeys);
export type QueryKeys = inferQueryKeyStore<typeof queryKeys>;
Infer the type of a feature's query keys
import { createQueryKeys, inferQueryKeys } from "@lukemorales/query-key-factory";
export const todosKeys = createQueryKeys('todos', {
list: (filters: TodoFilters) => ({ filters }),
search: (query: string, limit = 15) => [query, limit],
todo: (todoId: string) => todoId,
});
export type TodosKeys = inferQueryKeys<typeof todosKeys>;
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 TodosListQueryKey = QueryKeys['todos']['list'] | TodosKeys['list'];
const fetchTodos = async (ctx: QueryFunctionContext<TodosListQueryKey>) => {
const [, , { filters }] = ctx.queryKey; // readonly ['todos', 'list', { filters }]
const search = new URLSearchParams(filters);
return fetch(`${BASE_URL}/todos?${search.toString()}`).then(data => data.json());
}
export function useTodos(filters: TodoFilters) {
return useQuery(queryKeys.todos.list(filters), fetchTodos);
};