Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/prefer-query-options-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/eslint-plugin-query': minor
---

Add `prefer-query-options` rule and `recommendedStrict` config
4 changes: 4 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1293,6 +1293,10 @@
{
"label": "Mutation Property Order",
"to": "eslint/mutation-property-order"
},
{
"label": "Prefer Query Options",
"to": "eslint/prefer-query-options"
}
]
},
Expand Down
24 changes: 24 additions & 0 deletions docs/eslint/eslint-plugin-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ export default [
]
```

### Recommended strict setup

The `flat/recommended-strict` config extends `flat/recommended` with additional opinionated rules that enforce best practices more aggressively.

```js
import pluginQuery from '@tanstack/eslint-plugin-query'

export default [
...pluginQuery.configs['flat/recommended-strict'],
// Any other config...
]
```

### Custom setup

Alternatively, you can load the plugin and configure only the rules you want to use:
Expand Down Expand Up @@ -78,6 +91,16 @@ To enable all of the recommended rules for our plugin, add `plugin:@tanstack/que
}
```

### Recommended strict setup

The `recommendedStrict` config extends `recommended` with additional opinionated rules:

```json
{
"extends": ["plugin:@tanstack/query/recommendedStrict"]
}
```

### Custom setup

Alternatively, add `@tanstack/query` to the plugins section, and configure the rules you want to use:
Expand All @@ -100,3 +123,4 @@ Alternatively, add `@tanstack/query` to the plugins section, and configure the r
- [@tanstack/query/infinite-query-property-order](./infinite-query-property-order.md)
- [@tanstack/query/no-void-query-fn](./no-void-query-fn.md)
- [@tanstack/query/mutation-property-order](./mutation-property-order.md)
- [@tanstack/query/prefer-query-options](./prefer-query-options.md)
145 changes: 145 additions & 0 deletions docs/eslint/prefer-query-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
id: prefer-query-options
title: 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}`),
})
// ...
}
```

```tsx
/* 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))
// ...
}
Comment thread
TkDodo marked this conversation as resolved.
```

```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 })
}
```

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])
}
```

```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] })
}
```

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)
}
```

```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 })
}
```

## 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

- [x] βœ… Recommended (strict)
- [ ] πŸ”§ Fixable
Loading
Loading