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";
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";
export const users = createQueryKeys('users', {
all: null,
detail: (userId: string) => ({
queryKey: [userId],
queryFn: () => api.getUser(userId),
}),
});
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,
}),
}),
},
}),
});
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);
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 }],
}),
});
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),
}),
});
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),
},
},
}),
});
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._def;
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:
export const users = createQueryKeys('users', {
all: null,
detail: (userId: string) => ({
queryKey: [userId],
queryFn: () => api.getUser(userId),
}),
});
export const todos = createQueryKeys('todos', {
detail: (todoId: string) => [todoId],
list: (filters: TodoFilters) => ({
queryKey: [{ filters }],
queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
}),
});
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>;
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";
type TodosList = QueryKeys['todos']['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,
});
};