Separating queryKey and queryFn can cause unexpected runtime issues when the same query key is accidentally used with more than one queryFn. Wrapping them in queryOptions (or infiniteQueryOptions) co-locates the key and function, making queries safer and easier to reuse.
Examples of incorrect code for this rule:
/* eslint "@tanstack/query/prefer-query-options": "error" */
function Component({ id }) {
const query = useQuery({
queryKey: ['get', id],
queryFn: () => Api.get(`/foo/${id}`),
})
// ...
}
/* eslint "@tanstack/query/prefer-query-options": "error" */
function Component({ id }) {
const query = useQuery({
queryKey: ['get', id],
queryFn: () => Api.get(`/foo/${id}`),
})
// ...
}
/* eslint "@tanstack/query/prefer-query-options": "error" */
function useFooQuery(id) {
return useQuery({
queryKey: ['get', id],
queryFn: () => Api.get(`/foo/${id}`),
})
}
/* eslint "@tanstack/query/prefer-query-options": "error" */
function useFooQuery(id) {
return useQuery({
queryKey: ['get', id],
queryFn: () => Api.get(`/foo/${id}`),
})
}
Examples of correct code for this rule:
/* eslint "@tanstack/query/prefer-query-options": "error" */
function getFooOptions(id) {
return queryOptions({
queryKey: ['get', id],
queryFn: () => Api.get(`/foo/${id}`),
})
}
function Component({ id }) {
const query = useQuery(getFooOptions(id))
// ...
}
/* eslint "@tanstack/query/prefer-query-options": "error" */
function getFooOptions(id) {
return queryOptions({
queryKey: ['get', id],
queryFn: () => Api.get(`/foo/${id}`),
})
}
function Component({ id }) {
const query = useQuery(getFooOptions(id))
// ...
}
/* eslint "@tanstack/query/prefer-query-options": "error" */
function getFooOptions(id) {
return queryOptions({
queryKey: ['get', id],
queryFn: () => Api.get(`/foo/${id}`),
})
}
function useFooQuery(id) {
return useQuery({ ...getFooOptions(id), select: (data) => data.foo })
}
/* eslint "@tanstack/query/prefer-query-options": "error" */
function getFooOptions(id) {
return queryOptions({
queryKey: ['get', id],
queryFn: () => Api.get(`/foo/${id}`),
})
}
function useFooQuery(id) {
return useQuery({ ...getFooOptions(id), select: (data) => data.foo })
}
The rule also enforces reusing queryKey from a queryOptions result instead of typing it manually in QueryClient methods or filters.
Examples of incorrect queryKey references for this rule:
/* eslint "@tanstack/query/prefer-query-options": "error" */
function todoOptions(id) {
return queryOptions({
queryKey: ['todo', id],
queryFn: () => api.getTodo(id),
})
}
function Component({ id }) {
const queryClient = useQueryClient()
return queryClient.getQueryData(['todo', id])
}
/* eslint "@tanstack/query/prefer-query-options": "error" */
function todoOptions(id) {
return queryOptions({
queryKey: ['todo', id],
queryFn: () => api.getTodo(id),
})
}
function Component({ id }) {
const queryClient = useQueryClient()
return queryClient.getQueryData(['todo', id])
}
/* eslint "@tanstack/query/prefer-query-options": "error" */
function todoOptions(id) {
return queryOptions({
queryKey: ['todo', id],
queryFn: () => api.getTodo(id),
})
}
function Component({ id }) {
const queryClient = useQueryClient()
return queryClient.invalidateQueries({ queryKey: ['todo', id] })
}
/* eslint "@tanstack/query/prefer-query-options": "error" */
function todoOptions(id) {
return queryOptions({
queryKey: ['todo', id],
queryFn: () => api.getTodo(id),
})
}
function Component({ id }) {
const queryClient = useQueryClient()
return queryClient.invalidateQueries({ queryKey: ['todo', id] })
}
Examples of correct queryKey references for this rule:
/* eslint "@tanstack/query/prefer-query-options": "error" */
function todoOptions(id) {
return queryOptions({
queryKey: ['todo', id],
queryFn: () => api.getTodo(id),
})
}
function Component({ id }) {
const queryClient = useQueryClient()
return queryClient.getQueryData(todoOptions(id).queryKey)
}
/* eslint "@tanstack/query/prefer-query-options": "error" */
function todoOptions(id) {
return queryOptions({
queryKey: ['todo', id],
queryFn: () => api.getTodo(id),
})
}
function Component({ id }) {
const queryClient = useQueryClient()
return queryClient.getQueryData(todoOptions(id).queryKey)
}
/* eslint "@tanstack/query/prefer-query-options": "error" */
function todoOptions(id) {
return queryOptions({
queryKey: ['todo', id],
queryFn: () => api.getTodo(id),
})
}
function Component({ id }) {
const queryClient = useQueryClient()
return queryClient.invalidateQueries({ queryKey: todoOptions(id).queryKey })
}
/* eslint "@tanstack/query/prefer-query-options": "error" */
function todoOptions(id) {
return queryOptions({
queryKey: ['todo', id],
queryFn: () => api.getTodo(id),
})
}
function Component({ id }) {
const queryClient = useQueryClient()
return queryClient.invalidateQueries({ queryKey: todoOptions(id).queryKey })
}
If you do not want to enforce the use of queryOptions in your codebase, you will not need this rule.