@openmrs/esm-react-utils 5.2.1-pre.1094 → 5.2.1-pre.1101

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-react-utils",
3
- "version": "5.2.1-pre.1094",
3
+ "version": "5.2.1-pre.1101",
4
4
  "license": "MPL-2.0",
5
5
  "description": "React utilities for OpenMRS.",
6
6
  "browser": "dist/openmrs-esm-react-utils.js",
@@ -56,12 +56,12 @@
56
56
  "swr": "2.x"
57
57
  },
58
58
  "devDependencies": {
59
- "@openmrs/esm-api": "^5.2.1-pre.1094",
60
- "@openmrs/esm-config": "^5.2.1-pre.1094",
61
- "@openmrs/esm-error-handling": "^5.2.1-pre.1094",
62
- "@openmrs/esm-extensions": "^5.2.1-pre.1094",
63
- "@openmrs/esm-feature-flags": "^5.2.1-pre.1094",
64
- "@openmrs/esm-globals": "^5.2.1-pre.1094",
59
+ "@openmrs/esm-api": "^5.2.1-pre.1101",
60
+ "@openmrs/esm-config": "^5.2.1-pre.1101",
61
+ "@openmrs/esm-error-handling": "^5.2.1-pre.1101",
62
+ "@openmrs/esm-extensions": "^5.2.1-pre.1101",
63
+ "@openmrs/esm-feature-flags": "^5.2.1-pre.1101",
64
+ "@openmrs/esm-globals": "^5.2.1-pre.1101",
65
65
  "dayjs": "^1.10.8",
66
66
  "i18next": "^21.10.0",
67
67
  "react": "^18.1.0",
@@ -71,5 +71,5 @@
71
71
  "swr": "^2.2.2",
72
72
  "webpack": "^5.88.0"
73
73
  },
74
- "gitHead": "f6f860231a56910c3b5259b0954bbfa043f76602"
74
+ "gitHead": "db76586824c390393c7cacffc288d22160e5a54b"
75
75
  }
package/src/index.ts CHANGED
@@ -2,15 +2,17 @@ export * from "./ComponentContext";
2
2
  export * from "./ConfigurableLink";
3
3
  export * from "./Extension";
4
4
  export * from "./ExtensionSlot";
5
+ export * from "./UserHasAccess";
5
6
  export * from "./getLifecycle";
6
7
  export * from "./openmrsComponentDecorator";
8
+ export * from "./useAbortController";
7
9
  export * from "./useAssignedExtensions";
8
10
  export * from "./useAssignedExtensionIds";
9
11
  export * from "./useBodyScrollLock";
10
12
  export * from "./useConfig";
11
13
  export * from "./useConnectedExtensions";
12
14
  export * from "./useConnectivity";
13
- export * from "./usePatient";
15
+ export * from "./useDebounce";
14
16
  export * from "./useExtensionInternalStore";
15
17
  export * from "./useExtensionSlot";
16
18
  export * from "./useExtensionSlotMeta";
@@ -20,10 +22,10 @@ export * from "./useForceUpdate";
20
22
  export * from "./useLayoutType";
21
23
  export * from "./useLocations";
22
24
  export * from "./useOnClickOutside";
23
- export * from "./UserHasAccess";
25
+ export * from "./useOpenmrsSWR";
26
+ export * from "./usePatient";
24
27
  export { useSession } from "./useSession";
25
28
  export * from "./useStore";
26
29
  export * from "./useVisit";
27
30
  export * from "./useVisitTypes";
28
31
  export * from "./usePagination";
29
- export * from "./useDebounce";
package/src/public.ts CHANGED
@@ -2,24 +2,26 @@ export { type ExtensionData } from "./ComponentContext";
2
2
  export * from "./ConfigurableLink";
3
3
  export * from "./Extension";
4
4
  export * from "./ExtensionSlot";
5
+ export * from "./UserHasAccess";
5
6
  export * from "./getLifecycle";
7
+ export * from "./useAbortController";
6
8
  export * from "./useAssignedExtensions";
7
9
  export * from "./useAssignedExtensionIds";
8
10
  export * from "./useBodyScrollLock";
9
11
  export * from "./useConfig";
10
12
  export * from "./useConnectedExtensions";
11
13
  export * from "./useConnectivity";
12
- export * from "./usePatient";
14
+ export * from "./useDebounce";
13
15
  export * from "./useExtensionSlotMeta";
14
16
  export * from "./useExtensionStore";
15
17
  export * from "./useFeatureFlag";
16
18
  export * from "./useLayoutType";
17
19
  export * from "./useLocations";
18
20
  export * from "./useOnClickOutside";
19
- export * from "./UserHasAccess";
21
+ export * from "./useOpenmrsSWR";
22
+ export * from "./usePatient";
20
23
  export { useSession } from "./useSession";
21
24
  export * from "./useStore";
22
25
  export * from "./useVisit";
23
26
  export * from "./useVisitTypes";
