@openmrs/esm-react-utils 9.0.3-pre.4556 → 9.0.3-pre.4622
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/useAppContext.d.ts +31 -0
- package/dist/useAppContext.d.ts.map +1 -1
- package/dist/useAppContext.js +25 -31
- package/package.json +12 -12
- package/src/useAppContext.test.tsx +104 -0
- package/src/useAppContext.ts +35 -9
package/.turbo/turbo-build.log
CHANGED
package/dist/useAppContext.d.ts
CHANGED
|
@@ -17,6 +17,37 @@
|
|
|
17
17
|
*
|
|
18
18
|
* @typeParam T The type of the value stored in the namespace
|
|
19
19
|
* @param namespace The namespace to load properties from
|
|
20
|
+
* @returns The current value registered under `namespace`, or `undefined` when the namespace has not
|
|
21
|
+
* yet been registered, was unregistered (e.g. the owning component unmounted), or is a blank string.
|
|
22
|
+
* Consumers should handle the `undefined` case explicitly rather than defaulting to `{}`, since an
|
|
23
|
+
* empty-object default can mask a genuinely missing namespace and hide bugs.
|
|
20
24
|
*/
|
|
21
25
|
export declare function useAppContext<T extends NonNullable<object> = NonNullable<object>>(namespace: string): Readonly<T> | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* This hook is used to access a namespace within the overall AppContext, so that a component can
|
|
28
|
+
* use any shared contextual values. A selector may be provided to further restrict the properties
|
|
29
|
+
* returned from the namespace.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* // load a full namespace
|
|
34
|
+
* const patientContext = useAppContext<PatientContext>('patient');
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* // loads part of a namespace
|
|
40
|
+
* const patientName = useAppContext<PatientContext, string | undefined>('patient', (state) => state.display);
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @typeParam T The type of the value stored in the namespace
|
|
44
|
+
* @typeParam U The return type of this hook which is mostly relevant when using a selector
|
|
45
|
+
* @param namespace The namespace to load properties from
|
|
46
|
+
* @param selector An optional function which extracts the relevant part of the state
|
|
47
|
+
* @returns The selected value, or `undefined` when the namespace has not yet been registered, was
|
|
48
|
+
* unregistered (e.g. the owning component unmounted), or is a blank string. Consumers should handle
|
|
49
|
+
* the `undefined` case explicitly rather than defaulting to `{}`, since an empty-object default can
|
|
50
|
+
* mask a genuinely missing namespace and hide bugs.
|
|
51
|
+
*/
|
|
52
|
+
export declare function useAppContext<T extends NonNullable<object> = NonNullable<object>, U = T>(namespace: string, selector: (state: Readonly<T> | null) => Readonly<U>): Readonly<U> | undefined;
|
|
22
53
|
//# sourceMappingURL=useAppContext.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAppContext.d.ts","sourceRoot":"","sources":["../src/useAppContext.ts"],"names":[],"mappings":"AAKA
|
|
1
|
+
{"version":3,"file":"useAppContext.d.ts","sourceRoot":"","sources":["../src/useAppContext.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,CAAC,MAAM,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC,EAC/E,SAAS,EAAE,MAAM,GAChB,QAAQ,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,CAAC,MAAM,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,EACtF,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GACnD,QAAQ,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC"}
|
package/dist/useAppContext.js
CHANGED
|
@@ -1,31 +1,19 @@
|
|
|
1
1
|
/** @module @category Context */ import { useEffect, useState } from "react";
|
|
2
|
-
import { subscribeToContext } from "@openmrs/esm-context";
|
|
2
|
+
import { getContext, subscribeToContext } from "@openmrs/esm-context";
|
|
3
3
|
import { shallowEqual } from "@openmrs/esm-utils";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* @example
|
|
16
|
-
* ```ts
|
|
17
|
-
* // loads part of a namespace
|
|
18
|
-
* const patientName = useAppContext<PatientContext, string | undefined>('patient', (state) => state.display);
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* @typeParam T The type of the value stored in the namespace
|
|
22
|
-
* @typeParam U The return type of this hook which is mostly relevant when using a selector
|
|
23
|
-
* @param namespace The namespace to load properties from
|
|
24
|
-
* @param selector An optional function which extracts the relevant part of the state
|
|
25
|
-
*/ export function useAppContext(namespace, selector = (state)=>state ?? {}) {
|
|
26
|
-
const [value, setValue] = useState();
|
|
4
|
+
export function useAppContext(namespace, selector = (state)=>state ?? {}) {
|
|
5
|
+
const [value, setValue] = useState(()=>{
|
|
6
|
+
if (isBlankNamespace(namespace)) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const current = getContext(namespace);
|
|
10
|
+
if (current === null) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return selector ? selector(current) : current;
|
|
14
|
+
});
|
|
27
15
|
useEffect(()=>{
|
|
28
|
-
if (namespace
|
|
16
|
+
if (isBlankNamespace(namespace)) {
|
|
29
17
|
throw new Error(`The namespace supplied to useAppContext must be a non-empty string, but was "${namespace}".`);
|
|
30
18
|
}
|
|
31
19
|
}, [
|
|
@@ -33,13 +21,19 @@ import { shallowEqual } from "@openmrs/esm-utils";
|
|
|
33
21
|
]);
|
|
34
22
|
useEffect(()=>{
|
|
35
23
|
return subscribeToContext(namespace, (state)=>{
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
setValue(newValue);
|
|
40
|
-
}
|
|
24
|
+
if (state == null) {
|
|
25
|
+
setValue(undefined);
|
|
26
|
+
return;
|
|
41
27
|
}
|
|
28
|
+
const newValue = selector ? selector(state) : state;
|
|
29
|
+
setValue((prev)=>shallowEqual(prev, newValue) ? prev : newValue);
|
|
42
30
|
});
|
|
43
|
-
}, [
|
|
31
|
+
}, [
|
|
32
|
+
namespace,
|
|
33
|
+
selector
|
|
34
|
+
]);
|
|
44
35
|
return value;
|
|
45
36
|
}
|
|
37
|
+
function isBlankNamespace(namespace) {
|
|
38
|
+
return typeof namespace !== 'string' || namespace.trim().length === 0;
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-react-utils",
|
|
3
|
-
"version": "9.0.3-pre.
|
|
3
|
+
"version": "9.0.3-pre.4622",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "React utilities for OpenMRS.",
|
|
6
6
|
"type": "module",
|
|
@@ -79,17 +79,17 @@
|
|
|
79
79
|
"swr": "2.x"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@openmrs/esm-api": "9.0.3-pre.
|
|
83
|
-
"@openmrs/esm-config": "9.0.3-pre.
|
|
84
|
-
"@openmrs/esm-context": "9.0.3-pre.
|
|
85
|
-
"@openmrs/esm-emr-api": "9.0.3-pre.
|
|
86
|
-
"@openmrs/esm-error-handling": "9.0.3-pre.
|
|
87
|
-
"@openmrs/esm-extensions": "9.0.3-pre.
|
|
88
|
-
"@openmrs/esm-feature-flags": "9.0.3-pre.
|
|
89
|
-
"@openmrs/esm-globals": "9.0.3-pre.
|
|
90
|
-
"@openmrs/esm-navigation": "9.0.3-pre.
|
|
91
|
-
"@openmrs/esm-state": "9.0.3-pre.
|
|
92
|
-
"@openmrs/esm-utils": "9.0.3-pre.
|
|
82
|
+
"@openmrs/esm-api": "9.0.3-pre.4622",
|
|
83
|
+
"@openmrs/esm-config": "9.0.3-pre.4622",
|
|
84
|
+
"@openmrs/esm-context": "9.0.3-pre.4622",
|
|
85
|
+
"@openmrs/esm-emr-api": "9.0.3-pre.4622",
|
|
86
|
+
"@openmrs/esm-error-handling": "9.0.3-pre.4622",
|
|
87
|
+
"@openmrs/esm-extensions": "9.0.3-pre.4622",
|
|
88
|
+
"@openmrs/esm-feature-flags": "9.0.3-pre.4622",
|
|
89
|
+
"@openmrs/esm-globals": "9.0.3-pre.4622",
|
|
90
|
+
"@openmrs/esm-navigation": "9.0.3-pre.4622",
|
|
91
|
+
"@openmrs/esm-state": "9.0.3-pre.4622",
|
|
92
|
+
"@openmrs/esm-utils": "9.0.3-pre.4622",
|
|
93
93
|
"@swc/cli": "0.8.1",
|
|
94
94
|
"@swc/core": "1.15.21",
|
|
95
95
|
"@vitest/coverage-v8": "^4.1.2",
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { act, render, renderHook, screen } from '@testing-library/react';
|
|
4
|
+
import { registerContext, unregisterContext, updateContext } from '@openmrs/esm-context';
|
|
5
|
+
import { useAppContext } from './useAppContext';
|
|
6
|
+
import { useDefineAppContext } from './useDefineAppContext';
|
|
7
|
+
|
|
8
|
+
const namespace = 'test-context';
|
|
9
|
+
|
|
10
|
+
interface TestContext {
|
|
11
|
+
value: string;
|
|
12
|
+
count: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
unregisterContext(namespace);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('useAppContext', () => {
|
|
20
|
+
it('returns undefined when the namespace is not registered', () => {
|
|
21
|
+
const { result } = renderHook(() => useAppContext<TestContext>(namespace));
|
|
22
|
+
|
|
23
|
+
expect(result.current).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns the currently-registered value on the very first render', () => {
|
|
27
|
+
registerContext<TestContext>(namespace, { value: 'initial', count: 1 });
|
|
28
|
+
|
|
29
|
+
const { result } = renderHook(() => useAppContext<TestContext>(namespace));
|
|
30
|
+
|
|
31
|
+
expect(result.current).toEqual({ value: 'initial', count: 1 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('applies the selector on the first render', () => {
|
|
35
|
+
registerContext<TestContext>(namespace, { value: 'initial', count: 5 });
|
|
36
|
+
|
|
37
|
+
const { result } = renderHook(() => useAppContext<TestContext, number>(namespace, (state) => state?.count ?? 0));
|
|
38
|
+
|
|
39
|
+
expect(result.current).toBe(5);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('exposes a sibling useDefineAppContext value to the consumer after effects flush', () => {
|
|
43
|
+
const Producer = () => {
|
|
44
|
+
useDefineAppContext<TestContext>(namespace, { value: 'from-producer', count: 42 });
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const Consumer = () => {
|
|
49
|
+
const ctx = useAppContext<TestContext>(namespace);
|
|
50
|
+
return <div data-testid="consumer-value">{ctx ? `${ctx.value}:${ctx.count}` : 'UNDEFINED'}</div>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
render(
|
|
54
|
+
<>
|
|
55
|
+
<Producer />
|
|
56
|
+
<Consumer />
|
|
57
|
+
</>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(screen.getByTestId('consumer-value')).toHaveTextContent('from-producer:42');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns undefined (not {}) after the namespace owner unmounts (O3-4020 scenario)', () => {
|
|
64
|
+
// When the component that owns a namespace via useDefineAppContext
|
|
65
|
+
// unmounts while a consumer is still mounted, the consumer must observe
|
|
66
|
+
// undefined — not a frozen empty object that silently hides the absence.
|
|
67
|
+
const Producer = () => {
|
|
68
|
+
useDefineAppContext<TestContext>(namespace, { value: 'v', count: 1 });
|
|
69
|
+
return null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const Consumer = () => {
|
|
73
|
+
const ctx = useAppContext<TestContext>(namespace);
|
|
74
|
+
return <div data-testid="consumer-value">{ctx ? `${ctx.value}:${ctx.count}` : 'UNDEFINED'}</div>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const { rerender } = render(
|
|
78
|
+
<>
|
|
79
|
+
<Producer />
|
|
80
|
+
<Consumer />
|
|
81
|
+
</>,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(screen.getByTestId('consumer-value')).toHaveTextContent('v:1');
|
|
85
|
+
|
|
86
|
+
rerender(<Consumer />);
|
|
87
|
+
|
|
88
|
+
expect(screen.getByTestId('consumer-value')).toHaveTextContent('UNDEFINED');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('re-applies the selector when the underlying context updates', () => {
|
|
92
|
+
registerContext<TestContext>(namespace, { value: 'a', count: 1 });
|
|
93
|
+
|
|
94
|
+
const { result } = renderHook(() => useAppContext<TestContext, number>(namespace, (state) => state?.count ?? -1));
|
|
95
|
+
|
|
96
|
+
expect(result.current).toBe(1);
|
|
97
|
+
|
|
98
|
+
act(() => {
|
|
99
|
+
updateContext<TestContext>(namespace, (state) => ({ ...state, count: 7 }));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.current).toBe(7);
|
|
103
|
+
});
|
|
104
|
+
});
|
package/src/useAppContext.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @module @category Context */
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
-
import { subscribeToContext } from '@openmrs/esm-context';
|
|
3
|
+
import { getContext, subscribeToContext } from '@openmrs/esm-context';
|
|
4
4
|
import { shallowEqual } from '@openmrs/esm-utils';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -22,6 +22,10 @@ import { shallowEqual } from '@openmrs/esm-utils';
|
|
|
22
22
|
*
|
|
23
23
|
* @typeParam T The type of the value stored in the namespace
|
|
24
24
|
* @param namespace The namespace to load properties from
|
|
25
|
+
* @returns The current value registered under `namespace`, or `undefined` when the namespace has not
|
|
26
|
+
* yet been registered, was unregistered (e.g. the owning component unmounted), or is a blank string.
|
|
27
|
+
* Consumers should handle the `undefined` case explicitly rather than defaulting to `{}`, since an
|
|
28
|
+
* empty-object default can mask a genuinely missing namespace and hide bugs.
|
|
25
29
|
*/
|
|
26
30
|
export function useAppContext<T extends NonNullable<object> = NonNullable<object>>(
|
|
27
31
|
namespace: string,
|
|
@@ -48,29 +52,51 @@ export function useAppContext<T extends NonNullable<object> = NonNullable<object
|
|
|
48
52
|
* @typeParam U The return type of this hook which is mostly relevant when using a selector
|
|
49
53
|
* @param namespace The namespace to load properties from
|
|
50
54
|
* @param selector An optional function which extracts the relevant part of the state
|
|
55
|
+
* @returns The selected value, or `undefined` when the namespace has not yet been registered, was
|
|
56
|
+
* unregistered (e.g. the owning component unmounted), or is a blank string. Consumers should handle
|
|
57
|
+
* the `undefined` case explicitly rather than defaulting to `{}`, since an empty-object default can
|
|
58
|
+
* mask a genuinely missing namespace and hide bugs.
|
|
51
59
|
*/
|
|
60
|
+
export function useAppContext<T extends NonNullable<object> = NonNullable<object>, U = T>(
|
|
61
|
+
namespace: string,
|
|
62
|
+
selector: (state: Readonly<T> | null) => Readonly<U>,
|
|
63
|
+
): Readonly<U> | undefined;
|
|
64
|
+
|
|
52
65
|
export function useAppContext<T extends NonNullable<object> = NonNullable<object>, U = T>(
|
|
53
66
|
namespace: string,
|
|
54
67
|
selector: (state: Readonly<T> | null) => Readonly<U> = (state) => (state ?? {}) as Readonly<U>,
|
|
55
68
|
): Readonly<U> | undefined {
|
|
56
|
-
const [value, setValue] = useState<Readonly<U
|
|
69
|
+
const [value, setValue] = useState<Readonly<U> | undefined>(() => {
|
|
70
|
+
if (isBlankNamespace(namespace)) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const current = getContext<T>(namespace);
|
|
74
|
+
if (current === null) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return selector ? selector(current) : (current as unknown as Readonly<U>);
|
|
78
|
+
});
|
|
57
79
|
|
|
58
80
|
useEffect(() => {
|
|
59
|
-
if (namespace
|
|
81
|
+
if (isBlankNamespace(namespace)) {
|
|
60
82
|
throw new Error(`The namespace supplied to useAppContext must be a non-empty string, but was "${namespace}".`);
|
|
61
83
|
}
|
|
62
84
|
}, [namespace]);
|
|
63
85
|
|
|
64
86
|
useEffect(() => {
|
|
65
87
|
return subscribeToContext<T>(namespace, (state) => {
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
setValue(newValue);
|
|
70
|
-
}
|
|
88
|
+
if (state == null) {
|
|
89
|
+
setValue(undefined);
|
|
90
|
+
return;
|
|
71
91
|
}
|
|
92
|
+
const newValue = selector ? selector(state) : (state as unknown as Readonly<U>);
|
|
93
|
+
setValue((prev) => (shallowEqual(prev, newValue) ? prev : newValue));
|
|
72
94
|
});
|
|
73
|
-
}, []);
|
|
95
|
+
}, [namespace, selector]);
|
|
74
96
|
|
|
75
97
|
return value;
|
|
76
98
|
}
|
|
99
|
+
|
|
100
|
+
function isBlankNamespace(namespace: unknown): boolean {
|
|
101
|
+
return typeof namespace !== 'string' || namespace.trim().length === 0;
|
|
102
|
+
}
|