@dhis2/app-service-data 3.17.1 → 3.18.0-beta.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.
- package/README.md +104 -0
- package/build/cjs/react/hooks/useDataQuery.js +4 -0
- package/build/es/react/hooks/useDataQuery.js +5 -0
- package/build/types/react/hooks/useDataQuery.d.ts +6 -2
- package/package.json +5 -4
- package/.gitignore +0 -5
- package/d2.config.js +0 -9
- package/jest.config.js +0 -14
- package/src/__tests__/integration.test.tsx +0 -80
- package/src/__tests__/mutations.test.tsx +0 -71
- package/src/index.ts +0 -5
- package/src/react/components/CustomDataProvider.tsx +0 -33
- package/src/react/components/DataMutation.tsx +0 -24
- package/src/react/components/DataProvider.test.tsx +0 -22
- package/src/react/components/DataProvider.tsx +0 -58
- package/src/react/components/DataQuery.tsx +0 -26
- package/src/react/components/index.ts +0 -4
- package/src/react/context/DataContext.tsx +0 -5
- package/src/react/context/defaultDataContext.test.ts +0 -46
- package/src/react/context/defaultDataContext.ts +0 -9
- package/src/react/hooks/index.ts +0 -3
- package/src/react/hooks/mergeAndCompareVariables.test.ts +0 -65
- package/src/react/hooks/mergeAndCompareVariables.ts +0 -32
- package/src/react/hooks/stableVariablesHash.test.ts +0 -53
- package/src/react/hooks/stableVariablesHash.ts +0 -56
- package/src/react/hooks/useDataEngine.ts +0 -8
- package/src/react/hooks/useDataMutation.test.tsx +0 -296
- package/src/react/hooks/useDataMutation.ts +0 -42
- package/src/react/hooks/useDataQuery.test.tsx +0 -1029
- package/src/react/hooks/useDataQuery.ts +0 -170
- package/src/react/hooks/useQueryExecutor.test.tsx +0 -195
- package/src/react/hooks/useQueryExecutor.ts +0 -99
- package/src/react/hooks/useStaticInput.test.ts +0 -101
- package/src/react/hooks/useStaticInput.ts +0 -27
- package/src/react/index.ts +0 -2
- package/src/setupRTL.ts +0 -5
- package/src/types.ts +0 -72
- package/tsconfig.json +0 -10
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Query,
|
|
3
|
-
QueryOptions,
|
|
4
|
-
QueryResult,
|
|
5
|
-
QueryVariables,
|
|
6
|
-
FetchError,
|
|
7
|
-
} from '@dhis2/data-engine'
|
|
8
|
-
import { useQuery } from '@tanstack/react-query'
|
|
9
|
-
import { useState, useRef, useCallback, useDebugValue } from 'react'
|
|
10
|
-
import type { QueryRenderInput, QueryRefetchFunction } from '../../types'
|
|
11
|
-
import { mergeAndCompareVariables } from './mergeAndCompareVariables'
|
|
12
|
-
import { useDataEngine } from './useDataEngine'
|
|
13
|
-
import { useStaticInput } from './useStaticInput'
|
|
14
|
-
|
|
15
|
-
type QueryState = {
|
|
16
|
-
enabled: boolean
|
|
17
|
-
variables?: QueryVariables
|
|
18
|
-
variablesHash?: string
|
|
19
|
-
refetchCallback?: (data: any) => void
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const useDataQuery = <TQueryResult = QueryResult>(
|
|
23
|
-
query: Query,
|
|
24
|
-
{
|
|
25
|
-
onComplete: userOnSuccess,
|
|
26
|
-
onError: userOnError,
|
|
27
|
-
variables: initialVariables = {},
|
|
28
|
-
lazy: initialLazy = false,
|
|
29
|
-
}: QueryOptions<TQueryResult> = {}
|
|
30
|
-
): QueryRenderInput<TQueryResult> => {
|
|
31
|
-
const [staticQuery] = useStaticInput<Query>(query, {
|
|
32
|
-
warn: true,
|
|
33
|
-
name: 'query',
|
|
34
|
-
})
|
|
35
|
-
const [variablesUpdateCount, setVariablesUpdateCount] = useState(0)
|
|
36
|
-
|
|
37
|
-
const queryState = useRef<QueryState>({
|
|
38
|
-
variables: initialVariables,
|
|
39
|
-
variablesHash: undefined,
|
|
40
|
-
enabled: !initialLazy,
|
|
41
|
-
refetchCallback: undefined,
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Display current query state and refetch count in React DevTools
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
useDebugValue(
|
|
49
|
-
{
|
|
50
|
-
variablesUpdateCount,
|
|
51
|
-
enabled: queryState.current.enabled,
|
|
52
|
-
variables: queryState.current.variables,
|
|
53
|
-
},
|
|
54
|
-
(debugValue) => JSON.stringify(debugValue)
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* User callbacks and refetch handling
|
|
59
|
-
*/
|
|
60
|
-
|
|
61
|
-
const onSuccess = (data: any) => {
|
|
62
|
-
queryState.current.refetchCallback?.(data)
|
|
63
|
-
queryState.current.refetchCallback = undefined
|
|
64
|
-
|
|
65
|
-
if (userOnSuccess) {
|
|
66
|
-
userOnSuccess(data)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const onError = (error: FetchError) => {
|
|
71
|
-
// If we'd want to reject on errors we'd call the cb with the error here
|
|
72
|
-
queryState.current.refetchCallback = undefined
|
|
73
|
-
|
|
74
|
-
if (userOnError) {
|
|
75
|
-
userOnError(error)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Setting up react-query
|
|
81
|
-
*/
|
|
82
|
-
|
|
83
|
-
const engine = useDataEngine()
|
|
84
|
-
const queryKey = [staticQuery, queryState.current.variables]
|
|
85
|
-
const queryFn = () =>
|
|
86
|
-
engine.query(staticQuery, { variables: queryState.current.variables })
|
|
87
|
-
|
|
88
|
-
const {
|
|
89
|
-
status,
|
|
90
|
-
fetchStatus,
|
|
91
|
-
error,
|
|
92
|
-
data,
|
|
93
|
-
refetch: queryRefetch,
|
|
94
|
-
} = useQuery({
|
|
95
|
-
queryKey,
|
|
96
|
-
queryFn,
|
|
97
|
-
enabled: queryState.current.enabled,
|
|
98
|
-
onSuccess,
|
|
99
|
-
onError,
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Refetch allows a user to update the variables or just
|
|
104
|
-
* trigger a refetch of the query with the current variables.
|
|
105
|
-
*
|
|
106
|
-
* We're using useCallback to make the identity of the function
|
|
107
|
-
* as stable as possible, so that it won't trigger excessive
|
|
108
|
-
* rerenders when used for side-effects.
|
|
109
|
-
*/
|
|
110
|
-
|
|
111
|
-
const refetch: QueryRefetchFunction = useCallback(
|
|
112
|
-
(newVariables) => {
|
|
113
|
-
const { identical, mergedVariables, mergedVariablesHash } =
|
|
114
|
-
mergeAndCompareVariables(
|
|
115
|
-
queryState.current.variables,
|
|
116
|
-
newVariables,
|
|
117
|
-
queryState.current.variablesHash
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* If there are no updates that will trigger an automatic refetch
|
|
122
|
-
* we'll need to call react-query's refetch directly
|
|
123
|
-
*/
|
|
124
|
-
if (queryState.current.enabled && identical) {
|
|
125
|
-
return queryRefetch({ throwOnError: false }).then(
|
|
126
|
-
({ data }) => data
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
queryState.current.variables = mergedVariables
|
|
131
|
-
queryState.current.variablesHash = mergedVariablesHash
|
|
132
|
-
queryState.current.enabled = true
|
|
133
|
-
|
|
134
|
-
// This promise does not currently reject on errors
|
|
135
|
-
const refetchPromise = new Promise<TQueryResult>((resolve) => {
|
|
136
|
-
queryState.current.refetchCallback = (data) => {
|
|
137
|
-
resolve(data)
|
|
138
|
-
}
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
// Trigger a react-query refetch by incrementing variablesUpdateCount state
|
|
142
|
-
setVariablesUpdateCount((prevCount) => prevCount + 1)
|
|
143
|
-
|
|
144
|
-
return refetchPromise
|
|
145
|
-
},
|
|
146
|
-
[queryRefetch]
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* react-query returns null or an error, but we return undefined
|
|
151
|
-
* or an error, so this ensures consistency with the other types.
|
|
152
|
-
*/
|
|
153
|
-
const ourError = error || undefined
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
engine,
|
|
157
|
-
// A query has not been called if it is lazy (fetchStatus = 'idle') and no initial data is available (status = 'loading').
|
|
158
|
-
// https://tanstack.com/query/v4/docs/framework/react/guides/queries
|
|
159
|
-
called: !(status === 'loading' && fetchStatus === 'idle'),
|
|
160
|
-
// 'loading' should only be true when actively fetching (fetchStatus = 'fetching') while there is no data yet (status = 'loading').
|
|
161
|
-
// If there is already data for the query, then 'loading' will not become 'true' when refetching, so the previous data can still be
|
|
162
|
-
// displayed while new data is fetched in the background
|
|
163
|
-
loading: fetchStatus === 'fetching' && status === 'loading',
|
|
164
|
-
// 'fetching' reflects the fetching behavior behind the scenes
|
|
165
|
-
fetching: fetchStatus === 'fetching',
|
|
166
|
-
error: ourError,
|
|
167
|
-
data,
|
|
168
|
-
refetch,
|
|
169
|
-
}
|
|
170
|
-
}
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { renderHook, act, waitFor } from '@testing-library/react'
|
|
2
|
-
import { useQueryExecutor } from './useQueryExecutor'
|
|
3
|
-
|
|
4
|
-
const testError = new Error('TEST ERROR')
|
|
5
|
-
let theSignal: AbortSignal | undefined
|
|
6
|
-
const execute = jest.fn(async ({ signal }) => {
|
|
7
|
-
theSignal = signal
|
|
8
|
-
return 42
|
|
9
|
-
})
|
|
10
|
-
const failingExecute = jest.fn(async () => {
|
|
11
|
-
throw testError
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
describe('useQueryExecutor', () => {
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
jest.clearAllMocks()
|
|
17
|
-
theSignal = undefined
|
|
18
|
-
})
|
|
19
|
-
it('When not immediate, should start with called false and loading false', () => {
|
|
20
|
-
const { result } = renderHook(() =>
|
|
21
|
-
useQueryExecutor({
|
|
22
|
-
execute,
|
|
23
|
-
immediate: false,
|
|
24
|
-
singular: true,
|
|
25
|
-
variables: {},
|
|
26
|
-
})
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
expect(result.current).toMatchObject({
|
|
30
|
-
called: false,
|
|
31
|
-
loading: false,
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('When immediate, should start with called true and loading true', async () => {
|
|
36
|
-
const { result } = renderHook(() =>
|
|
37
|
-
useQueryExecutor({
|
|
38
|
-
execute,
|
|
39
|
-
immediate: true,
|
|
40
|
-
singular: true,
|
|
41
|
-
variables: {},
|
|
42
|
-
})
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
expect(result.current).toMatchObject({
|
|
46
|
-
called: true,
|
|
47
|
-
loading: true,
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
await waitFor(() => {
|
|
51
|
-
expect(result.current).toMatchObject({
|
|
52
|
-
called: true,
|
|
53
|
-
loading: false,
|
|
54
|
-
data: 42,
|
|
55
|
-
})
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('Should start when refetch called (if not immediate)', async () => {
|
|
60
|
-
const { result } = renderHook(() =>
|
|
61
|
-
useQueryExecutor({
|
|
62
|
-
execute,
|
|
63
|
-
immediate: false,
|
|
64
|
-
singular: true,
|
|
65
|
-
variables: {},
|
|
66
|
-
})
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
expect(result.current).toMatchObject({
|
|
70
|
-
called: false,
|
|
71
|
-
loading: false,
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
act(() => {
|
|
75
|
-
result.current.refetch()
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
expect(result.current).toMatchObject({
|
|
79
|
-
called: true,
|
|
80
|
-
loading: true,
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
await waitFor(() => {
|
|
84
|
-
expect(result.current).toMatchObject({
|
|
85
|
-
called: true,
|
|
86
|
-
loading: false,
|
|
87
|
-
data: 42,
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('Should report an error when execute fails', async () => {
|
|
93
|
-
const { result } = renderHook(() =>
|
|
94
|
-
useQueryExecutor({
|
|
95
|
-
execute: failingExecute,
|
|
96
|
-
immediate: false,
|
|
97
|
-
singular: true,
|
|
98
|
-
variables: {},
|
|
99
|
-
})
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
expect(result.current).toMatchObject({
|
|
103
|
-
called: false,
|
|
104
|
-
loading: false,
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
act(() => {
|
|
108
|
-
result.current.refetch()
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
expect(result.current).toMatchObject({
|
|
112
|
-
called: true,
|
|
113
|
-
loading: true,
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
await waitFor(() => {
|
|
117
|
-
expect(result.current).toMatchObject({
|
|
118
|
-
called: true,
|
|
119
|
-
loading: false,
|
|
120
|
-
error: testError,
|
|
121
|
-
})
|
|
122
|
-
})
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it("Shouldn't abort+refetch when inputs change on subsequent renders", async () => {
|
|
126
|
-
const { result, rerender } = renderHook(
|
|
127
|
-
({ onComplete }) =>
|
|
128
|
-
useQueryExecutor({
|
|
129
|
-
execute,
|
|
130
|
-
immediate: true,
|
|
131
|
-
singular: true,
|
|
132
|
-
variables: {},
|
|
133
|
-
onComplete,
|
|
134
|
-
}),
|
|
135
|
-
{
|
|
136
|
-
initialProps: { onComplete: () => null },
|
|
137
|
-
}
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
expect(result.current).toMatchObject({
|
|
141
|
-
called: true,
|
|
142
|
-
loading: true,
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
rerender({ onComplete: () => null })
|
|
146
|
-
|
|
147
|
-
await waitFor(() => {
|
|
148
|
-
expect(result.current).toMatchObject({
|
|
149
|
-
called: true,
|
|
150
|
-
loading: false,
|
|
151
|
-
data: 42,
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
expect(theSignal && theSignal.aborted).toBe(false)
|
|
155
|
-
expect(execute).toHaveBeenCalledTimes(1)
|
|
156
|
-
})
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('Should respect abort signal', async () => {
|
|
160
|
-
const { result } = renderHook(() =>
|
|
161
|
-
useQueryExecutor({
|
|
162
|
-
execute,
|
|
163
|
-
immediate: false,
|
|
164
|
-
singular: true,
|
|
165
|
-
variables: {},
|
|
166
|
-
})
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
expect(result.current).toMatchObject({
|
|
170
|
-
called: false,
|
|
171
|
-
loading: false,
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
act(() => {
|
|
175
|
-
result.current.refetch()
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
expect(result.current).toMatchObject({
|
|
179
|
-
called: true,
|
|
180
|
-
loading: true,
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
act(() => {
|
|
184
|
-
result.current.abort()
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
expect(theSignal && theSignal.aborted).toBe(true)
|
|
188
|
-
|
|
189
|
-
expect(result.current).toMatchObject({
|
|
190
|
-
called: true,
|
|
191
|
-
loading: false,
|
|
192
|
-
error: { type: 'aborted', message: 'Aborted' },
|
|
193
|
-
})
|
|
194
|
-
})
|
|
195
|
-
})
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { FetchError } from '@dhis2/data-engine'
|
|
2
|
-
import type { QueryExecuteOptions } from '@dhis2/data-engine'
|
|
3
|
-
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
4
|
-
import { ExecuteHookInput, ExecuteHookResult } from '../../types'
|
|
5
|
-
import { useStaticInput } from './useStaticInput'
|
|
6
|
-
|
|
7
|
-
interface StateType<T> {
|
|
8
|
-
called: boolean
|
|
9
|
-
loading: boolean
|
|
10
|
-
error?: FetchError
|
|
11
|
-
data?: T
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const useQueryExecutor = <ReturnType>({
|
|
15
|
-
execute,
|
|
16
|
-
variables: initialVariables,
|
|
17
|
-
singular,
|
|
18
|
-
immediate,
|
|
19
|
-
onComplete,
|
|
20
|
-
onError,
|
|
21
|
-
}: ExecuteHookInput<ReturnType>): ExecuteHookResult<ReturnType> => {
|
|
22
|
-
const [theExecute] = useStaticInput(execute)
|
|
23
|
-
const [state, setState] = useState<StateType<ReturnType>>({
|
|
24
|
-
called: !!immediate,
|
|
25
|
-
loading: !!immediate,
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
const variables = useRef(initialVariables)
|
|
29
|
-
|
|
30
|
-
const abortControllersRef = useRef<AbortController[]>([])
|
|
31
|
-
const abort = useCallback(() => {
|
|
32
|
-
abortControllersRef.current.forEach((controller) => controller.abort())
|
|
33
|
-
abortControllersRef.current = []
|
|
34
|
-
}, [])
|
|
35
|
-
|
|
36
|
-
const manualAbort = useCallback(() => {
|
|
37
|
-
abort()
|
|
38
|
-
setState((state) => ({
|
|
39
|
-
called: state.called,
|
|
40
|
-
loading: false,
|
|
41
|
-
error: new FetchError({ type: 'aborted', message: 'Aborted' }),
|
|
42
|
-
}))
|
|
43
|
-
}, [abort])
|
|
44
|
-
|
|
45
|
-
const refetch = useCallback(
|
|
46
|
-
(newVariables = {}) => {
|
|
47
|
-
setState((state) =>
|
|
48
|
-
!state.called || !state.loading
|
|
49
|
-
? { called: true, loading: true }
|
|
50
|
-
: state
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
if (singular) {
|
|
54
|
-
abort() // Cleanup any in-progress fetches
|
|
55
|
-
}
|
|
56
|
-
const controller = new AbortController()
|
|
57
|
-
abortControllersRef.current.push(controller)
|
|
58
|
-
|
|
59
|
-
variables.current = {
|
|
60
|
-
...variables.current,
|
|
61
|
-
...newVariables,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const options: QueryExecuteOptions = {
|
|
65
|
-
variables: variables.current,
|
|
66
|
-
signal: controller.signal,
|
|
67
|
-
onComplete,
|
|
68
|
-
onError,
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return theExecute(options)
|
|
72
|
-
.then((data: ReturnType) => {
|
|
73
|
-
if (!controller.signal.aborted) {
|
|
74
|
-
setState({ called: true, loading: false, data })
|
|
75
|
-
return data
|
|
76
|
-
}
|
|
77
|
-
return new Promise<ReturnType>(() => undefined) // Wait forever
|
|
78
|
-
})
|
|
79
|
-
.catch((error: FetchError) => {
|
|
80
|
-
if (!controller.signal.aborted) {
|
|
81
|
-
setState({ called: true, loading: false, error })
|
|
82
|
-
}
|
|
83
|
-
return new Promise<ReturnType>(() => undefined) // Don't throw errors in refetch promises, wait forever
|
|
84
|
-
})
|
|
85
|
-
},
|
|
86
|
-
[abort, onComplete, onError, singular, theExecute]
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
// Don't include immediate or refetch as deps, otherwise unintentional refetches
|
|
90
|
-
// may be triggered by changes to input, i.e. recreating the onComplete callback
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
if (immediate) {
|
|
93
|
-
refetch()
|
|
94
|
-
}
|
|
95
|
-
return abort
|
|
96
|
-
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
97
|
-
|
|
98
|
-
return { refetch, abort: manualAbort, ...state }
|
|
99
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { renderHook, act } from '@testing-library/react'
|
|
2
|
-
import { useStaticInput } from './useStaticInput'
|
|
3
|
-
|
|
4
|
-
describe('useStaticInput', () => {
|
|
5
|
-
const originalWarn = console.warn
|
|
6
|
-
const mockWarn = jest.fn()
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
jest.clearAllMocks()
|
|
9
|
-
console.warn = mockWarn
|
|
10
|
-
})
|
|
11
|
-
afterEach(() => (console.warn = originalWarn))
|
|
12
|
-
|
|
13
|
-
it('Should pass without warnings on first render', () => {
|
|
14
|
-
const { result } = renderHook(() => useStaticInput(42))
|
|
15
|
-
expect(result.current[0]).toBe(42)
|
|
16
|
-
expect(mockWarn).not.toHaveBeenCalled()
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('Should refuse update on updated render', () => {
|
|
20
|
-
const { result, rerender } = renderHook(
|
|
21
|
-
({ value }) => useStaticInput(value),
|
|
22
|
-
{ initialProps: { value: 42 } }
|
|
23
|
-
)
|
|
24
|
-
expect(result.current[0]).toBe(42)
|
|
25
|
-
rerender({ value: 54 })
|
|
26
|
-
expect(mockWarn).not.toHaveBeenCalled()
|
|
27
|
-
expect(result.current[0]).toBe(42)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('Should show a warning on updated render (with warn prop)', () => {
|
|
31
|
-
const { result, rerender } = renderHook(
|
|
32
|
-
({ value }) => useStaticInput(value, { warn: true }),
|
|
33
|
-
{ initialProps: { value: 42 } }
|
|
34
|
-
)
|
|
35
|
-
expect(result.current[0]).toBe(42)
|
|
36
|
-
rerender({ value: 54 })
|
|
37
|
-
expect(mockWarn).toHaveBeenCalled()
|
|
38
|
-
expect(mockWarn.mock.calls.pop()).toMatchInlineSnapshot(`
|
|
39
|
-
Array [
|
|
40
|
-
"The input should be static, don't create it within the render loop!",
|
|
41
|
-
]
|
|
42
|
-
`)
|
|
43
|
-
expect(result.current[0]).toBe(42)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('Should show custom name in warning if supplied', () => {
|
|
47
|
-
const { result, rerender } = renderHook(
|
|
48
|
-
({ value }) =>
|
|
49
|
-
useStaticInput(value, { warn: true, name: 'TESTING THING' }),
|
|
50
|
-
{ initialProps: { value: 42 } }
|
|
51
|
-
)
|
|
52
|
-
expect(result.current[0]).toBe(42)
|
|
53
|
-
rerender({ value: 54 })
|
|
54
|
-
expect(mockWarn).toHaveBeenCalled()
|
|
55
|
-
expect(mockWarn.mock.calls.pop()[0]).toMatch(/TESTING THING/g)
|
|
56
|
-
expect(result.current[0]).toBe(42)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('Should update when explicitly set to update', async () => {
|
|
60
|
-
const { result, rerender } = renderHook(
|
|
61
|
-
({ value }) => useStaticInput(value, { warn: true }),
|
|
62
|
-
{
|
|
63
|
-
initialProps: { value: 42 },
|
|
64
|
-
}
|
|
65
|
-
)
|
|
66
|
-
const [value, setValue] = result.current
|
|
67
|
-
expect(value).toBe(42)
|
|
68
|
-
|
|
69
|
-
act(() => {
|
|
70
|
-
setValue(54)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
expect(result.current[0]).toBe(54)
|
|
74
|
-
expect(mockWarn).not.toHaveBeenCalled()
|
|
75
|
-
|
|
76
|
-
rerender({ value: 54 })
|
|
77
|
-
expect(result.current[0]).toBe(54)
|
|
78
|
-
expect(mockWarn).toHaveBeenCalled()
|
|
79
|
-
|
|
80
|
-
rerender({ value: 75 })
|
|
81
|
-
expect(result.current[0]).toBe(54)
|
|
82
|
-
expect(mockWarn).toHaveBeenCalledTimes(2)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('Should support functional values', () => {
|
|
86
|
-
const fn = jest.fn()
|
|
87
|
-
const fn2 = jest.fn()
|
|
88
|
-
const { result, rerender } = renderHook(
|
|
89
|
-
({ value }) => useStaticInput(value, { warn: true }),
|
|
90
|
-
{
|
|
91
|
-
initialProps: { value: fn },
|
|
92
|
-
}
|
|
93
|
-
)
|
|
94
|
-
expect(fn).not.toHaveBeenCalled()
|
|
95
|
-
expect(result.current[0]).toBe(fn)
|
|
96
|
-
result.current[0]()
|
|
97
|
-
expect(fn).toHaveBeenCalled()
|
|
98
|
-
rerender({ value: fn2 })
|
|
99
|
-
expect(result.current[0]).toBe(fn)
|
|
100
|
-
})
|
|
101
|
-
})
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef, useDebugValue } from 'react'
|
|
2
|
-
|
|
3
|
-
interface StaticInputOptions {
|
|
4
|
-
warn?: boolean
|
|
5
|
-
name?: string
|
|
6
|
-
}
|
|
7
|
-
export const useStaticInput = <T>(
|
|
8
|
-
staticValue: T,
|
|
9
|
-
{ warn = false, name = 'input' }: StaticInputOptions = {}
|
|
10
|
-
): [T, React.Dispatch<React.SetStateAction<T>>] => {
|
|
11
|
-
const originalValue = useRef(staticValue)
|
|
12
|
-
const [value, setValue] = useState<T>(() => originalValue.current)
|
|
13
|
-
|
|
14
|
-
useDebugValue(
|
|
15
|
-
value,
|
|
16
|
-
(debugValue) => `${name}: ${JSON.stringify(debugValue)}`
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (warn && originalValue.current !== staticValue) {
|
|
21
|
-
console.warn(
|
|
22
|
-
`The ${name} should be static, don't create it within the render loop!`
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
}, [warn, staticValue, originalValue, name])
|
|
26
|
-
return [value, setValue]
|
|
27
|
-
}
|
package/src/react/index.ts
DELETED
package/src/setupRTL.ts
DELETED
package/src/types.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
DataEngine,
|
|
3
|
-
QueryExecuteOptions,
|
|
4
|
-
FetchError,
|
|
5
|
-
JsonValue,
|
|
6
|
-
QueryVariables,
|
|
7
|
-
QueryResult,
|
|
8
|
-
} from '@dhis2/data-engine'
|
|
9
|
-
|
|
10
|
-
export type { Mutation, Query } from '@dhis2/data-engine'
|
|
11
|
-
|
|
12
|
-
export interface ContextType {
|
|
13
|
-
engine: DataEngine
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ContextInput {
|
|
17
|
-
baseUrl: string
|
|
18
|
-
apiVersion: number
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type ExecuteOptions = QueryExecuteOptions
|
|
22
|
-
export type RefetchOptions = QueryVariables
|
|
23
|
-
export type RefetchFunction<ReturnType> = (
|
|
24
|
-
options?: RefetchOptions
|
|
25
|
-
) => Promise<ReturnType>
|
|
26
|
-
export type QueryRefetchFunction = RefetchFunction<QueryResult>
|
|
27
|
-
export type MutationFunction = RefetchFunction<JsonValue>
|
|
28
|
-
|
|
29
|
-
export type ExecuteFunction<T> = (options: QueryExecuteOptions) => Promise<T>
|
|
30
|
-
export interface ExecuteHookInput<ReturnType> {
|
|
31
|
-
execute: ExecuteFunction<ReturnType>
|
|
32
|
-
variables: QueryVariables
|
|
33
|
-
singular: boolean
|
|
34
|
-
immediate: boolean
|
|
35
|
-
transformData?: (data: JsonValue[]) => JsonValue
|
|
36
|
-
onComplete?: (data: any) => void
|
|
37
|
-
onError?: (error: FetchError) => void
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface ExecuteHookResult<ReturnType> {
|
|
41
|
-
refetch: RefetchFunction<ReturnType>
|
|
42
|
-
abort: () => void
|
|
43
|
-
called: boolean
|
|
44
|
-
loading: boolean
|
|
45
|
-
error?: FetchError
|
|
46
|
-
data?: ReturnType
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface QueryState<TQueryResult> {
|
|
50
|
-
called: boolean
|
|
51
|
-
loading: boolean
|
|
52
|
-
fetching: boolean
|
|
53
|
-
error?: FetchError
|
|
54
|
-
data?: TQueryResult
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface QueryRenderInput<
|
|
58
|
-
TQueryResult = QueryResult,
|
|
59
|
-
> extends QueryState<TQueryResult> {
|
|
60
|
-
engine: DataEngine
|
|
61
|
-
refetch: QueryRefetchFunction
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface MutationState {
|
|
65
|
-
engine: DataEngine
|
|
66
|
-
called: boolean
|
|
67
|
-
loading: boolean
|
|
68
|
-
error?: FetchError
|
|
69
|
-
data?: JsonValue
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export type MutationRenderInput = [MutationFunction, MutationState]
|