24
27
  export * from "./usePagination";
25
- export * from "./useDebounce";
@@ -0,0 +1,42 @@
1
+ import { renderHook, cleanup } from "@testing-library/react";
2
+ import "@testing-library/jest-dom/extend-expect";
3
+ import useAbortController from "./useAbortController";
4
+
5
+ describe("useAbortController", () => {
6
+ afterEach(cleanup);
7
+
8
+ it("returns an AbortController", () => {
9
+ const { result } = renderHook(() => useAbortController());
10
+ expect(result.current).not.toBeNull();
11
+ });
12
+
13
+ it("returns a consistent AbortController across re-renders", () => {
14
+ const { result, rerender } = renderHook(() => useAbortController());
15
+ const firstAc = result.current;
16
+
17
+ rerender();
18
+
19
+ expect(result.current).toBe(firstAc);
20
+ });
21
+
22
+ it("returns a new AbortController after the previous controller has been aborted", () => {
23
+ const { result, rerender } = renderHook(() => useAbortController());
24
+ const firstAc = result.current;
25
+
26
+ firstAc.abort();
27
+
28
+ rerender();
29
+
30
+ expect(result.current).not.toBe(firstAc);
31
+ });
32
+
33
+ it("aborts the AbortController when the component is unmounted", () => {
34
+ const { result, unmount } = renderHook(() => useAbortController());
35
+
36
+ expect(result.current.signal.aborted).toBe(false);
37
+
38
+ unmount();
39
+
40
+ expect(result.current.signal.aborted).toBe(true);
41
+ });
42
+ });
@@ -0,0 +1,40 @@
1
+ /** @module @category Utility */
2
+ import { useEffect, useRef } from "react";
3
+
4
+ /**
5
+ * @beta
6
+ *
7
+ * This hook creates an AbortController that lasts either until the previous AbortController
8
+ * is aborted or until the component unmounts. This can be used to ensure that all fetch requests
9
+ * are cancelled when a component is unmounted.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * import { useAbortController } from "@openmrs/esm-framework";
14
+ *
15
+ * function MyComponent() {
16
+ * const abortController = useAbortController();
17
+ * const { data } = useSWR(key, (key) => openmrsFetch(key, { signal: abortController.signal }));
18
+ *
19
+ * return (
20
+ * // render something with data
21
+ * );
22
+ * }
23
+ * ```
24
+ */
25
+ export function useAbortController() {
26
+ const abortController = useRef<AbortController>();
27
+
28
+ if (!abortController.current || abortController.current.signal.aborted) {
29
+ abortController.current = new AbortController();
30
+ }
31
+
32
+ useEffect(() => {
33
+ const ac = abortController.current;
34
+ return () => ac?.abort();
35
+ }, []);
36
+
37
+ return abortController.current;
38
+ }
39
+
40
+ export default useAbortController;
@@ -1,4 +1,4 @@
1
- /** @module @category API */
1
+ /** @module @category Utility */
2
2
  import { useEffect, useState } from "react";
3
3
 
