@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.
@@ -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,SASzE,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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-query",
3
- "version": "4.40.1",
3
+ "version": "4.40.2",
4
4
  "description": "Hooks for managing, caching and syncing asynchronous and remote data in React",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
@@ -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