Skip to content

Commit 5c4de91

Browse files
committed
feat(persistQueryClient): PersistQueryClientProvider
defer subscription if we are hydrating
1 parent 5a0aab7 commit 5c4de91

File tree

8 files changed

+182
-66
lines changed

8 files changed

+182
-66
lines changed

src/core/queryObserver.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ export class QueryObserver<
440440
let data: TData | undefined
441441

442442
// Optimistically set result in fetching state if needed
443-
if (options.optimisticResults) {
443+
if (options._optimisticResults) {
444444
const mounted = this.hasListeners()
445445

446446
const fetchOnMount = !mounted && shouldFetchOnMount(query, options)
@@ -456,6 +456,9 @@ export class QueryObserver<
456456
status = 'loading'
457457
}
458458
}
459+
if (options._optimisticResults === 'isHydrating') {
460+
fetchStatus = 'paused'
461+
}
459462
}
460463

461464
// Keep previous data if needed

src/core/types.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,8 @@ export interface QueryObserverOptions<
202202
* If set, this value will be used as the placeholder data for this particular query observer while the query is still in the `loading` data and no initialData has been provided.
203203
*/
204204
placeholderData?: TQueryData | PlaceholderDataFunction<TQueryData>
205-
/**
206-
* If set, the observer will optimistically set the result in fetching state before the query has actually started fetching.
207-
* This is to make sure the results are not lagging behind.
208-
* Defaults to `true`.
209-
*/
210-
optimisticResults?: boolean
205+
206+
_optimisticResults?: 'optimistic' | 'isHydrating'
211207
}
212208

213209
type WithRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import React from 'react'
22

33
import { persistQueryClient, PersistQueryClientOptions } from './persist'
4-
import { QueryClientProvider, QueryClientProviderProps } from '../reactjs'
4+
import {
5+
QueryClientProvider,
6+
QueryClientProviderProps,
7+
IsHydratingProvider,
8+
} from '../reactjs'
59

610
export interface PersistQueryClientProviderProps
711
extends QueryClientProviderProps {
812
persistOptions: Omit<PersistQueryClientOptions, 'queryClient'>
9-
loading?: React.ReactNode
1013
}
1114

1215
export const PersistQueryClientProvider = ({
1316
client,
14-
loading,
1517
children,
1618
persistOptions,
1719
...props
1820
}: PersistQueryClientProviderProps): JSX.Element => {
19-
const [initialized, setInitialized] = React.useState(false)
21+
const [isHydrating, setIsHydrating] = React.useState(true)
2022
const options = React.useRef(persistOptions)
2123
React.useEffect(() => {
2224
let unsubscribe: (() => void) | undefined
@@ -26,21 +28,17 @@ export const PersistQueryClientProvider = ({
2628
...options.current,
2729
queryClient: client,
2830
})
29-
setInitialized(true)
31+
setIsHydrating(false)
3032
}
3133

3234
void run()
3335

3436
return unsubscribe
3537
}, [client])
3638

37-
if (!initialized) {
38-
return <>{loading}</>
39-
}
40-
4139
return (
4240
<QueryClientProvider client={client} {...props}>
43-
{children}
41+
<IsHydratingProvider value={isHydrating}>{children}</IsHydratingProvider>
4442
</QueryClientProvider>
4543
)
4644
}

src/persistQueryClient/tests/PersistQueryClientProvider.test.tsx

