@tanstack/react-query 4.40.1 → 4.40.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/lib/suspense.d.ts +7 -0
- package/build/lib/suspense.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/suspense.test.tsx +230 -0
- package/src/suspense.ts +11 -0
package/build/lib/suspense.d.ts
CHANGED
|
@@ -3,6 +3,13 @@ import type { QueryObserver } from '@tanstack/query-core';
|
|
|
3
3
|
import type { QueryErrorResetBoundaryValue } from './QueryErrorResetBoundary';
|
|
4
4
|
import type { QueryObserverResult } from '@tanstack/query-core';
|
|
5
5
|
import type { QueryKey } from '@tanstack/query-core';
|
|
6
|
+
/**
|
|
7
|
+
* Ensures minimum staleTime and cacheTime values when suspense is enabled.
|
|
8
|
+
* Despite the name, this function guards both staleTime and cacheTime to prevent
|
|
9
|
+
* infinite re-render loops with synchronous queries.
|
|
10
|
+
*
|
|
11
|
+
* @deprecated in v5 - replaced by ensureSuspenseTimers
|
|
12
|
+
*/
|
|
6
13
|
export declare const ensureStaleTime: (defaultedOptions: DefaultedQueryObserverOptions<any, any, any, any, any>) => void;
|
|
7
14
|
export declare const willFetch: (result: QueryObserverResult<any, any>, isRestoring: boolean) => boolean;
|
|
8
15
|
export declare const shouldSuspend: (defaultedOptions: DefaultedQueryObserverOptions<any, any, any, any, any> | undefined, result: QueryObserverResult<any, any>, isRestoring: boolean) => boolean | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suspense.d.ts","sourceRoot":"","sources":["../../src/suspense.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAA;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAA;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAEpD,eAAO,MAAM,eAAe,qBACR,8BAA8B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"suspense.d.ts","sourceRoot":"","sources":["../../src/suspense.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAA;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAA;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAEpD;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,qBACR,8BAA8B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,SAazE,CAAA;AAED,eAAO,MAAM,SAAS,WACZ,oBAAoB,GAAG,EAAE,GAAG,CAAC,eACxB,OAAO,YACoC,CAAA;AAE1D,eAAO,MAAM,aAAa,qBAEpB,8BAA8B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GACtD,SAAS,UACL,oBAAoB,GAAG,EAAE,GAAG,CAAC,eACxB,OAAO,wBAC2C,CAAA;AAEjE,eAAO,MAAM,eAAe,8QAeN,4BAA4B,kBAY5C,CAAA"}
|
package/package.json
CHANGED
|
@@ -1238,3 +1238,233 @@ describe('useQueries with suspense', () => {
|
|
|
1238
1238
|
expect(results).toEqual(['1', '2', 'loading'])
|
|
1239
1239
|
})
|
|
1240
1240
|
})
|
|
1241
|
+
|
|
1242
|
+
describe('cacheTime minimum enforcement with suspense', () => {
|
|
1243
|
+
const queryClient = createQueryClient()
|
|
1244
|
+
|
|
1245
|
+
it('should not cause infinite re-renders with synchronous query function and cacheTime: 0', async () => {
|
|
1246
|
+
const key = queryKey()
|
|
1247
|
+
let renderCount = 0
|
|
1248
|
+
let queryFnCallCount = 0
|
|
1249
|
+
const maxChecks = 20
|
|
1250
|
+
|
|
1251
|
+
function Page() {
|
|
1252
|
+
renderCount++
|
|
1253
|
+
|
|
1254
|
+
if (renderCount > maxChecks) {
|
|
1255
|
+
throw new Error(`Infinite loop detected! Renders: ${renderCount}`)
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const result = useQuery(
|
|
1259
|
+
key,
|
|
1260
|
+
() => {
|
|
1261
|
+
queryFnCallCount++
|
|
1262
|
+
return 42
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
cacheTime: 0,
|
|
1266
|
+
suspense: true,
|
|
1267
|
+
},
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
return <div>data: {result.data}</div>
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const rendered = renderWithClient(
|
|
1274
|
+
queryClient,
|
|
1275
|
+
<React.Suspense fallback="loading">
|
|
1276
|
+
<Page />
|
|
1277
|
+
</React.Suspense>,
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
await waitFor(() => rendered.getByText('data: 42'))
|
|
1281
|
+
|
|
1282
|
+
expect(renderCount).toBeLessThan(5)
|
|
1283
|
+
expect(queryFnCallCount).toBe(1)
|
|
1284
|
+
expect(rendered.queryByText('data: 42')).not.toBeNull()
|
|
1285
|
+
expect(rendered.queryByText('loading')).toBeNull()
|
|
1286
|
+
})
|
|
1287
|
+
|
|
1288
|
+
describe('boundary value tests', () => {
|
|
1289
|
+
test.each([
|
|
1290
|
+
[0, 1000],
|
|
1291
|
+
[1, 1000],
|
|
1292
|
+
[999, 1000],
|
|
1293
|
+
[1000, 1000],
|
|
1294
|
+
[2000, 2000],
|
|
1295
|
+
])(
|
|
1296
|
+
'cacheTime %i should be adjusted to %i with suspense',
|
|
1297
|
+
async (input, expected) => {
|
|
1298
|
+
const key = queryKey()
|
|
1299
|
+
|
|
1300
|
+
function Page() {
|
|
1301
|
+
const result = useQuery(key, () => 42, {
|
|
1302
|
+
suspense: true,
|
|
1303
|
+
cacheTime: input,
|
|
1304
|
+
})
|
|
1305
|
+
return <div>data: {result.data}</div>
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const rendered = renderWithClient(
|
|
1309
|
+
queryClient,
|
|
1310
|
+
<React.Suspense fallback="loading">
|
|
1311
|
+
<Page />
|
|
1312
|
+
</React.Suspense>,
|
|
1313
|
+
)
|
|
1314
|
+
|
|
1315
|
+
await waitFor(() => rendered.getByText('data: 42'))
|
|
1316
|
+
|
|
1317
|
+
const query = queryClient.getQueryCache().find(key)
|
|
1318
|
+
const options = query?.options
|
|
1319
|
+
expect(options?.cacheTime).toBe(expected)
|
|
1320
|
+
},
|
|
1321
|
+
)
|
|
1322
|
+
})
|
|
1323
|
+
|
|
1324
|
+
it('should preserve user cacheTime when >= 1000ms', async () => {
|
|
1325
|
+
const key = queryKey()
|
|
1326
|
+
const userCacheTime = 5000
|
|
1327
|
+
|
|
1328
|
+
function Page() {
|
|
1329
|
+
useQuery(key, () => 'test', {
|
|
1330
|
+
suspense: true,
|
|
1331
|
+
cacheTime: userCacheTime,
|
|
1332
|
+
})
|
|
1333
|
+
return <div>rendered</div>
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
renderWithClient(
|
|
1337
|
+
queryClient,
|
|
1338
|
+
<React.Suspense fallback="loading">
|
|
1339
|
+
<Page />
|
|
1340
|
+
</React.Suspense>,
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
await waitFor(() => {
|
|
1344
|
+
const query = queryClient.getQueryCache().find(key)
|
|
1345
|
+
const options = query?.options
|
|
1346
|
+
expect(options?.cacheTime).toBe(userCacheTime)
|
|
1347
|
+
})
|
|
1348
|
+
})
|
|
1349
|
+
|
|
1350
|
+
it('should handle async queries with adjusted cacheTime', async () => {
|
|
1351
|
+
const key = queryKey()
|
|
1352
|
+
let renderCount = 0
|
|
1353
|
+
|
|
1354
|
+
function Page() {
|
|
1355
|
+
renderCount++
|
|
1356
|
+
const result = useQuery(
|
|
1357
|
+
key,
|
|
1358
|
+
async () => {
|
|
1359
|
+
await sleep(10)
|
|
1360
|
+
return 'async-result'
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
suspense: true,
|
|
1364
|
+
cacheTime: 0,
|
|
1365
|
+
},
|
|
1366
|
+
)
|
|
1367
|
+
return <div>data: {result.data}</div>
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
const rendered = renderWithClient(
|
|
1371
|
+
queryClient,
|
|
1372
|
+
<React.Suspense fallback="loading">
|
|
1373
|
+
<Page />
|
|
1374
|
+
</React.Suspense>,
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
await waitFor(() => rendered.getByText('data: async-result'))
|
|
1378
|
+
expect(renderCount).toBeLessThan(5)
|
|
1379
|
+
})
|
|
1380
|
+
|
|
1381
|
+
describe('staleTime and cacheTime relationship', () => {
|
|
1382
|
+
it('should handle when both need adjustment', async () => {
|
|
1383
|
+
const key = queryKey()
|
|
1384
|
+
|
|
1385
|
+
function Page() {
|
|
1386
|
+
useQuery(key, () => 42, {
|
|
1387
|
+
suspense: true,
|
|
1388
|
+
cacheTime: 0,
|
|
1389
|
+
staleTime: undefined,
|
|
1390
|
+
})
|
|
1391
|
+
return <div>rendered</div>
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
renderWithClient(
|
|
1395
|
+
queryClient,
|
|
1396
|
+
<React.Suspense fallback="loading">
|
|
1397
|
+
<Page />
|
|
1398
|
+
</React.Suspense>,
|
|
1399
|
+
)
|
|
1400
|
+
|
|
1401
|
+
await waitFor(() => {
|
|
1402
|
+
const query = queryClient.getQueryCache().find(key)
|
|
1403
|
+
const options = query?.options as any
|
|
1404
|
+
expect(options?.cacheTime).toBe(1000)
|
|
1405
|
+
expect(options?.staleTime).toBe(1000)
|
|
1406
|
+
})
|
|
1407
|
+
})
|
|
1408
|
+
|
|
1409
|
+
it('should maintain staleTime < cacheTime invariant', async () => {
|
|
1410
|
+
const key = queryKey()
|
|
1411
|
+
|
|
1412
|
+
function Page() {
|
|
1413
|
+
useQuery(key, () => 42, {
|
|
1414
|
+
suspense: true,
|
|
1415
|
+
cacheTime: 500,
|
|
1416
|
+
staleTime: 2000,
|
|
1417
|
+
})
|
|
1418
|
+
return <div>rendered</div>
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
renderWithClient(
|
|
1422
|
+
queryClient,
|
|
1423
|
+
<React.Suspense fallback="loading">
|
|
1424
|
+
<Page />
|
|
1425
|
+
</React.Suspense>,
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
await waitFor(() => {
|
|
1429
|
+
const query = queryClient.getQueryCache().find(key)
|
|
1430
|
+
const options = query?.options as any
|
|
1431
|
+
expect(options?.cacheTime).toBe(1000)
|
|
1432
|
+
expect(options?.staleTime).toBe(2000)
|
|
1433
|
+
})
|
|
1434
|
+
})
|
|
1435
|
+
})
|
|
1436
|
+
|
|
1437
|
+
it('should fix synchronous query with cacheTime 0 infinite loop', async () => {
|
|
1438
|
+
const key = queryKey()
|
|
1439
|
+
let renderCount = 0
|
|
1440
|
+
let queryFnCallCount = 0
|
|
1441
|
+
|
|
1442
|
+
function Page() {
|
|
1443
|
+
renderCount++
|
|
1444
|
+
const result = useQuery(
|
|
1445
|
+
key,
|
|
1446
|
+
() => {
|
|
1447
|
+
queryFnCallCount++
|
|
1448
|
+
return 42
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
suspense: true,
|
|
1452
|
+
cacheTime: 0,
|
|
1453
|
+
},
|
|
1454
|
+
)
|
|
1455
|
+
return <div>data: {result.data}</div>
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const rendered = renderWithClient(
|
|
1459
|
+
queryClient,
|
|
1460
|
+
<React.Suspense fallback="loading">
|
|
1461
|
+
<Page />
|
|
1462
|
+
</React.Suspense>,
|
|
1463
|
+
)
|
|
1464
|
+
|
|
1465
|
+
await waitFor(() => rendered.getByText('data: 42'))
|
|
1466
|
+
|
|
1467
|
+
expect(renderCount).toBeLessThan(5)
|
|
1468
|
+
expect(queryFnCallCount).toBe(1)
|
|
1469
|
+
})
|
|
1470
|
+
})
|
package/src/suspense.ts
CHANGED
|
@@ -4,6 +4,13 @@ import type { QueryErrorResetBoundaryValue } from './QueryErrorResetBoundary'
|
|
|
4
4
|
import type { QueryObserverResult } from '@tanstack/query-core'
|
|
5
5
|
import type { QueryKey } from '@tanstack/query-core'
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Ensures minimum staleTime and cacheTime values when suspense is enabled.
|
|
9
|
+
* Despite the name, this function guards both staleTime and cacheTime to prevent
|
|
10
|
+
* infinite re-render loops with synchronous queries.
|
|
11
|
+
*
|
|
12
|
+
* @deprecated in v5 - replaced by ensureSuspenseTimers
|
|
13
|
+
*/
|
|
7
14
|
export const ensureStaleTime = (
|
|
8
15
|
defaultedOptions: DefaultedQueryObserverOptions<any, any, any, any, any>,
|
|
9
16
|
) => {
|
|
@@ -13,6 +20,10 @@ export const ensureStaleTime = (
|
|
|
13
20
|
if (typeof defaultedOptions.staleTime !== 'number') {
|
|
14
21
|
defaultedOptions.staleTime = 1000
|
|
15
22
|
}
|
|
23
|
+
|
|
24
|
+
if (typeof defaultedOptions.cacheTime === 'number') {
|
|
25
|
+
defaultedOptions.cacheTime = Math.max(defaultedOptions.cacheTime, 1000)
|
|
26
|
+
}
|
|
16
27
|
}
|
|
17
28
|
}
|
|
18
29
|
|