@openmrs/esm-react-utils 10.0.1-pre.5240 → 10.0.1-pre.5251
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/.turbo/turbo-build.log +1 -1
- package/dist/useVisit.d.ts.map +1 -1
- package/dist/useVisit.js +12 -2
- package/dist/useVisitContextStore.d.ts +17 -5
- package/dist/useVisitContextStore.d.ts.map +1 -1
- package/dist/useVisitContextStore.js +27 -28
- package/package.json +23 -23
- package/src/useVisit.ts +14 -2
- package/src/useVisitContextStore.test.tsx +128 -0
- package/src/useVisitContextStore.ts +29 -25
package/.turbo/turbo-build.log
CHANGED
package/dist/useVisit.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useVisit.d.ts","sourceRoot":"","sources":["../src/useVisit.ts"],"names":[],"mappings":"AAMA,OAAO,EAAoC,KAAK,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAKpF,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,KAAK,GAAG,IAAI,CAAC;IAE1B;;;OAGG;IACH,YAAY,EAAE,KAAK,GAAG,IAAI,CAAC;IAE3B;;;OAGG;IACH,2BAA2B,EAAE,OAAO,CAAC;IACrC,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,SAAmC,GAAG,eAAe,
|
|
1
|
+
{"version":3,"file":"useVisit.d.ts","sourceRoot":"","sources":["../src/useVisit.ts"],"names":[],"mappings":"AAMA,OAAO,EAAoC,KAAK,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAKpF,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,KAAK,GAAG,IAAI,CAAC;IAE1B;;;OAGG;IACH,YAAY,EAAE,KAAK,GAAG,IAAI,CAAC;IAE3B;;;OAGG;IACH,2BAA2B,EAAE,OAAO,CAAC;IACrC,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,SAAmC,GAAG,eAAe,CAyFhH"}
|
package/dist/useVisit.js
CHANGED
|
@@ -26,7 +26,13 @@ dayjs.extend(isToday);
|
|
|
26
26
|
* @param patientUuid Unique patient identifier `string`
|
|
27
27
|
* @param representation The custom representation of the visit
|
|
28
28
|
*/ export function useVisit(patientUuid, representation = defaultVisitCustomRepresentation) {
|
|
29
|
-
|
|
29
|
+
// The callback we register has to exist before we read the store, but the mutate functions
|
|
30
|
+
// it calls come from SWR calls that are keyed off what the store holds. Registering a stable
|
|
31
|
+
// indirection through a ref breaks that cycle, so the store is read once per hook rather than
|
|
32
|
+
// once to get the key and again to register.
|
|
33
|
+
const mutateVisitRef = useRef(undefined);
|
|
34
|
+
const registeredMutateVisit = useCallback(()=>mutateVisitRef.current?.(), []);
|
|
35
|
+
const { patientUuid: visitStorePatientUuid, manuallySetVisitUuid, setVisitContext } = useVisitContextStore(registeredMutateVisit);
|
|
30
36
|
// Ignore the visit store data if it is not for this patient
|
|
31
37
|
const retrospectiveVisitUuid = patientUuid && visitStorePatientUuid == patientUuid ? manuallySetVisitUuid : null;
|
|
32
38
|
const activeVisitUrlSuffix = `?patient=${patientUuid}&v=${representation}&includeInactive=false`;
|
|
@@ -69,7 +75,11 @@ dayjs.extend(isToday);
|
|
|
69
75
|
activeMutate,
|
|
70
76
|
retroMutate
|
|
71
77
|
]);
|
|
72
|
-
|
|
78
|
+
useEffect(()=>{
|
|
79
|
+
mutateVisitRef.current = mutateVisit;
|
|
80
|
+
}, [
|
|
81
|
+
mutateVisit
|
|
82
|
+
]);
|
|
73
83
|
const waitingForData = Boolean(!activeData || retrospectiveVisitUuid && !retroData);
|
|
74
84
|
const hasRelevantError = Boolean(retrospectiveVisitUuid ? retroError : activeError);
|
|
75
85
|
const isLoading = waitingForData && !hasRelevantError;
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import { type Visit
|
|
1
|
+
import { type Visit } from '@openmrs/esm-emr-api';
|
|
2
|
+
/**
|
|
3
|
+
* Invokes every registered mutate callback, revalidating visit data across the application.
|
|
4
|
+
*
|
|
5
|
+
* Iterates over a copy so that a callback which registers or unregisters another does not
|
|
6
|
+
* disturb the traversal.
|
|
7
|
+
*/
|
|
8
|
+
declare function mutateVisit(): void;
|
|
2
9
|
/**
|
|
3
10
|
* A hook to return the visit context store and corresponding actions.
|
|
11
|
+
*
|
|
4
12
|
* @param mutateVisitCallback An optional mutate callback to register. If provided, the
|
|
5
|
-
*
|
|
6
|
-
* callbacks
|
|
13
|
+
* returned `mutateVisit` function will invoke this callback (along with any other
|
|
14
|
+
* callbacks registered by other components). Pass a callback with a stable identity;
|
|
15
|
+
* one that changes on every render re-registers on every render.
|
|
7
16
|
* @returns
|
|
8
17
|
*/
|
|
9
|
-
export declare function useVisitContextStore(mutateVisitCallback?: () => void):
|
|
18
|
+
export declare function useVisitContextStore(mutateVisitCallback?: () => void): {
|
|
19
|
+
mutateVisit: typeof mutateVisit;
|
|
20
|
+
patientUuid: string | null;
|
|
21
|
+
manuallySetVisitUuid: string | null;
|
|
10
22
|
setVisitContext: (newSelectedVisit: Visit | null) => void;
|
|
11
|
-
mutateVisit: () => void;
|
|
12
23
|
};
|
|
24
|
+
export {};
|
|
13
25
|
//# sourceMappingURL=useVisitContextStore.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useVisitContextStore.d.ts","sourceRoot":"","sources":["../src/useVisitContextStore.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,KAAK,
|
|
1
|
+
{"version":3,"file":"useVisitContextStore.d.ts","sourceRoot":"","sources":["../src/useVisitContextStore.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,KAAK,EAAwB,MAAM,sBAAsB,CAAC;AASvF;;;;;GAKG;AACH,iBAAS,WAAW,SAInB;AAcD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,mBAAmB,CAAC,EAAE,MAAM,IAAI;;;;;EAiBpE"}
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { useEffect, useId } from "react";
|
|
2
2
|
import { getVisitStore } from "@openmrs/esm-emr-api";
|
|
3
3
|
import { useStoreWithActions } from "./useStore.js";
|
|
4
|
+
/**
|
|
5
|
+
* The mutate callbacks to invoke when visit data changes, keyed by the `useId()` of the
|
|
6
|
+
* component that registered each one.
|
|
7
|
+
*/ const mutateVisitCallbacks = new Map();
|
|
8
|
+
/**
|
|
9
|
+
* Invokes every registered mutate callback, revalidating visit data across the application.
|
|
10
|
+
*
|
|
11
|
+
* Iterates over a copy so that a callback which registers or unregisters another does not
|
|
12
|
+
* disturb the traversal.
|
|
13
|
+
*/ function mutateVisit() {
|
|
14
|
+
for (const mutateCallback of Array.from(mutateVisitCallbacks.values())){
|
|
15
|
+
mutateCallback();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
4
18
|
const visitContextStoreActions = {
|
|
5
19
|
setVisitContext (_, newSelectedVisit) {
|
|
6
20
|
if (newSelectedVisit == null) {
|
|
@@ -12,48 +26,33 @@ const visitContextStoreActions = {
|
|
|
12
26
|
manuallySetVisitUuid: newSelectedVisit.uuid,
|
|
13
27
|
patientUuid: newSelectedVisit.patient?.uuid
|
|
14
28
|
};
|
|
15
|
-
},
|
|
16
|
-
mutateVisit (currState) {
|
|
17
|
-
for (const mutateCallback of Object.values(currState.mutateVisitCallbacks ?? {})){
|
|
18
|
-
mutateCallback();
|
|
19
|
-
}
|
|
20
|
-
return {};
|
|
21
29
|
}
|
|
22
30
|
};
|
|
23
31
|
/**
|
|
24
32
|
* A hook to return the visit context store and corresponding actions.
|
|
33
|
+
*
|
|
25
34
|
* @param mutateVisitCallback An optional mutate callback to register. If provided, the
|
|
26
|
-
*
|
|
27
|
-
* callbacks
|
|
35
|
+
* returned `mutateVisit` function will invoke this callback (along with any other
|
|
36
|
+
* callbacks registered by other components). Pass a callback with a stable identity;
|
|
37
|
+
* one that changes on every render re-registers on every render.
|
|
28
38
|
* @returns
|
|
29
39
|
*/ export function useVisitContextStore(mutateVisitCallback) {
|
|
30
40
|
const id = useId();
|
|
31
41
|
useEffect(()=>{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (mutateVisitCallback) {
|
|
35
|
-
visitStore.setState({
|
|
36
|
-
mutateVisitCallbacks: {
|
|
37
|
-
...visitStore.getState().mutateVisitCallbacks,
|
|
38
|
-
[id]: mutateVisitCallback
|
|
39
|
-
}
|
|
40
|
-
});
|
|
42
|
+
if (!mutateVisitCallback) {
|
|
43
|
+
return;
|
|
41
44
|
}
|
|
42
|
-
|
|
45
|
+
mutateVisitCallbacks.set(id, mutateVisitCallback);
|
|
43
46
|
return ()=>{
|
|
44
|
-
|
|
45
|
-
const mutateVisitCallbacks = {
|
|
46
|
-
...visitStore.getState().mutateVisitCallbacks
|
|
47
|
-
};
|
|
48
|
-
delete mutateVisitCallbacks[id];
|
|
49
|
-
visitStore.setState({
|
|
50
|
-
mutateVisitCallbacks
|
|
51
|
-
});
|
|
52
|
-
}
|
|
47
|
+
mutateVisitCallbacks.delete(id);
|
|
53
48
|
};
|
|
54
49
|
}, [
|
|
55
50
|
id,
|
|
56
51
|
mutateVisitCallback
|
|
57
52
|
]);
|
|
58
|
-
|
|
53
|
+
const visitContextStore = useStoreWithActions(getVisitStore(), visitContextStoreActions);
|
|
54
|
+
return {
|
|
55
|
+
...visitContextStore,
|
|
56
|
+
mutateVisit
|
|
57
|
+
};
|
|
59
58
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-react-utils",
|
|
3
|
-
"version": "10.0.1-pre.
|
|
3
|
+
"version": "10.0.1-pre.5251",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "React utilities for OpenMRS.",
|
|
6
6
|
"type": "module",
|
|
@@ -59,17 +59,17 @@
|
|
|
59
59
|
"single-spa-react": "^6.0.2"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@openmrs/esm-api": "^10.0.1-pre.
|
|
63
|
-
"@openmrs/esm-config": "^10.0.1-pre.
|
|
64
|
-
"@openmrs/esm-context": "^10.0.1-pre.
|
|
65
|
-
"@openmrs/esm-emr-api": "^10.0.1-pre.
|
|
66
|
-
"@openmrs/esm-error-handling": "^10.0.1-pre.
|
|
67
|
-
"@openmrs/esm-extensions": "^10.0.1-pre.
|
|
68
|
-
"@openmrs/esm-feature-flags": "^10.0.1-pre.
|
|
69
|
-
"@openmrs/esm-globals": "^10.0.1-pre.
|
|
70
|
-
"@openmrs/esm-navigation": "^10.0.1-pre.
|
|
71
|
-
"@openmrs/esm-state": "^10.0.1-pre.
|
|
72
|
-
"@openmrs/esm-utils": "^10.0.1-pre.
|
|
62
|
+
"@openmrs/esm-api": "^10.0.1-pre.5251",
|
|
63
|
+
"@openmrs/esm-config": "^10.0.1-pre.5251",
|
|
64
|
+
"@openmrs/esm-context": "^10.0.1-pre.5251",
|
|
65
|
+
"@openmrs/esm-emr-api": "^10.0.1-pre.5251",
|
|
66
|
+
"@openmrs/esm-error-handling": "^10.0.1-pre.5251",
|
|
67
|
+
"@openmrs/esm-extensions": "^10.0.1-pre.5251",
|
|
68
|
+
"@openmrs/esm-feature-flags": "^10.0.1-pre.5251",
|
|
69
|
+
"@openmrs/esm-globals": "^10.0.1-pre.5251",
|
|
70
|
+
"@openmrs/esm-navigation": "^10.0.1-pre.5251",
|
|
71
|
+
"@openmrs/esm-state": "^10.0.1-pre.5251",
|
|
72
|
+
"@openmrs/esm-utils": "^10.0.1-pre.5251",
|
|
73
73
|
"dayjs": "1.x",
|
|
74
74
|
"i18next": "25.x",
|
|
75
75
|
"react": "18.x",
|
|
@@ -79,17 +79,17 @@
|
|
|
79
79
|
"swr": "2.x"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@openmrs/esm-api": "10.0.1-pre.
|
|
83
|
-
"@openmrs/esm-config": "10.0.1-pre.
|
|
84
|
-
"@openmrs/esm-context": "10.0.1-pre.
|
|
85
|
-
"@openmrs/esm-emr-api": "10.0.1-pre.
|
|
86
|
-
"@openmrs/esm-error-handling": "10.0.1-pre.
|
|
87
|
-
"@openmrs/esm-extensions": "10.0.1-pre.
|
|
88
|
-
"@openmrs/esm-feature-flags": "10.0.1-pre.
|
|
89
|
-
"@openmrs/esm-globals": "10.0.1-pre.
|
|
90
|
-
"@openmrs/esm-navigation": "10.0.1-pre.
|
|
91
|
-
"@openmrs/esm-state": "10.0.1-pre.
|
|
92
|
-
"@openmrs/esm-utils": "10.0.1-pre.
|
|
82
|
+
"@openmrs/esm-api": "10.0.1-pre.5251",
|
|
83
|
+
"@openmrs/esm-config": "10.0.1-pre.5251",
|
|
84
|
+
"@openmrs/esm-context": "10.0.1-pre.5251",
|
|
85
|
+
"@openmrs/esm-emr-api": "10.0.1-pre.5251",
|
|
86
|
+
"@openmrs/esm-error-handling": "10.0.1-pre.5251",
|
|
87
|
+
"@openmrs/esm-extensions": "10.0.1-pre.5251",
|
|
88
|
+
"@openmrs/esm-feature-flags": "10.0.1-pre.5251",
|
|
89
|
+
"@openmrs/esm-globals": "10.0.1-pre.5251",
|
|
90
|
+
"@openmrs/esm-navigation": "10.0.1-pre.5251",
|
|
91
|
+
"@openmrs/esm-state": "10.0.1-pre.5251",
|
|
92
|
+
"@openmrs/esm-utils": "10.0.1-pre.5251",
|
|
93
93
|
"@swc/cli": "0.8.1",
|
|
94
94
|
"@swc/core": "1.15.21",
|
|
95
95
|
"@vitest/coverage-v8": "^4.1.2",
|
package/src/useVisit.ts
CHANGED
|
@@ -50,7 +50,17 @@ export interface VisitReturnType {
|
|
|
50
50
|
* @param representation The custom representation of the visit
|
|
51
51
|
*/
|
|
52
52
|
export function useVisit(patientUuid: string, representation = defaultVisitCustomRepresentation): VisitReturnType {
|
|
53
|
-
|
|
53
|
+
// The callback we register has to exist before we read the store, but the mutate functions
|
|
54
|
+
// it calls come from SWR calls that are keyed off what the store holds. Registering a stable
|
|
55
|
+
// indirection through a ref breaks that cycle, so the store is read once per hook rather than
|
|
56
|
+
// once to get the key and again to register.
|
|
57
|
+
const mutateVisitRef = useRef<() => void>(undefined);
|
|
58
|
+
const registeredMutateVisit = useCallback(() => mutateVisitRef.current?.(), []);
|
|
59
|
+
const {
|
|
60
|
+
patientUuid: visitStorePatientUuid,
|
|
61
|
+
manuallySetVisitUuid,
|
|
62
|
+
setVisitContext,
|
|
63
|
+
} = useVisitContextStore(registeredMutateVisit);
|
|
54
64
|
// Ignore the visit store data if it is not for this patient
|
|
55
65
|
const retrospectiveVisitUuid = patientUuid && visitStorePatientUuid == patientUuid ? manuallySetVisitUuid : null;
|
|
56
66
|
const activeVisitUrlSuffix = `?patient=${patientUuid}&v=${representation}&includeInactive=false`;
|
|
@@ -111,7 +121,9 @@ export function useVisit(patientUuid: string, representation = defaultVisitCusto
|
|
|
111
121
|
retroMutate();
|
|
112
122
|
}, [activeMutate, retroMutate]);
|
|
113
123
|
|
|
114
|
-
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
mutateVisitRef.current = mutateVisit;
|
|
126
|
+
}, [mutateVisit]);
|
|
115
127
|
|
|
116
128
|
const waitingForData = Boolean(!activeData || (retrospectiveVisitUuid && !retroData));
|
|
117
129
|
const hasRelevantError = Boolean(retrospectiveVisitUuid ? retroError : activeError);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { act, render, renderHook } from '@testing-library/react';
|
|
4
|
+
import { getVisitStore } from '@openmrs/esm-emr-api';
|
|
5
|
+
import { useVisitContextStore } from './useVisitContextStore';
|
|
6
|
+
|
|
7
|
+
/** Counts calls to `setState` on the visit store for the duration of `run`. */
|
|
8
|
+
function countStoreWrites(run: () => void) {
|
|
9
|
+
const store = getVisitStore();
|
|
10
|
+
const setState = store.setState;
|
|
11
|
+
let writes = 0;
|
|
12
|
+
store.setState = ((...args: Parameters<typeof setState>) => {
|
|
13
|
+
writes++;
|
|
14
|
+
return setState(...args);
|
|
15
|
+
}) as typeof setState;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
run();
|
|
19
|
+
} finally {
|
|
20
|
+
store.setState = setState;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return writes;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('useVisitContextStore', () => {
|
|
27
|
+
it('invokes every registered callback when mutateVisit is called', () => {
|
|
28
|
+
const first = vi.fn();
|
|
29
|
+
const second = vi.fn();
|
|
30
|
+
|
|
31
|
+
const { result } = renderHook(() => useVisitContextStore(first));
|
|
32
|
+
renderHook(() => useVisitContextStore(second));
|
|
33
|
+
|
|
34
|
+
act(() => result.current.mutateVisit());
|
|
35
|
+
|
|
36
|
+
expect(first).toHaveBeenCalledTimes(1);
|
|
37
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('stops invoking a callback once its component unmounts', () => {
|
|
41
|
+
const callback = vi.fn();
|
|
42
|
+
|
|
43
|
+
const { result } = renderHook(() => useVisitContextStore());
|
|
44
|
+
const { unmount } = renderHook(() => useVisitContextStore(callback));
|
|
45
|
+
|
|
46
|
+
act(() => result.current.mutateVisit());
|
|
47
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
48
|
+
|
|
49
|
+
unmount();
|
|
50
|
+
act(() => result.current.mutateVisit());
|
|
51
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does not write to the store when registering or unregistering a callback', () => {
|
|
55
|
+
let unmount = () => {};
|
|
56
|
+
|
|
57
|
+
const mountWrites = countStoreWrites(() => {
|
|
58
|
+
unmount = renderHook(() => useVisitContextStore(() => {})).unmount;
|
|
59
|
+
});
|
|
60
|
+
expect(mountWrites).toBe(0);
|
|
61
|
+
|
|
62
|
+
expect(countStoreWrites(unmount)).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('does not write to the store when mutateVisit is called', () => {
|
|
66
|
+
const { result } = renderHook(() => useVisitContextStore(() => {}));
|
|
67
|
+
|
|
68
|
+
expect(countStoreWrites(() => act(() => result.current.mutateVisit()))).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('keeps mutate callbacks out of the persisted store state', () => {
|
|
72
|
+
renderHook(() => useVisitContextStore(() => {}));
|
|
73
|
+
|
|
74
|
+
act(() => {
|
|
75
|
+
getVisitStore().setState({ patientUuid: 'patient-123' });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const persisted = JSON.parse(window.sessionStorage.getItem('openmrs:visitStoreState') ?? '{}');
|
|
79
|
+
expect(persisted).toEqual({ patientUuid: 'patient-123', manuallySetVisitUuid: null });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('does not re-render already-mounted consumers as more consumers mount', () => {
|
|
83
|
+
const renderCounts: Array<number> = [];
|
|
84
|
+
|
|
85
|
+
// Memoized so that re-rendering the parent to add a row does not itself re-render the
|
|
86
|
+
// existing rows; anything counted here came from the store notifying its subscribers.
|
|
87
|
+
const Row = React.memo(function Row({ index }: { index: number }) {
|
|
88
|
+
renderCounts[index] = (renderCounts[index] ?? 0) + 1;
|
|
89
|
+
const mutate = useCallback(() => {}, []);
|
|
90
|
+
useVisitContextStore(mutate);
|
|
91
|
+
return null;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function Screen({ count }: { count: number }) {
|
|
95
|
+
return (
|
|
96
|
+
<>
|
|
97
|
+
{Array.from({ length: count }, (_, index) => (
|
|
98
|
+
<Row index={index} key={index} />
|
|
99
|
+
))}
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const rows = 15;
|
|
105
|
+
const { rerender } = render(<Screen count={0} />);
|
|
106
|
+
for (let count = 1; count <= rows; count++) {
|
|
107
|
+
rerender(<Screen count={count} />);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Each row should have rendered exactly once: mounting a row must not notify the rows
|
|
111
|
+
// already on screen, or filling a screen of N rows costs O(N²) renders.
|
|
112
|
+
expect(renderCounts).toEqual(Array.from({ length: rows }, () => 1));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('re-renders consumers when the visit context itself changes', () => {
|
|
116
|
+
const { result } = renderHook(() => useVisitContextStore());
|
|
117
|
+
|
|
118
|
+
act(() => {
|
|
119
|
+
result.current.setVisitContext({
|
|
120
|
+
uuid: 'visit-1',
|
|
121
|
+
patient: { uuid: 'patient-123' },
|
|
122
|
+
} as Parameters<typeof result.current.setVisitContext>[0]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(result.current.manuallySetVisitUuid).toBe('visit-1');
|
|
126
|
+
expect(result.current.patientUuid).toBe('patient-123');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -2,6 +2,24 @@ import { useEffect, useId } from 'react';
|
|
|
2
2
|
import { getVisitStore, type Visit, type VisitStoreState } from '@openmrs/esm-emr-api';
|
|
3
3
|
import { type Actions, useStoreWithActions } from './useStore';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The mutate callbacks to invoke when visit data changes, keyed by the `useId()` of the
|
|
7
|
+
* component that registered each one.
|
|
8
|
+
*/
|
|
9
|
+
const mutateVisitCallbacks = new Map<string, () => void>();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Invokes every registered mutate callback, revalidating visit data across the application.
|
|
13
|
+
*
|
|
14
|
+
* Iterates over a copy so that a callback which registers or unregisters another does not
|
|
15
|
+
* disturb the traversal.
|
|
16
|
+
*/
|
|
17
|
+
function mutateVisit() {
|
|
18
|
+
for (const mutateCallback of Array.from(mutateVisitCallbacks.values())) {
|
|
19
|
+
mutateCallback();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
const visitContextStoreActions = {
|
|
6
24
|
setVisitContext(_: VisitStoreState, newSelectedVisit: Visit | null) {
|
|
7
25
|
if (newSelectedVisit == null) {
|
|
@@ -12,46 +30,32 @@ const visitContextStoreActions = {
|
|
|
12
30
|
patientUuid: newSelectedVisit.patient?.uuid,
|
|
13
31
|
};
|
|
14
32
|
},
|
|
15
|
-
mutateVisit(currState: VisitStoreState) {
|
|
16
|
-
for (const mutateCallback of Object.values(currState.mutateVisitCallbacks ?? {})) {
|
|
17
|
-
mutateCallback();
|
|
18
|
-
}
|
|
19
|
-
return {};
|
|
20
|
-
},
|
|
21
33
|
} satisfies Actions<VisitStoreState>;
|
|
22
34
|
|
|
23
35
|
/**
|
|
24
36
|
* A hook to return the visit context store and corresponding actions.
|
|
37
|
+
*
|
|
25
38
|
* @param mutateVisitCallback An optional mutate callback to register. If provided, the
|
|
26
|
-
*
|
|
27
|
-
* callbacks
|
|
39
|
+
* returned `mutateVisit` function will invoke this callback (along with any other
|
|
40
|
+
* callbacks registered by other components). Pass a callback with a stable identity;
|
|
41
|
+
* one that changes on every render re-registers on every render.
|
|
28
42
|
* @returns
|
|
29
43
|
*/
|
|
30
44
|
export function useVisitContextStore(mutateVisitCallback?: () => void) {
|
|
31
45
|
const id = useId();
|
|
32
46
|
|
|
33
47
|
useEffect(() => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// register the callback if it exists
|
|
37
|
-
if (mutateVisitCallback) {
|
|
38
|
-
visitStore.setState({
|
|
39
|
-
mutateVisitCallbacks: {
|
|
40
|
-
...visitStore.getState().mutateVisitCallbacks,
|
|
41
|
-
[id]: mutateVisitCallback,
|
|
42
|
-
},
|
|
43
|
-
});
|
|
48
|
+
if (!mutateVisitCallback) {
|
|
49
|
+
return;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
mutateVisitCallbacks.set(id, mutateVisitCallback);
|
|
47
53
|
return () => {
|
|
48
|
-
|
|
49
|
-
const mutateVisitCallbacks = { ...visitStore.getState().mutateVisitCallbacks };
|
|
50
|
-
delete mutateVisitCallbacks[id];
|
|
51
|
-
visitStore.setState({ mutateVisitCallbacks });
|
|
52
|
-
}
|
|
54
|
+
mutateVisitCallbacks.delete(id);
|
|
53
55
|
};
|
|
54
56
|
}, [id, mutateVisitCallback]);
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
const visitContextStore = useStoreWithActions(getVisitStore(), visitContextStoreActions);
|
|
59
|
+
|
|
60
|
+
return { ...visitContextStore, mutateVisit };
|
|
57
61
|
}
|