@openmrs/esm-react-utils 3.3.2-pre.1184 → 3.3.2-pre.1193

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": "3.3.2-pre.1184",
3
+ "version": "3.3.2-pre.1193",
4
4
  "license": "MPL-2.0",
5
5
  "description": "React utilities for OpenMRS.",
6
6
  "browser": "dist/openmrs-esm-react-utils.js",
@@ -55,11 +55,11 @@
55
55
  "react-i18next": "11.x"
56
56
  },
57
57
  "devDependencies": {
58
- "@openmrs/esm-api": "^3.3.2-pre.1184",
59
- "@openmrs/esm-config": "^3.3.2-pre.1184",
60
- "@openmrs/esm-error-handling": "^3.3.2-pre.1184",
61
- "@openmrs/esm-extensions": "^3.3.2-pre.1184",
62
- "@openmrs/esm-globals": "^3.3.2-pre.1184",
58
+ "@openmrs/esm-api": "^3.3.2-pre.1193",
59
+ "@openmrs/esm-config": "^3.3.2-pre.1193",
60
+ "@openmrs/esm-error-handling": "^3.3.2-pre.1193",
61
+ "@openmrs/esm-extensions": "^3.3.2-pre.1193",
62
+ "@openmrs/esm-globals": "^3.3.2-pre.1193",
63
63
  "dayjs": "^1.10.8",
64
64
  "i18next": "^19.6.0",
65
65
  "react": "^16.13.1",
@@ -68,5 +68,5 @@
68
68
  "rxjs": "^6.5.3",
69
69
  "unistore": "^3.5.2"
70
70
  },
71
- "gitHead": "e111f79dfae61386b2f738291787310f8fc93f9d"
71
+ "gitHead": "8a30755ac68941f2b6d70373ef98126410608d24"
72
72
  }
package/src/index.ts CHANGED
@@ -22,7 +22,7 @@ export * from "./useLayoutType";
22
22
  export * from "./useLocations";
23
23
  export * from "./useOnClickOutside";
24
24
  export * from "./UserHasAccess";
25
- export * from "./useSessionUser";
25
+ export { useSession } from "./useSession";
26
26
  export * from "./useStore";
27
27
  export * from "./useVisit";
28
28
  export * from "./useVisitTypes";
package/src/public.ts CHANGED
@@ -19,7 +19,7 @@ export * from "./useLayoutType";
19
19
  export * from "./useLocations";
20
20
  export * from "./useOnClickOutside";
21
21
  export * from "./UserHasAccess";
22
- export * from "./useSessionUser";
22
+ export { useSession } from "./useSession";
23
23
  export * from "./useStore";
24
24
  export * from "./useVisit";
25
25
  export * from "./useVisitTypes";
