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.
Query Key Factory is available as a package on NPM, install with your favorite package manager:
npm install @lukemorales/query-key-factory
Start by defining the query keys for the features of your app:
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,
}),
}),
},
}),
},
});
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,
});
},
});
};
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(),
}
});
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));
};
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);
};
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']
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 }),
}),
},
});
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);
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.
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>;
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>;
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,
});
};