@tanstack/react-query 4.13.4 → 4.14.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-query",
3
- "version": "4.13.4",
3
+ "version": "4.14.0",
4
4
  "description": "Hooks for managing, caching and syncing asynchronous and remote data in React",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
@@ -46,7 +46,7 @@
46
46
  "react-error-boundary": "^3.1.4"
47
47
  },
48
48
  "dependencies": {
49
- "@tanstack/query-core": "4.13.4",
49
+ "@tanstack/query-core": "4.14.0",
50
50
  "use-sync-external-store": "^1.2.0"
51
51
  },
52
52
  "peerDependencies": {
@@ -80,6 +80,14 @@ export const QueryClientProvider = ({
80
80
  }
81
81
  }, [client])
82
82
 
83
+ if (process.env.NODE_ENV !== 'production' && contextSharing) {
84
+ client
85
+ .getLogger()
86
+ .error(
87
+ `The contextSharing option has been deprecated and will be removed in the next major version`,
88
+ )
89
+ }
90
+
83
91
  const Context = getQueryClientContext(context, contextSharing)
84
92
 
85
93
  return (
@@ -12,7 +12,7 @@ import {
12
12
  dehydrate,
13
13
  hydrate,
14
14
  } from '..'
15
- import { createQueryClient, mockLogger, setIsServer, sleep } from './utils'
15
+ import { createQueryClient, setIsServer, sleep } from './utils'
16
16
 
17
17
  const isReact18 = () => (process.env.REACTJS_VERSION || '18') === '18'
18
18
 
@@ -55,6 +55,9 @@ describe('Server side rendering with de/rehydration', () => {
55
55
  globalThis.IS_REACT_ACT_ENVIRONMENT = previousIsReactActEnvironment
56
56
  })
57
57
  it('should not mismatch on success', async () => {
58
+ const consoleMock = jest.spyOn(console, 'error')
59
+ consoleMock.mockImplementation(() => undefined)
60
+
58
61
  if (!isReact18()) {
59
62
  return
60
63
  }
@@ -118,15 +121,23 @@ describe('Server side rendering with de/rehydration', () => {
118
121
  )
119
122
 
120
123
  // Check that we have no React hydration mismatches
121
- expect(mockLogger.error).not.toHaveBeenCalled()
124
+ // this should be zero calls and can be changed once we drop react17 support
125
+ expect(consoleMock).toHaveBeenNthCalledWith(
126
+ 1,
127
+ 'Warning: You are importing hydrateRoot from "react-dom" which is not supported. You should instead import it from "react-dom/client".',
128
+ )
122
129
  expect(fetchDataSuccess).toHaveBeenCalledTimes(2)
123
130
  expect(el.innerHTML).toBe(expectedMarkup)
124
131
 
125
132
  unmount()
126
133
  queryClient.clear()
134
+ consoleMock.mockRestore()
127
135
  })
128
136
 
129
137
  it('should not mismatch on error', async () => {
138
+ const consoleMock = jest.spyOn(console, 'error')
139
+ consoleMock.mockImplementation(() => undefined)
140
+
130
141
  if (!isReact18()) {
131
142
  return
132
143
  }
@@ -187,7 +198,7 @@ describe('Server side rendering with de/rehydration', () => {
187
198
  )
188
199
 
189
200
  // We expect exactly one console.error here, which is from the
190
- expect(mockLogger.error).toHaveBeenCalledTimes(1)
201
+ expect(consoleMock).toHaveBeenCalledTimes(1)
191
202
  expect(fetchDataError).toHaveBeenCalledTimes(2)
192
203
  expect(el.innerHTML).toBe(expectedMarkup)
193
204
  await sleep(50)
@@ -198,9 +209,13 @@ describe('Server side rendering with de/rehydration', () => {
198
209
 
199
210
  unmount()
200
211
  queryClient.clear()
212
+ consoleMock.mockRestore()
201
213
  })
202
214
 
203
215
  it('should not mismatch on queries that were not prefetched', async () => {
216
+ const consoleMock = jest.spyOn(console, 'error')
217
+ consoleMock.mockImplementation(() => undefined)
218
+
204
219
  if (!isReact18()) {
205
220
  return
206
221
  }
@@ -254,7 +269,11 @@ describe('Server side rendering with de/rehydration', () => {
254
269
  )
255
270
 
256
271
  // Check that we have no React hydration mismatches
257
- expect(mockLogger.error).not.toHaveBeenCalled()
272
+ // this should be zero calls and can be changed once we drop react17 support
273
+ expect(consoleMock).toHaveBeenNthCalledWith(
274
+ 1,
275
+ 'Warning: You are importing hydrateRoot from "react-dom" which is not supported. You should instead import it from "react-dom/client".',
276
+ )
258
277
  expect(fetchDataSuccess).toHaveBeenCalledTimes(1)
259
278
  expect(el.innerHTML).toBe(expectedMarkup)
260
279
  await sleep(50)
@@ -265,5 +284,6 @@ describe('Server side rendering with de/rehydration', () => {
265
284
 
266
285
  unmount()
267
286
  queryClient.clear()
287
+ consoleMock.mockRestore()
268
288
  })
269
289
  })
@@ -873,20 +873,35 @@ describe('useQuery', () => {
873
873
 
874
874
  states.push(state)
875
875
 
876
- const { remove } = state
877
-
878
- React.useEffect(() => {
879
- setActTimeout(() => {
880
- remove()
881
- rerender({})
882
- }, 20)
883
- }, [remove])
876
+ return (
877
+ <>
878
+ <div>{state.data}</div>
884
879
 
885
- return null
880
+ <button
881
+ onClick={() => {
882
+ state.remove()
883
+ rerender({})
884
+ }}
885
+ >
886
+ remove
887
+ </button>
888
+ </>
889
+ )
886
890
  }
887
891
 
888
- renderWithClient(queryClient, <Page />)
892
+ const rendered = renderWithClient(queryClient, <Page />)
893
+
894
+ await waitFor(() => {
895
+ rendered.getByText('data')
896
+ })
897
+
898
+ fireEvent.click(rendered.getByRole('button', { name: 'remove' }))
889
899
 
900
+ await waitFor(() => {
901
+ rendered.getByText('data')
902
+ })
903
+
904
+ // required to make sure no additional renders are happening after data is successfully fetched for the second time
890
905
  await sleep(100)
891
906
 
892
907
  expect(states.length).toBe(5)
@@ -1518,17 +1533,27 @@ describe('useQuery', () => {
1518
1533
 
1519
1534
  states.push(state)
1520
1535
 
1521
- React.useEffect(() => {
1522
- setActTimeout(() => {
1523
- setCount(1)
1524
- }, 10)
1525
- }, [])
1526
-
1527
- return null
1536
+ return (
1537
+ <div>
1538
+ <button onClick={() => setCount(1)}>increment</button>
1539
+ <div>data: {state.data ?? 'undefined'}</div>
1540
+ <div>count: {count}</div>
1541
+ </div>
1542
+ )
1528
1543
  }
1529
1544
 
1530
- renderWithClient(queryClient, <Page />)
1545
+ const rendered = renderWithClient(queryClient, <Page />)
1546
+
1547
+ await waitFor(() => rendered.getByText('data: 0'))
1548
+
1549
+ fireEvent.click(rendered.getByRole('button', { name: /increment/i }))
1531
1550
 
1551
+ await waitFor(() => {
1552
+ rendered.getByText('count: 1')
1553
+ rendered.getByText('data: undefined')
1554
+ })
1555
+
1556
+ // making sure no additional fetches are triggered
1532
1557
  await sleep(50)
1533
1558
 
1534
1559
  expect(states.length).toBe(3)
@@ -1744,18 +1769,30 @@ describe('useQuery', () => {
1744
1769
 
1745
1770
  states.push(state)
1746
1771
 
1747
- React.useEffect(() => {
1748
- setActTimeout(() => {
1749
- setCount(1)
1750
- }, 20)
1751
- }, [])
1752
-
1753
- return null
1772
+ return (
1773
+ <div>
1774
+ <h1>
1775
+ data: {state.data}, count: {count}, isFetching:{' '}
1776
+ {String(state.isFetching)}
1777
+ </h1>
1778
+ <button onClick={() => setCount(1)}>inc</button>
1779
+ </div>
1780
+ )
1754
1781
  }
1755
1782
 
1756
- renderWithClient(queryClient, <Page />)
1783
+ const rendered = renderWithClient(queryClient, <Page />)
1757
1784
 
1758
- await waitFor(() => expect(states.length).toBe(5))
1785
+ await waitFor(() =>
1786
+ rendered.getByText('data: 0, count: 0, isFetching: false'),
1787
+ )
1788
+
1789
+ fireEvent.click(rendered.getByRole('button', { name: 'inc' }))
1790
+
1791
+ await waitFor(() =>
1792
+ rendered.getByText('data: 1, count: 1, isFetching: false'),
1793
+ )
1794
+
1795
+ expect(states.length).toBe(5)
1759
1796
 
1760
1797
  // Initial
1761
1798
  expect(states[0]).toMatchObject({
@@ -1773,17 +1810,17 @@ describe('useQuery', () => {
1773
1810
  })
1774
1811
  // Set state
1775
1812
  expect(states[2]).toMatchObject({
1776
- data: 0,
1813
+ data: 99,
1777
1814
  isFetching: true,
1778
1815
  isSuccess: true,
1779
- isPreviousData: true,
1816
+ isPreviousData: false,
1780
1817
  })
1781
1818
  // Hook state update
1782
1819
  expect(states[3]).toMatchObject({
1783
- data: 0,
1820
+ data: 99,
1784
1821
  isFetching: true,
1785
1822
  isSuccess: true,
1786
- isPreviousData: true,
1823
+ isPreviousData: false,
1787
1824
  })
1788
1825
  // New data
1789
1826
  expect(states[4]).toMatchObject({
@@ -1812,26 +1849,41 @@ describe('useQuery', () => {
1812
1849
 
1813
1850
  states.push(state)
1814
1851
 
1815
- const { refetch } = state
1852
+ return (
1853
+ <div>
1854
+ <button onClick={() => state.refetch()}>refetch</button>
1855
+ <button onClick={() => setCount(1)}>setCount</button>
1856
+ <div>data: {state.data ?? 'undefined'}</div>
1857
+ </div>
1858
+ )
1859
+ }
1816
1860
 
1817
- React.useEffect(() => {
1818
- refetch()
1861
+ const rendered = renderWithClient(queryClient, <Page />)
1819
1862
 
1820
- setActTimeout(() => {
1821
- setCount(1)
1822
- }, 20)
1863
+ await waitFor(() => {
1864
+ rendered.getByText('data: undefined')
1865
+ })
1823
1866
 
1824
- setActTimeout(() => {
1825
- refetch()
1826
- }, 30)
1827
- }, [refetch])
1867
+ fireEvent.click(rendered.getByRole('button', { name: 'refetch' }))
1828
1868
 
1829
- return null
1830
- }
1869
+ await waitFor(() => {
1870
+ rendered.getByText('data: 0')
1871
+ })
1831
1872
 
1832
- renderWithClient(queryClient, <Page />)
1873
+ fireEvent.click(rendered.getByRole('button', { name: 'setCount' }))
1833
1874
 
1834
- await sleep(100)
1875
+ await waitFor(() => {
1876
+ rendered.getByText('data: 0')
1877
+ })
1878
+
1879
+ fireEvent.click(rendered.getByRole('button', { name: 'refetch' }))
1880
+
1881
+ await waitFor(() => {
1882
+ rendered.getByText('data: 1')
1883
+ })
1884
+
1885
+ // making sure no additional renders are triggered
1886
+ await sleep(20)
1835
1887
 
1836
1888
  expect(states.length).toBe(6)
1837
1889
 
@@ -2194,19 +2246,31 @@ describe('useQuery', () => {
2194
2246
 
2195
2247
  states.push(state)
2196
2248
 
2197
- const { refetch } = state
2249
+ return (
2250
+ <>
2251
+ <button
2252
+ onClick={async () => {
2253
+ await state.refetch()
2254
+ }}
2255
+ >
2256
+ refetch
2257
+ </button>
2198
2258
 
2199
- React.useEffect(() => {
2200
- setActTimeout(() => {
2201
- refetch()
2202
- }, 10)
2203
- }, [refetch])
2204
- return null
2259
+ <div>{state.data}</div>
2260
+ </>
2261
+ )
2205
2262
  }
2206
2263
 
2207
- renderWithClient(queryClient, <Page />)
2264
+ const rendered = renderWithClient(queryClient, <Page />)
2265
+
2266
+ await waitFor(() => {
2267
+ rendered.getByText('test')
2268
+ })
2208
2269
 
2209
- await sleep(30)
2270
+ fireEvent.click(rendered.getByRole('button', { name: 'refetch' }))
2271
+
2272
+ // sleep is required to make sure no additional renders happen after click
2273
+ await sleep(20)
2210
2274
 
2211
2275
  expect(states.length).toBe(2)
2212
2276
  expect(states[0]).toMatchObject({
@@ -3681,6 +3745,61 @@ describe('useQuery', () => {
3681
3745
  expect(results[1]).toMatchObject({ data: 1, isFetching: false })
3682
3746
  })
3683
3747
 
3748
+ it('should show the correct data when switching keys with initialData, keepPreviousData & staleTime', async () => {
3749
+ const key = queryKey()
3750
+
3751
+ const ALL_TODOS = [
3752
+ { name: 'todo A', priority: 'high' },
3753
+ { name: 'todo B', priority: 'medium' },
3754
+ ]
3755
+
3756
+ const initialTodos = ALL_TODOS
3757
+
3758
+ function Page() {
3759
+ const [filter, setFilter] = React.useState('')
3760
+ const { data: todos } = useQuery(
3761
+ [...key, filter],
3762
+ async () => {
3763
+ return ALL_TODOS.filter((todo) =>
3764
+ filter ? todo.priority === filter : true,
3765
+ )
3766
+ },
3767
+ {
3768
+ initialData() {
3769
+ return filter === '' ? initialTodos : undefined
3770
+ },
3771
+ keepPreviousData: true,
3772
+ staleTime: 5000,
3773
+ },
3774
+ )
3775
+
3776
+ return (
3777
+ <div>
3778
+ Current Todos, filter: {filter || 'all'}
3779
+ <hr />
3780
+ <button onClick={() => setFilter('')}>All</button>
3781
+ <button onClick={() => setFilter('high')}>High</button>
3782
+ <ul>
3783
+ {(todos ?? []).map((todo) => (
3784
+ <li key={todo.name}>
3785
+ {todo.name} - {todo.priority}
3786
+ </li>
3787
+ ))}
3788
+ </ul>
3789
+ </div>
3790
+ )
3791
+ }
3792
+
3793
+ const rendered = renderWithClient(queryClient, <Page />)
3794
+
3795
+ await waitFor(() => rendered.getByText('Current Todos, filter: all'))
3796
+
3797
+ fireEvent.click(rendered.getByRole('button', { name: /high/i }))
3798
+ await waitFor(() => rendered.getByText('Current Todos, filter: high'))
3799
+ fireEvent.click(rendered.getByRole('button', { name: /all/i }))
3800
+ await waitFor(() => rendered.getByText('todo B - medium'))
3801
+ })
3802
+
3684
3803
  // // See https://github.com/tannerlinsley/react-query/issues/214
3685
3804
  it('data should persist when enabled is changed to false', async () => {
3686
3805
  const key = queryKey()