Skip to content

Commit c2e5272

Browse files
feat(eslint-plugin): no void queryFn (#8878)
* feat(eslint-plugin-query): implement no-void-query-fn rule * add docs * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 1fa539a commit c2e5272

File tree

8 files changed

+464
-0
lines changed

8 files changed

+464
-0
lines changed

docs/eslint/eslint-plugin-query.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,4 @@ Alternatively, add `@tanstack/query` to the plugins section, and configure the r
9898
- [@tanstack/query/stable-query-client](./stable-query-client.md)
9999
- [@tanstack/query/no-unstable-deps](./no-unstable-deps.md)
100100
- [@tanstack/query/infinite-query-property-order](./infinite-query-property-order.md)
101+
- [@tanstack/query/no-void-query-fn](./no-void-query-fn.md)

docs/eslint/no-void-query-fn.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
id: no-void-query-fn
3+
title: Disallow returning void from query functions
4+
---
5+
6+
Query functions must return a value that will be cached by TanStack Query. Functions that don't return a value (void functions) can lead to unexpected behavior and might indicate a mistake in the implementation.
7+
8+
## Rule Details
9+
10+
Example of **incorrect** code for this rule:
11+
12+
```tsx
13+
/* eslint "@tanstack/query/no-void-query-fn": "error" */
14+
15+
useQuery({
16+
queryKey: ['todos'],
17+
queryFn: async () => {
18+
await api.todos.fetch() // Function doesn't return the fetched data
19+
},
20+
})
21+
```
22+
23+
Example of **correct** code for this rule:
24+
25+
```tsx
26+
/* eslint "@tanstack/query/no-void-query-fn": "error" */
27+
useQuery({
28+
queryKey: ['todos'],
29+
queryFn: async () => {
30+
const todos = await api.todos.fetch()
31+
return todos
32+
},
33+
})
34+
```
35+
36+
## Attributes
37+
38+
- [x] ✅ Recommended
39+
- [ ] 🔧 Fixable
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
import path from 'node:path'
2+
import { RuleTester } from '@typescript-eslint/rule-tester'
3+
import { afterAll, describe, it } from 'vitest'
4+
import { rule } from '../rules/no-void-query-fn/no-void-query-fn.rule'
5+
import { normalizeIndent } from './test-utils'
6+
7+
RuleTester.afterAll = afterAll
8+
RuleTester.describe = describe
9+
RuleTester.it = it
10+
11+
const ruleTester = new RuleTester({
12+
languageOptions: {
13+
parser: await import('@typescript-eslint/parser'),
14+
parserOptions: {
15+
project: true,
16+
tsconfigRootDir: path.resolve(__dirname, './ts-fixture'),
17+
},
18+
},
19+
})
20+
21+
ruleTester.run('no-void-query-fn', rule, {
22+
valid: [
23+
{
24+
name: 'queryFn returns a value',
25+
code: normalizeIndent`
26+
import { useQuery } from '@tanstack/react-query'
27+
28+
function Component() {
29+
const query = useQuery({
30+
queryKey: ['test'],
31+
queryFn: () => ({ data: 'test' }),
32+
})
33+
return null
34+
}
35+
`,
36+
},
37+
{
38+
name: 'queryFn returns a Promise',
39+
code: normalizeIndent`
40+
import { useQuery } from '@tanstack/react-query'
41+
42+
function Component() {
43+
const query = useQuery({
44+
queryKey: ['test'],
45+
queryFn: async () => ({ data: 'test' }),
46+
})
47+
return null
48+
}
49+
`,
50+
},
51+
{
52+
name: 'queryFn returns Promise.resolve',
53+
code: normalizeIndent`
54+
import { useQuery } from '@tanstack/react-query'
55+
56+
function Component() {
57+
const query = useQuery({
58+
queryKey: ['test'],
59+
queryFn: () => Promise.resolve({ data: 'test' }),
60+
})
61+
return null
62+
}
63+
`,
64+
},
65+
{
66+
name: 'queryFn with explicit Promise type',
67+
code: normalizeIndent`
68+
import { useQuery } from '@tanstack/react-query'
69+
70+
interface Data {
71+
value: string
72+
}
73+
74+
function Component() {
75+
const query = useQuery({
76+
queryKey: ['test'],
77+
queryFn: async (): Promise<Data> => {
78+
return { value: 'test' }
79+
},
80+
})
81+
return null
82+
}
83+
`,
84+
},
85+
{
86+
name: 'queryFn with generic Promise type',
87+
code: normalizeIndent`
88+
import { useQuery } from '@tanstack/react-query'
89+
90+
interface Response<T> {
91+
data: T
92+
}
93+
94+
function Component() {
95+
const query = useQuery({
96+
queryKey: ['test'],
97+
queryFn: async (): Promise<Response<string>> => {
98+
return { data: 'test' }
99+
},
100+
})
101+
return null
102+
}
103+
`,
104+
},
105+
{
106+
name: 'queryFn with external async function',
107+
code: normalizeIndent`
108+
import { useQuery } from '@tanstack/react-query'
109+
110+
async function fetchData(): Promise<{ data: string }> {
111+
return { data: 'test' }
112+
}
113+
114+
function Component() {
115+
const query = useQuery({
116+
queryKey: ['test'],
117+
queryFn: fetchData,
118+
})
119+
return null
120+
}
121+
`,
122+
},
123+
{
124+
name: 'queryFn returns null',
125+
code: normalizeIndent`
126+
import { useQuery } from '@tanstack/react-query'
127+
128+
function Component() {
129+
const query = useQuery({
130+
queryKey: ['test'],
131+
queryFn: () => null,
132+
})
133+
return null
134+
}
135+
`,
136+
},
137+
{
138+
name: 'queryFn returns 0',
139+
code: normalizeIndent`
140+
import { useQuery } from '@tanstack/react-query'
141+
142+
function Component() {
143+
const query = useQuery({
144+
queryKey: ['test'],
145+
queryFn: () => 0,
146+
})
147+
return null
148+
}
149+
`,
150+
},
151+
{
152+
name: 'queryFn returns false',
153+
code: normalizeIndent`
154+
import { useQuery } from '@tanstack/react-query'
155+
156+
function Component() {
157+
const query = useQuery({
158+
queryKey: ['test'],
159+
queryFn: () => false,
160+
})
161+
return null
162+
}
163+
`,
164+
},
165+
],
166+
invalid: [
167+
{
168+
name: 'queryFn returns void',
169+
code: normalizeIndent`
170+
import { useQuery } from '@tanstack/react-query'
171+
172+
function Component() {
173+
const query = useQuery({
174+
queryKey: ['test'],
175+
queryFn: () => {
176+
console.log('test')
177+
},
178+
})
179+
return null
180+
}
181+
`,
182+
errors: [{ messageId: 'noVoidReturn' }],
183+
},
184+
{
185+
name: 'queryFn returns undefined',
186+
code: normalizeIndent`
187+
import { useQuery } from '@tanstack/react-query'
188+
189+
function Component() {
190+
const query = useQuery({
191+
queryKey: ['test'],
192+
queryFn: () => undefined,
193+
})
194+
return null
195+
}
196+
`,
197+
errors: [{ messageId: 'noVoidReturn' }],
198+
},
199+
{
200+
name: 'async queryFn returns void',
201+
code: normalizeIndent`
202+
import { useQuery } from '@tanstack/react-query'
203+
204+
function Component() {
205+
const query = useQuery({
206+
queryKey: ['test'],
207+
queryFn: async () => {
208+
await someOperation()
209+
},
210+
})
211+
return null
212+
}
213+
`,
214+
errors: [{ messageId: 'noVoidReturn' }],
215+
},
216+
{
217+
name: 'queryFn with explicit void Promise',
218+
code: normalizeIndent`
219+
import { useQuery } from '@tanstack/react-query'
220+
221+
function Component() {
222+
const query = useQuery({
223+
queryKey: ['test'],
224+
queryFn: async (): Promise<void> => {
225+
await someOperation()
226+
},
227+
})
228+
return null
229+
}
230+
`,
231+
errors: [{ messageId: 'noVoidReturn' }],
232+
},
233+
{
234+
name: 'queryFn with Promise.resolve(undefined)',
235+
code: normalizeIndent`
236+
import { useQuery } from '@tanstack/react-query'
237+
238+
function Component() {
239+
const query = useQuery({
240+
queryKey: ['test'],
241+
queryFn: () => Promise.resolve(undefined),
242+
})
243+
return null
244+
}
245+
`,
246+
errors: [{ messageId: 'noVoidReturn' }],
247+
},
248+
{
249+
name: 'queryFn with external void async function',
250+
code: normalizeIndent`
251+
import { useQuery } from '@tanstack/react-query'
252+
253+
async function voidOperation(): Promise<void> {
254+
await someOperation()
255+
}
256+
257+
function Component() {
258+
const query = useQuery({
259+
queryKey: ['test'],
260+
queryFn: voidOperation,
261+
})
262+
return null
263+
}
264+
`,
265+
errors: [{ messageId: 'noVoidReturn' }],
266+
},
267+
{
268+
name: 'queryFn with conditional return (one branch missing)',
269+
code: normalizeIndent`
270+
import { useQuery } from '@tanstack/react-query'
271+
272+
function Component() {
273+
const query = useQuery({
274+
queryKey: ['test'],
275+
queryFn: () => {
276+
if (Math.random() > 0.5) {
277+
return { data: 'test' }
278+
}
279+
// Missing return in the else case
280+
},
281+
})
282+
return null
283+
}
284+
`,
285+
errors: [{ messageId: 'noVoidReturn' }],
286+
},
287+
{
288+
name: 'queryFn with ternary operator returning undefined',
289+
code: normalizeIndent`
290+
import { useQuery } from '@tanstack/react-query'
291+
292+
function Component() {
293+
const query = useQuery({
294+
queryKey: ['test'],
295+
queryFn: () => Math.random() > 0.5 ? { data: 'test' } : undefined,
296+
})
297+
return null
298+
}
299+
`,
300+
errors: [{ messageId: 'noVoidReturn' }],
301+
},
302+
{
303+
name: 'async queryFn with try/catch missing return in catch',
304+
code: normalizeIndent`
305+
import { useQuery } from '@tanstack/react-query'
306+
307+
function Component() {
308+
const query = useQuery({
309+
queryKey: ['test'],
310+
queryFn: async () => {
311+
try {
312+
return { data: 'test' }
313+
} catch (error) {
314+
console.error(error)
315+
// No return here results in an implicit undefined
316+
}
317+
},
318+
})
319+
return null
320+
}
321+
`,
322+
errors: [{ messageId: 'noVoidReturn' }],
323+
},
324+
],
325+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// File needs to exists
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "ESNext",
5+
"moduleResolution": "Bundler",
6+
"strict": true,
7+
"skipLibCheck": true
8+
},
9+
"include": ["**/*"]
10+
}

packages/eslint-plugin-query/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Object.assign(plugin.configs, {
3030
'@tanstack/query/stable-query-client': 'error',
3131
'@tanstack/query/no-unstable-deps': 'error',
3232
'@tanstack/query/infinite-query-property-order': 'error',
33+
'@tanstack/query/no-void-query-fn': 'error',
3334
},
3435
},
3536
'flat/recommended': [
@@ -44,6 +45,7 @@ Object.assign(plugin.configs, {
4445
'@tanstack/query/stable-query-client': 'error',
4546
'@tanstack/query/no-unstable-deps': 'error',
4647
'@tanstack/query/infinite-query-property-order': 'error',
48+
'@tanstack/query/no-void-query-fn': 'error',
4749
},
4850
},
4951
],

0 commit comments

Comments
 (0)