Framework
Version

Prefer the use of queryOptions

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.

Rule Details

Examples of incorrect code for this rule:

tsx
/* 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}`),
  })
  // ...
}
tsx
/* 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:

tsx
/* 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))
  // ...
}
tsx
/* 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:

tsx
/* 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])
}
tsx
/* 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:

tsx
/* 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)
}
tsx
/* 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 })
}

When Not To Use It

If you do not want to enforce the use of queryOptions in your codebase, you will not need this rule.

Attributes

  • ✅ Recommended (strict)
  • 🔧 Fixable