Skip to content

Commit 1948a6f

Browse files
authored
feat(query-core): default networkMode to 'offlineFirst' when persisters are present (#6046)
* feat(query-core): default networkMode to 'offlineFirst' when persisters are present * chore: stabilize test * test: remove flaky test
1 parent 4e6bf29 commit 1948a6f

File tree

5 files changed

+66
-131
lines changed

5 files changed

+66
-131
lines changed

docs/react/plugins/createPersister.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ yarn add @tanstack/query-persist-client-core
3636

3737
This way, you do not need to store whole `QueryClient`, but choose what is worth to be persisted in your application. Each query is lazily restored (when the Query is first used) and persisted (after each run of the `queryFn`), so it does not need to be throttled. `staleTime` is also respected after restoring the Query, so if data is considered `stale`, it will be refetched immediately after restoring. If data is `fresh`, the `queryFn` will not run.
3838

39+
Garbage collecting a Query from memory **does not** affect the persisted data. That means Queries can be kept in memory for a shorter period of time to be more **memory efficient**. If they are used the next time, they will just be restored from the persistent storage again.
40+
3941
```tsx
4042
import AsyncStorage from '@react-native-async-storage/async-storage'
4143
import { QueryClient } from '@tanstack/react-query'
@@ -44,15 +46,20 @@ import { experimental_createPersister } from '@tanstack/query-persist-client-cor
4446
const queryClient = new QueryClient({
4547
defaultOptions: {
4648
queries: {
47-
gcTime: 1000 * 60 * 60 * 24, // 24 hours
49+
gcTime: 1000 * 30, // 30 seconds
4850
persister: experimental_createPersister({
4951
storage: AsyncStorage,
52+
maxAge: 1000 * 60 * 60 * 12 // 12 hours
5053
}),
5154
},
5255
},
5356
})
5457
```
5558

59+
### Adapted defaults
60+
61+
The `createPersister` plugin technically wraps the `queryFn`, so it doesn't restore if the `queryFn` doesn't run. In that way, it acts as a caching layer between the Query and the network. Thus, the `networkMode` defaults to `'offlineFirst'` when a persister is used, so that restoring from the persistent storage can also happen even if there is no network connection.
62+
5663
## API
5764

5865
### `experimental_createPersister`
@@ -88,6 +95,7 @@ export interface StoragePersisterOptions {
8895
* The max-allowed age of the cache in milliseconds.
8996
* If a persisted cache is found that is older than this
9097
* time, it will be discarded
98+
* @default 24 hours
9199
*/
92100
maxAge?: number
93101
/**

packages/query-core/src/queryClient.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,13 @@ export class QueryClient {
489489
defaultedOptions.throwOnError = !!defaultedOptions.suspense
490490
}
491491

492+
if (
493+
typeof defaultedOptions.networkMode === 'undefined' &&
494+
defaultedOptions.persister
495+
) {
496+
defaultedOptions.networkMode = 'offlineFirst'
497+
}
498+
492499
return defaultedOptions as DefaultedQueryObserverOptions<
493500
TQueryFnData,
494501
TError,

packages/query-core/src/tests/queryClient.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,45 @@ describe('queryClient', () => {
150150
})
151151
})
152152

153+
describe('defaultQueryOptions', () => {
154+
test('should default networkMode when persister is present', async () => {
155+
expect(
156+
createQueryClient({
157+
defaultOptions: {
158+
queries: {
159+
persister: 'ignore' as any,
160+
},
161+
},
162+
}).defaultQueryOptions({ queryKey: queryKey() }).networkMode,
163+
).toBe('offlineFirst')
164+
})
165+
166+
test('should not default networkMode without persister', async () => {
167+
expect(
168+
createQueryClient({
169+
defaultOptions: {
170+
queries: {
171+
staleTime: 1000,
172+
},
173+
},
174+
}).defaultQueryOptions({ queryKey: queryKey() }).networkMode,
175+
).toBe(undefined)
176+
})
177+
178+
test('should not default networkMode when already present', async () => {
179+
expect(
180+
createQueryClient({
181+
defaultOptions: {
182+
queries: {
183+
persister: 'ignore' as any,
184+
networkMode: 'always',
185+
},
186+
},
187+
}).defaultQueryOptions({ queryKey: queryKey() }).networkMode,
188+
).toBe('always')
189+
})
190+
})
191+
153192
describe('setQueryData', () => {
154193
test('should not crash if query could not be found', () => {
155194
const key = queryKey()

packages/react-query/src/__tests__/useQuery.test.tsx

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,7 +1154,6 @@ describe('useQuery', () => {
11541154

11551155
it('should update query stale state and refetch when invalidated with invalidateQueries', async () => {
11561156
const key = queryKey()
1157-
const states: Array<UseQueryResult<number>> = []
11581157
let count = 0
11591158

11601159
function Page() {
@@ -1166,59 +1165,33 @@ describe('useQuery', () => {
11661165
return count
11671166
},
11681167
staleTime: Infinity,
1169-
notifyOnChangeProps: 'all',
11701168
})
11711169

1172-
states.push(state)
1173-
11741170
return (
11751171
<div>
11761172
<button
11771173
onClick={() => queryClient.invalidateQueries({ queryKey: key })}
11781174
>
11791175
invalidate
11801176
</button>
1181-
data: {state.data}
1177+
data: {state.data}, isStale: {String(state.isStale)}, isFetching:{' '}
1178+
{String(state.isFetching)}
11821179
</div>
11831180
)
11841181
}
11851182

11861183
const rendered = renderWithClient(queryClient, <Page />)
11871184

1188-
await waitFor(() => rendered.getByText('data: 1'))
1185+
await waitFor(() =>
1186+
rendered.getByText('data: 1, isStale: false, isFetching: false'),
1187+
)
11891188
fireEvent.click(rendered.getByRole('button', { name: /invalidate/i }))
1190-
await waitFor(() => rendered.getByText('data: 2'))
1191-
1192-
await waitFor(() => expect(states.length).toBe(4))
1193-
1194-
expect(states[0]).toMatchObject({
1195-
data: undefined,
1196-
isFetching: true,
1197-
isRefetching: false,
1198-
isSuccess: false,
1199-
isStale: true,
1200-
})
1201-
expect(states[1]).toMatchObject({
1202-
data: 1,
1203-
isFetching: false,
1204-
isRefetching: false,
1205-
isSuccess: true,
1206-
isStale: false,
1207-
})
1208-
expect(states[2]).toMatchObject({
1209-
data: 1,
1210-
isFetching: true,
1211-
isRefetching: true,
1212-
isSuccess: true,
1213-
isStale: true,
1214-
})
1215-
expect(states[3]).toMatchObject({
1216-
data: 2,
1217-
isFetching: false,
1218-
isRefetching: false,
1219-
isSuccess: true,
1220-
isStale: false,
1221-
})
1189+
await waitFor(() =>
1190+
rendered.getByText('data: 1, isStale: true, isFetching: true'),
1191+
)
1192+
await waitFor(() =>
1193+
rendered.getByText('data: 2, isStale: false, isFetching: false'),
1194+
)
12221195
})
12231196

12241197
it('should not update disabled query when refetched with refetchQueries', async () => {

packages/solid-query/src/__tests__/createQuery.test.tsx

Lines changed: 0 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,98 +1511,6 @@ describe('createQuery', () => {
15111511
})
15121512
})
15131513

1514-
it('should keep the previous data on disabled query when placeholderData is set to identity function', async () => {
1515-
const key = queryKey()
1516-
const states: Array<CreateQueryResult<number>> = []
1517-
1518-
function Page() {
1519-
const [count, setCount] = createSignal(0)
1520-
1521-
const state = createQuery(() => ({
1522-
queryKey: [key, count()],
1523-
queryFn: async () => {
1524-
await sleep(10)
1525-
return count()
1526-
},
1527-
enabled: false,
1528-
placeholderData: keepPreviousData,
1529-
notifyOnChangeProps: 'all',
1530-
}))
1531-
1532-
createRenderEffect(() => {
1533-
states.push({ ...state })
1534-
})
1535-
1536-
createEffect(() => {
1537-
const refetch = state.refetch
1538-
refetch()
1539-
1540-
setActTimeout(() => {
1541-
setCount(1)
1542-
}, 20)
1543-
1544-
setActTimeout(() => {
1545-
refetch()
1546-
}, 30)
1547-
})
1548-
1549-
return null
1550-
}
1551-
1552-
render(() => (
1553-
<QueryClientProvider client={queryClient}>
1554-
<Page />
1555-
</QueryClientProvider>
1556-
))
1557-
1558-
await sleep(100)
1559-
1560-
expect(states.length).toBe(6)
1561-
1562-
// Disabled query
1563-
expect(states[0]).toMatchObject({
1564-
data: undefined,
1565-
isFetching: false,
1566-
isSuccess: false,
1567-
isPlaceholderData: false,
1568-
})
1569-
// Fetching query
1570-
expect(states[1]).toMatchObject({
1571-
data: undefined,
1572-
isFetching: true,
1573-
isSuccess: false,
1574-
isPlaceholderData: false,
1575-
})
1576-
// Fetched query
1577-
expect(states[2]).toMatchObject({
1578-
data: 0,
1579-
isFetching: false,
1580-
isSuccess: true,
1581-
isPlaceholderData: false,
1582-
})
1583-
// Set state
1584-
expect(states[3]).toMatchObject({
1585-
data: 0,
1586-
isFetching: false,
1587-
isSuccess: true,
1588-
isPlaceholderData: true,
1589-
})
1590-
// Fetching new query
1591-
expect(states[4]).toMatchObject({
1592-
data: 0,
1593-
isFetching: true,
1594-
isSuccess: true,
1595-
isPlaceholderData: true,
1596-
})
1597-
// Fetched new query
1598-
expect(states[5]).toMatchObject({
1599-
data: 1,
1600-
isFetching: false,
1601-
isSuccess: true,
1602-
isPlaceholderData: false,
1603-
})
1604-
})
1605-
16061514
it('should keep the previous data on disabled query when placeholderData is set and switching query key multiple times', async () => {
16071515
const key = queryKey()
16081516
const states: Array<CreateQueryResult<number>> = []

0 commit comments

Comments
 (0)