4
4
  /**
@@ -8,7 +8,7 @@ import { useEffect, useState } from "react";
8
8
  *
9
9
  * @example
10
10
  * ```tsx
11
- * import { useDebounce } from "@openmrs/esm-react-utils";
11
+ * import { useDebounce } from "@openmrs/esm-framework";
12
12
  *
13
13
  * function MyComponent() {
14
14
  * const [searchTerm, setSearchTerm] = useState('');
@@ -35,6 +35,7 @@ export function useDebounce<T>(value: T, delay: number = 300) {
35
35
  const timer = setTimeout(() => {
36
36
  setDebounceValue(value);
37
37
  }, delay);
38
+
38
39
  return () => {
39
40
  clearTimeout(timer);
40
41
  };
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { PropsWithChildren } from "react";
2
2
  import { render, fireEvent } from "@testing-library/react";
3
3
  import { useOnClickOutside } from "./useOnClickOutside";
4
4
 
@@ -8,7 +8,7 @@ describe("useOnClickOutside", () => {
8
8
 
9
9
  it("should call the handler when clicking outside", () => {
10
10
  // setup
11
- const Component: React.FC = ({ children }) => {
11
+ const Component: React.FC<PropsWithChildren> = ({ children }) => {
12
12
  const ref = useOnClickOutside<HTMLDivElement>(handler);
13
13
  return <div ref={ref}>{children}</div>;
14
14
  };
@@ -23,11 +23,11 @@ describe("useOnClickOutside", () => {
23
23
 
24
24
  it("should not call the handler when clicking on the element", () => {
25
25
  // setup
26
- const Component: React.FC = ({ children }) => {
26
+ const Component: React.FC<PropsWithChildren> = ({ children }) => {
27
27
  const ref = useOnClickOutside<HTMLDivElement>(handler);
28
28
  return <div ref={ref}>{children}</div>;
29
29
  };
30
- const mutableRef: { current: HTMLDivElement } = { current: undefined };
30
+ const mutableRef: { current: HTMLDivElement | null } = { current: null };
31
31
  render(
32
32
  <Component>
33
33
  <div ref={mutableRef}></div>
@@ -35,7 +35,9 @@ describe("useOnClickOutside", () => {
35
35
  );
36
36
 
37
37
  // act
38
- fireEvent.click(mutableRef.current);
38
+ if (mutableRef.current) {
39
+ fireEvent.click(mutableRef.current);
40
+ }
39
41
 
40
42
  // verify
41
43
  expect(handler).not.toHaveBeenCalled();
@@ -43,7 +45,7 @@ describe("useOnClickOutside", () => {
43
45
 
44
46
  it("should unregister the event listener when unmounted", () => {
45
47
  // setup
46
- const Component: React.FC = ({ children }) => {
48
+ const Component: React.FC<PropsWithChildren> = ({ children }) => {
47
49
  const ref = useOnClickOutside<HTMLDivElement>(handler);
48
50
  return <div ref={ref}>{children}</div>;
49
51
  };
@@ -0,0 +1,86 @@
1
+ /** @module @category Utility */
2
+ import { useCallback, useMemo } from "react";
3
+ import useSWR, { SWRConfiguration } from "swr";
4
+ import { type FetchConfig, openmrsFetch } from "@openmrs/esm-api";
5
+ import useAbortController from "./useAbortController";
6
+
7
+ export type ArgumentsTuple = [any, ...unknown[]];
8
+ export type Key = string | ArgumentsTuple | undefined | null;
9
+ export type UseOpenmrsSWROptions = {
10
+ abortController?: AbortController;
11
+ fetchInit?: FetchConfig;
12
+ url?: string | ((key: Key) => string);
13
+ swrConfig?: SWRConfiguration;
14
+ };
15
+
16
+ function getUrl(key: Key, url?: string | ((key: Key) => string)): string {
17
+ if (url) {
18
+ return typeof url === "function" ? url(key) : url;
19
+ }
20
+
21
+ if (typeof key === "string") {
22
+ return key;
23
+ }
24
+
25
+ throw new Error(
26
+ `When using useOpenmrsSWR with a key that is not a string, you must provide a url() function that converts the key to a valid url. The key for this hook is ${key}.`
27
+ );
28
+ }
29
+
30
+ /**
31
+ * @beta
32
+ *
33
+ * This hook is intended to simplify using openmrsFetch in useSWR, while also ensuring that
34
+ * all useSWR usages properly use an abort controller, so that fetch requests are cancelled
35
+ * if the React component unmounts.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * import { useOpenmrsSWR } from "@openmrs/esm-framework";
40
+ *
41
+ * function MyComponent() {
42
+ * const { data } = useOpenmrsSWR(key);
43
+ *
44
+ * return (
45
+ * // render something with data
46
+ * );
47
+ * }
48
+ * ```
49
+ *
50
+ * Note that if you are using a complex SWR key you must provide a url function to the options parameter,
51
+ * which translates the key into a URL to be sent to `openmrsFetch()`
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * import { useOpenmrsSWR } from "@openmrs/esm-framework";
56
+ *
57
+ * function MyComponent() {
58
+ * const { data } = useOpenmrsSWR(['key', 'url'], { url: (key) => key[1] });
59
+ *
60
+ * return (
61
+ * // render something with data
62
+ * );
63
+ * }
64
+ * ```
65
+ * @param key The SWR key to use
66
+ * @param options An object of optional parameters to provide, including a {@link FetchConfig} object
67
+ * to pass to {@link openmrsFetch} or options to pass to SWR
68
+ */
69
+ export function useOpenmrsSWR(key: Key, options: UseOpenmrsSWROptions = {}) {
70
+ const { abortController, fetchInit, url, swrConfig } = options;
71
+ const ac = useAbortController();
72
+ const abortSignal = useMemo<AbortSignal>(
73
+ () => fetchInit?.signal ?? abortController?.signal ?? ac.signal,
74
+ [abortController?.signal, fetchInit?.signal, ac.signal]
75
+ );
76
+
77
+ const fetcher = useCallback(
78
+ (key: Key) => {
79
+ const url_ = getUrl(key, url);
80
+ return openmrsFetch(url_, { ...fetchInit, signal: abortSignal });
81
+ },
82
+ [abortSignal, fetchInit, url]
83
+ );
84
+
85
+ return useSWR(key, fetcher, swrConfig);
86
+ }
@@ -1,7 +1,7 @@
1
1
  import React, { Suspense } from "react";
2
2
  import { act, render, screen } from "@testing-library/react";
3
3
  import "@testing-library/jest-dom";
4
- import { useSession, __cleanup } from "./useSession.tsx";
4
+ import { useSession, __cleanup } from "./useSession";
5
5
  import { createGlobalStore } from "@openmrs/esm-state";
6
6
  import { SessionStore } from "@openmrs/esm-api";
7
7
 
File without changes