@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.
@@ -1,3 +1,3 @@
1
- [0] Successfully compiled: 53 files with swc (162.43ms)
1
+ [0] Successfully compiled: 53 files with swc (168.97ms)
2
2
  [0] swc --strip-leading-paths src -d dist exited with code 0
3
3
  [1] tsc --project tsconfig.build.json exited with code 0
@@ -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;;;;;;;;;;;;;;;;;;;GAmBG;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"}
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"}
@@ -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
- * This hook is used to access a namespace within the overall AppContext, so that a component can
6
- * use any shared contextual values. A selector may be provided to further restrict the properties
7
- * returned from the namespace.
8
- *
9
- * @example
10
- * ```ts
11
- * // load a full namespace
12
- * const patientContext = useAppContext<PatientContext>('patient');
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 === null || typeof namespace === 'undefined' || namespace.replace(' ', '') === '') {
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 (typeof state !== 'undefined') {
37
- const newValue = selector ? selector(state) : state ?? {};
38
- if (!shallowEqual(value, newValue)) {
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.4556",
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.4556",
83
- "@openmrs/esm-config": "9.0.3-pre.4556",
84
- "@openmrs/esm-context": "9.0.3-pre.4556",
85
- "@openmrs/esm-emr-api": "9.0.3-pre.4556",
86
- "@openmrs/esm-error-handling": "9.0.3-pre.4556",
87
- "@openmrs/esm-extensions": "9.0.3-pre.4556",
88
- "@openmrs/esm-feature-flags": "9.0.3-pre.4556",
89
- "@openmrs/esm-globals": "9.0.3-pre.4556",
90
- "@openmrs/esm-navigation": "9.0.3-pre.4556",
91
- "@openmrs/esm-state": "9.0.3-pre.4556",
92
- "@openmrs/esm-utils": "9.0.3-pre.4556",
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
+ });
@@ -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 === null || typeof namespace === 'undefined' || namespace.replace(' ', '') === '') {
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 (typeof state !== 'undefined') {
67
- const newValue = selector ? selector(state) : ((state ?? {}) as Readonly<U>);
68
- if (!shallowEqual(value, newValue)) {
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
+ }