Skip to content

Commit 87fdcd8

Browse files
author
ismay
committed
feat(use-data-query): use react-query to cache queries
The useDataQuery hook now uses react-query internally to add caching, query deduplication, automatic retries on errors, automatic refetching when the window regains focus and several other optimizations. There are no breaking changes to the useDataQuery api, so you should be able to update to this version without making any changes to your app's code.
1 parent 5bc031c commit 87fdcd8

File tree

8 files changed

+1072
-269
lines changed

8 files changed

+1072
-269
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"@dhis2/cli-style": "^7.2.2",
1515
"@dhis2/cli-utils-docsite": "^2.0.3",
1616
"@testing-library/jest-dom": "^5.0.2",
17-
"@testing-library/react": "^9.4.0",
17+
"@testing-library/react": "^10.0.0",
1818
"@testing-library/react-hooks": "^3.2.1",
1919
"@types/jest": "^24.9.0",
2020
"@types/node": "^13.1.8",

services/data/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,8 @@
3636
"type-check:watch": "yarn type-check --watch",
3737
"test": "d2-app-scripts test",
3838
"coverage": "yarn test --coverage"
39+
},
40+
"dependencies": {
41+
"react-query": "^3.13.11"
3942
}
4043
}
+83-123
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,44 @@
1-
import { render, waitForElement, act } from '@testing-library/react'
2-
import React from 'react'
3-
import {
4-
FetchType,
5-
DataEngineLinkExecuteOptions,
6-
ResolvedResourceQuery,
7-
} from '../engine'
1+
import { render, waitFor } from '@testing-library/react'
2+
import React, { ReactNode } from 'react'
3+
import { QueryClient, QueryClientProvider, setLogger } from 'react-query'
84
import { CustomDataProvider, DataQuery } from '../react'
95
import { QueryRenderInput } from '../types'
106