@@ -0,0 +1,84 @@
1
+ import React, { Suspense } from "react";
2
+ import { act, render, screen } from "@testing-library/react";
3
+ import "@testing-library/jest-dom";
4
+ import { useSession, __cleanup } from "./useSession.tsx";
5
+ import { createGlobalStore } from "@openmrs/esm-state";
6
+ import { SessionStore } from "@openmrs/esm-api";
7
+
8
+ const mockSessionStore = createGlobalStore<SessionStore>("mockSessionStore", {
9
+ loaded: false,
10
+ session: null,
11
+ });
12
+
13
+ jest.mock("@openmrs/esm-api", () => ({
14
+ getSessionStore: jest.fn(() => mockSessionStore),
15
+ }));
16
+
17
+ function Component() {
18
+ const session = useSession();
19
+ return <div>{JSON.stringify(session)}</div>;
20
+ }
21
+
22
+ describe("useSession", () => {
23
+ beforeEach(() => {
24
+ __cleanup();
25
+ mockSessionStore.setState({ loaded: false, session: null });
26
+ });
27
+
28
+ it("should suspend and then resolve to the session", async () => {
29
+ render(
30
+ <Suspense fallback={"suspended"}>
31
+ <Component />
32
+ </Suspense>
33
+ );
34
+
35
+ expect(screen.getByText("suspended")).toBeInTheDocument();
36
+ act(() => {
37
+ mockSessionStore.setState({
38
+ loaded: true,
39
+ session: { authenticated: false, sessionId: "test1" },
40
+ });
41
+ });
42
+ await screen.findByText(/"authenticated":false/);
43
+ });
44
+
45
+ it("should resolve immediately when the session is present", async () => {
46
+ mockSessionStore.setState({
47
+ loaded: true,
48
+ session: { authenticated: false, sessionId: "test2" },
49
+ });
50
+ render(
51
+ <Suspense fallback={"suspended"}>
52
+ <Component />
53
+ </Suspense>
54
+ );
55
+ expect(screen.getByText(/"authenticated":false/)).toBeInTheDocument();
56
+ });
57
+
58
+ it("should not return stale data when re-created", async () => {
59
+ const { unmount } = render(
60
+ <Suspense fallback={"suspended"}>
61
+ <Component />
62
+ </Suspense>
63
+ );
64
+ expect(screen.getByText("suspended")).toBeInTheDocument();
65
+ act(() => {
66
+ mockSessionStore.setState({
67
+ loaded: true,
68
+ session: { authenticated: true, sessionId: "test3" },
69
+ });
70
+ });
71
+ await screen.findByText(/"authenticated":true/);
72
+ unmount();
73
+ mockSessionStore.setState({
74
+ loaded: true,
75
+ session: { authenticated: false, sessionId: "test3" },
76
+ });
77
+ render(
78
+ <Suspense fallback={"suspended"}>
79
+ <Component />
80
+ </Suspense>
81
+ );
82
+ expect(screen.getByText(/"authenticated":false/)).toBeInTheDocument();
83
+ });
84
+ });
@@ -0,0 +1,117 @@
1
+ /** @module @category API */
2
+ import { getSessionStore, Session } from "@openmrs/esm-api";
3
+ import { useState, useEffect } from "react";
4
+
5
+ let promise: undefined | Promise<Session>;
6
+ let unsubscribe: undefined | (() => void);
7
+
8
+ /**
9
+ * Gets the current user session information. Returns an object with
10
+ * property `authenticated` == `false` if the user is not logged in.
11
+ *
12
+ * Uses Suspense. This hook will always either return a Session object
13
+ * or throw for Suspense. It will never return `null`/`undefined`.
14
+ *
15
+ * @returns Current user session information
16
+ */
17
+ export function useSession(): Session {
18
+ // We have two separate variables for the session.
19
+ //
20
+ // `session` is a temporary variable, which starts as `null` every time this
21
+ // hook is executed. It is important that we can set and return this
22
+ // variable synchronously, because every time we `throw` for Suspense, this
23
+ // hook will unmount and a new instance will be created, destroying whatever
24
+ // state existed. Thus, if this hook were to try to always set and return
25
+ // `stateSession`, it would cause an infinite loop:
26
+ // 1. instance A mounts
27
+ // 2. instance A receives value, calls `setStateSession`
28
+ // 3. instance A throws
29
+ // 4. instance A unmounts
30
+ // 5. instance B mounts
31
+ // ...
32
+ // What would happen if we moved `session` to the module scope, so that it
33
+ // could be re-used across instances of this hook? Then we would have no way
34
+ // to tell whether the session was fresh.
35
+ //
36
+ // `stateSession` is React state, which is needed to update components using
37
+ // this hook when the session changes.
38
+ const [stateSession, setStateSession] = useState<Session | null>(null);
39
+ let session: Session | null = null;
40
+
41
+ if (!stateSession) {
42
+ if (!promise) {
43
+ // If we haven't created a promise to throw yet, do that.
44
+ promise = new Promise<Session>((resolve) => {
45
+ const handleNewSession = ({ loaded, session: newSession }) => {
46
+ if (loaded) {
47
+ resolve(newSession);
48
+ session = newSession;
49
+ unsubscribe && unsubscribe();
50
+ unsubscribe = undefined;
51
+ }
52
+ };
53
+ handleNewSession(getSessionStore().getState());
54
+ if (!session) {
55
+ unsubscribe = getSessionStore().subscribe(handleNewSession);
56
+ }
57
+ });
58
+ } else {
59
+ // However, if we have created a promise to throw, but there's no `stateSession`
60
+ // yet, then it's probably just this hook's first render. Check to see if
61
+ // there's already a session that we can return.
62
+ const currentState = getSessionStore().getState();
63
+ if (currentState.loaded) {
64
+ session = currentState.session;
65
+ }
66
+ }
67
+
68
+ // If the session got set synchronously in the above block, then we can just
69
+ // return it rather than throwing. Otherwise, throw for Suspense.
70
+ if (session) {
71
+ setStateSession(session);
72
+ } else {
73
+ throw promise;
74
+ }
75
+ }
76
+
77
+ // Once this hook is established (no longer throwing and getting re-created)
78
+ // we need to set up a subscription that will update its value the good
79
+ // old-fashioned React way.
80
+ useEffect(() => {
81
+ if (!unsubscribe) {
82
+ unsubscribe = getSessionStore().subscribe(
83
+ ({ loaded, session: newSession }) => {
84
+ if (loaded) {
85
+ session = newSession;
86
+ setStateSession(newSession);
87
+ }
88
+ }
89
+ );
90
+ }
91
+ return () => {
92
+ unsubscribe && unsubscribe();
93
+ unsubscribe = undefined;
94
+ };
95
+ }, []);
96
+
97
+ const result = stateSession || session;
98
+ if (!result) {
99
+ if (promise) {
100
+ console.warn(
101
+ "useSessionUser is in an unexpected state. Attempting to recover."
102
+ );
103
+ throw promise;
104
+ } else {
105
+ throw Error("useSessionUser is in an invalid state.");
106
+ }
107
+ }
108
+ return result;
109
+ }
110
+
111
+ /**
112
+ * For testing.
113
+ */
114
+ export function __cleanup() {
115
+ promise = undefined;
116
+ unsubscribe = undefined;
117
+ }
@@ -1,17 +0,0 @@
1
- /** @module @category API */
2
- import { getCurrentUser, Session } from "@openmrs/esm-api";
3
- import { useState, useEffect } from "react";
4
-
5
- export function useSession() {
6
- const [session, setSession] = useState<Session | null>(null);
7
-
8
- useEffect(() => {
9
- const sub = getCurrentUser({ includeAuthStatus: true }).subscribe(
10
- (session) => setSession(session)
11
- );
12
-
13
- return () => sub.unsubscribe();
14
- }, [setSession]);
15
-
16
- return session;
17
- }