Lines changed: 115 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,95 @@ describe('PersistQueryClientProvider', () => {
2929
const states: UseQueryResult<string>[] = []
3030

3131
const queryClient = new QueryClient()
32-
await queryClient.prefetchQuery(key, () => Promise.resolve('prefetched'))
32+
await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated'))
3333

3434
const persister = createMockPersister()
3535

3636
await persistQueryClientSave({ queryClient, persister })
3737

3838
queryClient.clear()
3939

40-
const initialData = jest.fn().mockReturnValue('initial')
40+
function Page() {
41+
const state = useQuery(key, async () => {
42+
await sleep(10)
43+
return 'fetched'
44+
})
45+
46+
states.push(state)
47+
48+
return (
49+
<div>
50+
<h1>{state.data}</h1>
51+
<h2>fetchStatus: {state.fetchStatus}</h2>
52+
</div>
53+
)
54+
}
55+
56+
const rendered = render(
57+
<PersistQueryClientProvider
58+
client={queryClient}
59+
persistOptions={{ persister }}
60+
>
61+
<Page />
62+
</PersistQueryClientProvider>
63+
)
64+
65+
await waitFor(() => rendered.getByText('fetchStatus: paused'))
66+
await waitFor(() => rendered.getByText('hydrated'))
67+
await waitFor(() => rendered.getByText('fetched'))
68+
69+
expect(states).toHaveLength(4)
70+
71+
expect(states[0]).toMatchObject({
72+
status: 'loading',
73+
fetchStatus: 'paused',
74+
data: undefined,
75+
})
76+
77+
expect(states[1]).toMatchObject({
78+
status: 'success',
79+
fetchStatus: 'fetching',
80+
data: 'hydrated',
81+
})
82+
83+
expect(states[2]).toMatchObject({
84+
status: 'success',
85+
fetchStatus: 'fetching',
86+
data: 'hydrated',
87+
})
88+
89+
expect(states[3]).toMatchObject({
90+
status: 'success',
91+
fetchStatus: 'idle',
92+
data: 'fetched',
93+
})
94+
})
95+
96+
test('should show initialData while restoring', async () => {
97+
const key = queryKey()
98+
const states: UseQueryResult<string>[] = []
99+
100+
const queryClient = new QueryClient()
101+
await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated'))
102+
103+
const persister = createMockPersister()
104+
105+
await persistQueryClientSave({ queryClient, persister })
106+
107+
queryClient.clear()
41108

42109
function Page() {
43110
const state = useQuery(
44111
key,
45112
async () => {
46113
await sleep(10)
47-
return 'test'
114+
return 'fetched'
48115
},
49116
{
50-
initialData,
117+
initialData: 'initial',
118+
// make sure that initial data is older than the hydration data
119+
// otherwise initialData would be newer and takes precedence
120+
initialDataUpdatedAt: 1,
51121
}
52122
)
53123

@@ -56,6 +126,7 @@ describe('PersistQueryClientProvider', () => {
56126
return (
57127
<div>
58128
<h1>{state.data}</h1>
129+
<h2>fetchStatus: {state.fetchStatus}</h2>
59130
</div>
60131
)
61132
}
@@ -64,38 +135,48 @@ describe('PersistQueryClientProvider', () => {
64135
<PersistQueryClientProvider
65136
client={queryClient}
66137
persistOptions={{ persister }}
67-
loading="loading"
68138
>
69139
<Page />
70140
</PersistQueryClientProvider>
71141
)
72142

73-
await waitFor(() => rendered.getByText('loading'))
74-
await waitFor(() => rendered.getByText('prefetched'))
75-
await waitFor(() => rendered.getByText('test'))
143+
await waitFor(() => rendered.getByText('initial'))
144+
await waitFor(() => rendered.getByText('hydrated'))
145+
await waitFor(() => rendered.getByText('fetched'))
76146

77-
expect(states).toHaveLength(2)
147+
expect(states).toHaveLength(4)
78148

79149
expect(states[0]).toMatchObject({
80150
status: 'success',
81-
isFetching: true,
82-
data: 'prefetched',
151+
fetchStatus: 'paused',
152+
data: 'initial',
83153
})
84154

85155
expect(states[1]).toMatchObject({
86156
status: 'success',
87-
isFetching: false,
88-
data: 'test',
157+
fetchStatus: 'fetching',
158+
data: 'hydrated',
89159
})
90160

91-
expect(initialData).toHaveBeenCalledTimes(0)
161+
expect(states[2]).toMatchObject({
162+
status: 'success',
163+
fetchStatus: 'fetching',
164+
data: 'hydrated',
165+
})
166+
167+
expect(states[3]).toMatchObject({
168+
status: 'success',
169+
fetchStatus: 'idle',
170+
data: 'fetched',
171+
})
92172
})
173+
93174
test('should not refetch after restoring when data is fresh', async () => {
94175
const key = queryKey()
95176
const states: UseQueryResult<string>[] = []
96177

97178
const queryClient = new QueryClient()
98-
await queryClient.prefetchQuery(key, () => Promise.resolve('prefetched'))
179+
await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated'))
99180

100181
const persister = createMockPersister()
101182

@@ -108,7 +189,7 @@ describe('PersistQueryClientProvider', () => {
108189
key,
109190
async () => {
110191
await sleep(10)
111-
return 'test'
192+
return 'fetched'
112193
},
113194
{
114195
staleTime: Infinity,
@@ -120,6 +201,7 @@ describe('PersistQueryClientProvider', () => {
120201
return (
121202
<div>
122203
<h1>{state.data}</h1>
204+
<h2>fetchStatus: {state.fetchStatus}</h2>
123205
</div>
124206
)
125207
}
@@ -128,21 +210,32 @@ describe('PersistQueryClientProvider', () => {
128210
<PersistQueryClientProvider
129211
client={queryClient}
130212
persistOptions={{ persister }}
131-
loading="loading"
132213
>
133214
<Page />
134215
</PersistQueryClientProvider>
135216
)
136217

137-
await waitFor(() => rendered.getByText('loading'))
138-
await waitFor(() => rendered.getByText('prefetched'))
218+
await waitFor(() => rendered.getByText('fetchStatus: paused'))
219+
await waitFor(() => rendered.getByText('fetchStatus: idle'))
139220

140-
expect(states).toHaveLength(1)
221+
expect(states).toHaveLength(3)
141222

142223
expect(states[0]).toMatchObject({
224+
status: 'loading',
225+
fetchStatus: 'paused',
226+
data: undefined,
227+
})
228+
229+
expect(states[1]).toMatchObject({
230+
status: 'success',
231+
fetchStatus: 'idle',
232+
data: 'hydrated',
233+
})
234+
235+
expect(states[2]).toMatchObject({
143236
status: 'success',
144-
isFetching: false,
145-
data: 'prefetched',
237+
fetchStatus: 'idle',
238+
data: 'hydrated',
146239
})
147240
})
148241
})

src/reactjs/Hydrate.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import React from 'react'
33
import { hydrate, HydrateOptions } from '../core'
44
import { useQueryClient } from './QueryClientProvider'
55

6+
const IsHydratingContext = React.createContext(false)
7+
8+
export const useIsHydrating = () => React.useContext(IsHydratingContext)
9+
export const IsHydratingProvider = IsHydratingContext.Provider
10+
611
export function useHydrate(state: unknown, options?: HydrateOptions) {
712
const queryClient = useQueryClient()
813

src/reactjs/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ export { useMutation } from './useMutation'
1313
export { useQuery } from './useQuery'
1414
export { useQueries } from './useQueries'
1515
export { useInfiniteQuery } from './useInfiniteQuery'
16-
export { useHydrate, Hydrate } from './Hydrate'
16+
export {
17+
useHydrate,
18+
Hydrate,
19+
IsHydratingProvider,
20+
useIsHydrating,
21+
} from './Hydrate'
1722

1823
// Types
1924
export * from './types'

0 commit comments

Comments
 (0)