11-
const customData = {
12-
answer: 42,
7+
// eslint-disable-next-line react/display-name
8+
const createWrapper = (mockData, queryClientOptions = {}) => ({
9+
children,
10+
}: {
11+
children?: ReactNode
12+
}) => {
13+
const queryClient = new QueryClient(queryClientOptions)
14+
15+
return (
16+
<QueryClientProvider client={queryClient}>
17+
<CustomDataProvider data={mockData}>{children}</CustomDataProvider>
18+
</QueryClientProvider>
19+
)
1320
}
1421

22+
beforeAll(() => {
23+
// Prevent the react-query logger from logging to the console
24+
setLogger({
25+
log: jest.fn(),
26+
warn: jest.fn(),
27+
error: jest.fn(),
28+
})
29+
})
30+
31+
afterAll(() => {
32+
// Restore the original react-query logger
33+
setLogger(console)
34+
})
35+
1536
describe('Testing custom data provider and useQuery hook', () => {
1637
it('Should render without failing', async () => {
38+
const data = {
39+
answer: 42,
40+
}
41+
const wrapper = createWrapper(data)
1742
const renderFunction = jest.fn(
1843
({ loading, error, data }: QueryRenderInput) => {
1944
if (loading) return 'loading'
@@ -23,37 +48,54 @@ describe('Testing custom data provider and useQuery hook', () => {
2348
)
2449

2550
const { getByText } = render(
26-
<CustomDataProvider data={customData}>
27-
<DataQuery query={{ answer: { resource: 'answer' } }}>
28-
{renderFunction}
29-
</DataQuery>
30-
</CustomDataProvider>
51+
<DataQuery query={{ answer: { resource: 'answer' } }}>
52+
{renderFunction}
53+
</DataQuery>,
54+
{ wrapper }
3155
)
3256

3357
expect(getByText(/loading/i)).not.toBeUndefined()
3458
expect(renderFunction).toHaveBeenCalledTimes(1)
3559
expect(renderFunction).toHaveBeenLastCalledWith({
3660
called: true,
61+
data: undefined,
62+
engine: expect.any(Object),
63+
error: undefined,
3764
loading: true,
3865
refetch: expect.any(Function),
39-
engine: expect.any(Object),
4066
})
41-
await waitForElement(() => getByText(/data: /i))
67+
68+
await waitFor(() => {
69+
getByText(/data: /i)
70+
})
71+
72+
expect(getByText(/data: /i)).toHaveTextContent(`data: ${data.answer}`)
4273
expect(renderFunction).toHaveBeenCalledTimes(2)
4374
expect(renderFunction).toHaveBeenLastCalledWith({
4475
called: true,
76+
data,
77+
engine: expect.any(Object),
78+
error: undefined,
4579
loading: false,
46-
data: customData,
4780
refetch: expect.any(Function),
48-
engine: expect.any(Object),
4981
})
50-
51-
expect(getByText(/data: /i)).toHaveTextContent(
52-
`data: ${customData.answer}`
53-
)
5482
})
5583

5684
it('Should render an error', async () => {
85+
const expectedError = new Error('Something went wrong')
86+
const data = {
87+
test: () => {
88+
throw expectedError
89+
},
90+
}
91+
// Disable automatic retries, see: https://react-query.tanstack.com/reference/useQuery
92+
const wrapper = createWrapper(data, {
93+
defaultOptions: {
94+
queries: {
95+
retry: false,
96+
},
97+
},
98+
})
5799
const renderFunction = jest.fn(
58100
({ loading, error, data }: QueryRenderInput) => {
59101
if (loading) return 'loading'
@@ -63,120 +105,38 @@ describe('Testing custom data provider and useQuery hook', () => {
63105
)
64106

65107
const { getByText } = render(
66-
<CustomDataProvider data={customData}>
67-
<DataQuery query={{ test: { resource: 'test' } }}>
68-
{renderFunction}
69-
</DataQuery>
70-
</CustomDataProvider>
108+
<DataQuery query={{ test: { resource: 'test' } }}>
109+
{renderFunction}
110+
</DataQuery>,
111+
{ wrapper }
71112
)
72113

73114
expect(getByText(/loading/i)).not.toBeUndefined()
74115
expect(renderFunction).toHaveBeenCalledTimes(1)
75116
expect(renderFunction).toHaveBeenLastCalledWith({
76117
called: true,
118+
data: undefined,
119+
engine: expect.any(Object),
120+
error: undefined,
77121
loading: true,
78122
refetch: expect.any(Function),
79-
engine: expect.any(Object),
80123
})
81-
await waitForElement(() => getByText(/error: /i))
82-
expect(renderFunction).toHaveBeenCalledTimes(2)
83-
expect(String(renderFunction.mock.calls[1][0].error)).toBe(
84-
'Error: No data provided for resource type test!'
85-
)
86-
// expect(getByText(/data: /i)).toHaveTextContent(
87-
// `data: ${customData.answer}`
88-
// )
89-
})
90-
91-
it('Should abort the fetch when unmounted', async () => {
92-
const renderFunction = jest.fn(
93-
({ loading, error, data }: QueryRenderInput) => {
94-
if (loading) return 'loading'
95-
if (error) return <div>error: {error.message}</div>
96-
return <div>data: {data && data.test}</div>
97-
}
98-
)
99-
100-
let signal: AbortSignal | null | undefined
101-
const mockData = {
102-
factory: jest.fn(
103-
async (
104-
type: FetchType,
105-
_: ResolvedResourceQuery,
106-
options?: DataEngineLinkExecuteOptions
107-
) => {
108-
if (options && options.signal && !signal) {
109-
signal = options.signal
110-
}
111-
return 'done'
112-
}
113-
),
114-
}
115-
116-
const { unmount } = render(
117-
<CustomDataProvider data={mockData}>
118-
<DataQuery query={{ test: { resource: 'factory' } }}>
119-
{renderFunction}
120-
</DataQuery>
121-
</CustomDataProvider>
122-
)
123124

124-
expect(renderFunction).toHaveBeenCalledTimes(1)
125-
expect(mockData.factory).toHaveBeenCalledTimes(1)
126-
act(() => {
127-
unmount()
125+
await waitFor(() => {
126+
getByText(/error: /i)
128127
})
129-
expect(signal && signal.aborted).toBe(true)
130-
})
131128

132-
it('Should abort the fetch when refetching', async () => {
133-
let refetch: any
134-
const renderFunction = jest.fn(
135-
({ loading, error, data, refetch: _refetch }: QueryRenderInput) => {
136-
refetch = _refetch
137-
if (loading) return 'loading'
138-
if (error) return <div>error: {error.message}</div>
139-
return <div>data: {data && data.test}</div>
140-
}
141-
)
142-
143-
let signal: any
144-
const mockData = {
145-
factory: jest.fn(
146-
async (
147-
type: FetchType,
148-
q: ResolvedResourceQuery,
149-
options?: DataEngineLinkExecuteOptions
150-
) => {
151-
if (options && options.signal && !signal) {
152-
signal = options.signal
153-
}
154-
return 'test'
155-
}
156-
),
157-
}
158-
159-
const { getByText } = render(
160-
<CustomDataProvider data={mockData}>
161-
<DataQuery query={{ test: { resource: 'factory' } }}>
162-
{renderFunction}
163-
</DataQuery>
164-
</CustomDataProvider>
129+
expect(renderFunction).toHaveBeenCalledTimes(2)
130+
expect(getByText(/error: /i)).toHaveTextContent(
131+
`error: ${expectedError.message}`
165132
)
166-
167-
expect(renderFunction).toHaveBeenCalledTimes(1)
168-
expect(mockData.factory).toHaveBeenCalledTimes(1)
169-
170-
expect(signal.aborted).toBe(false)
171-
expect(refetch).not.toBeUndefined()
172-
act(() => {
173-
refetch()
133+
expect(renderFunction).toHaveBeenLastCalledWith({
134+
called: true,
135+
data: undefined,
136+
engine: expect.any(Object),
137+
error: expectedError,
138+
loading: false,
139+
refetch: expect.any(Function),
174140
})
175-
176-
expect(signal.aborted).toBe(true)
177-
await waitForElement(() => getByText(/data: /i))
178-
179-
expect(renderFunction).toHaveBeenCalledTimes(2)
180-
expect(mockData.factory).toHaveBeenCalledTimes(2)
181141
})
182142
})

services/data/src/__tests__/mutations.test.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, act, waitForElement } from '@testing-library/react'
1+
import { render, act, waitFor } from '@testing-library/react'
22
import React from 'react'
33
import { Mutation, FetchType, ResolvedResourceQuery, JsonMap } from '../engine'
44
import { CustomDataProvider, DataMutation } from '../react'
@@ -71,7 +71,7 @@ describe('Test mutations', () => {
7171
])
7272
expect(mockBackend.target).toHaveBeenCalledTimes(1)
7373

74-
await waitForElement(() => getByText(/data: /i))
74+
await waitFor(() => getByText(/data: /i))
7575
expect(renderFunction).toHaveBeenCalledTimes(3)
7676
expect(renderFunction).toHaveBeenLastCalledWith([
7777
doMutation,
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useConfig } from '@dhis2/app-service-config'
22
import React from 'react'
3+
import { QueryClient, QueryClientProvider } from 'react-query'
34
import { DataEngine } from '../../engine'
45
import { RestAPILink } from '../../links'
56
import { DataContext } from '../context/DataContext'
@@ -9,6 +10,9 @@ export interface ProviderInput {
910
apiVersion?: number
1011
children: React.ReactNode
1112
}
13+
14+
const queryClient = new QueryClient()
15+
1216
export const DataProvider = (props: ProviderInput) => {
1317
const config = {
1418
...useConfig(),
@@ -17,12 +21,13 @@ export const DataProvider = (props: ProviderInput) => {
1721

1822
const link = new RestAPILink(config)
1923
const engine = new DataEngine(link)
20-
2124
const context = { engine }
2225

2326
return (
24-
<DataContext.Provider value={context}>
25-
{props.children}
26-
</DataContext.Provider>
27+
<QueryClientProvider client={queryClient}>
28+
<DataContext.Provider value={context}>
29+
{props.children}
30+
</DataContext.Provider>
31+
</QueryClientProvider>
2732
)
2833
}

0 commit comments

Comments
 (0)