Skip to content

Commit e2289d3

Browse files
committed
Add a session store managing component.
1 parent 3016f87 commit e2289d3

File tree

6 files changed

+360
-0
lines changed

6 files changed

+360
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as React from "react";
2+
import { useEventRefresh } from "../../hooks/useEventRefresh";
3+
import type { SessionStore } from "../../services/SessionStore";
4+
import type { Json } from "../../types/Json";
5+
6+
/**
7+
* Creates a React component which automatically manages a session store,
8+
* loading it and unloading it as appropriate and passing its content down to
9+
* its props.
10+
*
11+
* A re-render will be triggered automatically should the store's content
12+
* change.
13+
* @template T The type of session maintained by the session store.
14+
* @param sessionStore The session store. This must not yet be loaded.
15+
* @returns A React component which automatically manages the session
16+
* store, loading it and unloading it as appropriate and
17+
* passing its content down to its props.
18+
*/
19+
export const createSessionStoreManagerComponent = <T extends Json>(
20+
sessionStore: SessionStore<T>
21+
): React.FunctionComponent<{
22+
readonly loading: React.ReactElement<any, any> | null;
23+
readonly ready: (
24+
session: T,
25+
setSession: (to: T) => void
26+
) => React.ReactElement<any, any> | null;
27+
}> => {
28+
return ({ loading, ready }) => {
29+
const [loaded, setLoaded] = React.useState(false);
30+
useEventRefresh(sessionStore, `set`);
31+
32+
React.useEffect(() => {
33+
let state: `loading` | `loaded` | `aborting` = `loading`;
34+
35+
(async () => {
36+
await sessionStore.load();
37+
38+
switch (state as `loading` | `aborting`) {
39+
case `loading`:
40+
state = `loaded`;
41+
setLoaded(true);
42+
break;
43+
44+
case `aborting`:
45+
await sessionStore.unload();
46+
break;
47+
}
48+
})();
49+
50+
return () => {
51+
switch (state) {
52+
case `loading`:
53+
state = `aborting`;
54+
break;
55+
56+
case `loaded`:
57+
(async () => {
58+
await sessionStore.unload();
59+
})();
60+
}
61+
};
62+
}, []);
63+
64+
if (loaded) {
65+
return ready(sessionStore.get(), (to: T) => {
66+
sessionStore.set(to);
67+
});
68+
} else {
69+
return loading;
70+
}
71+
};
72+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# `react-native-app-helpers/createSessionStoreManagerComponent`
2+
3+
Creates a React component which automatically manages a session store, loading
4+
it and unloading it as appropriate and passing its content down to its props.
5+
6+
A re-render will be triggered automatically should the store's content change.
7+
8+
## Usage
9+
10+
```tsx
11+
import {
12+
SessionStore,
13+
createSessionStoreManagerComponent,
14+
} from "react-native-app-helpers";
15+
16+
const sessionStore = new SessionStore<number>(0, `Secure Storage Key`);
17+
18+
const SessionStoreManager = createSessionStoreManager(sessionStore);
19+
20+
const ExampleScreen = () => (
21+
<SessionStoreManager
22+
loading={<Text>The session store is loading...</Text>}
23+
ready={(session, setSession) => (
24+
<Button
25+
title={`Session: ${session}; click or touch to increment.`}
26+
onPress={() => {
27+
setSession(session + 1);
28+
}}
29+
/>
30+
)}
31+
/>
32+
);
33+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import * as uuid from "uuid";
2+
import * as React from "react";
3+
import { Button, Text } from "react-native";
4+
import * as TestRenderer from "react-test-renderer";
5+
import { createSessionStoreManagerComponent, SessionStore } from "../../";
6+
7+
type TestSession = { readonly value: number };
8+
9+
test(`displays the loading screen`, async () => {
10+
const sessionStore = new SessionStore<TestSession>({ value: 5 }, uuid.v4());
11+
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore);
12+
13+
const renderer = TestRenderer.create(
14+
<SessionStoreManager
15+
loading={<Text>Loading</Text>}
16+
ready={(session, setSession) => (
17+
<Button
18+
title={`Session contains ${session.value}`}
19+
onPress={() => {
20+
setSession({ value: session.value + 1 });
21+
}}
22+
/>
23+
)}
24+
/>
25+
);
26+
27+
expect(renderer.toTree()?.rendered).toEqual(
28+
expect.objectContaining({
29+
props: expect.objectContaining({
30+
children: `Loading`,
31+
}),
32+
})
33+
);
34+
35+
renderer.unmount();
36+
37+
await TestRenderer.act(async () => {
38+
await new Promise((resolve) => setTimeout(resolve, 250));
39+
});
40+
41+
await sessionStore.load();
42+
expect(sessionStore.get()).toEqual({ value: 5 });
43+
await sessionStore.unload();
44+
});
45+
46+
test(`shows the ready screen once given time to load`, async () => {
47+
const sessionStore = new SessionStore<TestSession>({ value: 5 }, uuid.v4());
48+
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore);
49+
50+
const renderer = TestRenderer.create(
51+
<SessionStoreManager
52+
loading={<Text>Loading</Text>}
53+
ready={(session, setSession) => (
54+
<Button
55+
title={`Session contains ${session.value}`}
56+
onPress={() => {
57+
setSession({ value: session.value + 1 });
58+
}}
59+
/>
60+
)}
61+
/>
62+
);
63+
64+
await TestRenderer.act(async () => {
65+
await new Promise((resolve) => setTimeout(resolve, 250));
66+
});
67+
68+
expect(renderer.toTree()?.rendered).toEqual(
69+
expect.objectContaining({
70+
props: expect.objectContaining({
71+
title: `Session contains 5`,
72+
}),
73+
})
74+
);
75+
76+
renderer.unmount();
77+
78+
await new Promise((resolve) => setTimeout(resolve, 250));
79+
80+
await sessionStore.load();
81+
expect(sessionStore.get()).toEqual({ value: 5 });
82+
await sessionStore.unload();
83+
});
84+
85+
test(`re-renders when the session is changed externally once`, async () => {
86+
const sessionStore = new SessionStore<TestSession>({ value: 5 }, uuid.v4());
87+
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore);
88+
89+
const renderer = TestRenderer.create(
90+
<SessionStoreManager
91+
loading={<Text>Loading</Text>}
92+
ready={(session, setSession) => (
93+
<Button
94+
title={`Session contains ${session.value}`}
95+
onPress={() => {
96+
setSession({ value: session.value + 1 });
97+
}}
98+
/>
99+
)}
100+
/>
101+
);
102+
103+
await TestRenderer.act(async () => {
104+
await new Promise((resolve) => setTimeout(resolve, 250));
105+
sessionStore.set({ value: 6 });
106+
});
107+
108+
expect(renderer.toTree()?.rendered).toEqual(
109+
expect.objectContaining({
110+
props: expect.objectContaining({
111+
title: `Session contains 6`,
112+
}),
113+
})
114+
);
115+
116+
renderer.unmount();
117+
118+
await new Promise((resolve) => setTimeout(resolve, 250));
119+
120+
await sessionStore.load();
121+
expect(sessionStore.get()).toEqual({ value: 6 });
122+
await sessionStore.unload();
123+
});
124+
125+
test(`re-renders when the session is changed externally twice`, async () => {
126+
const sessionStore = new SessionStore<TestSession>({ value: 5 }, uuid.v4());
127+
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore);
128+
129+
const renderer = TestRenderer.create(
130+
<SessionStoreManager
131+
loading={<Text>Loading</Text>}
132+
ready={(session, setSession) => (
133+
<Button
134+
title={`Session contains ${session.value}`}
135+
onPress={() => {
136+
setSession({ value: session.value + 1 });
137+
}}
138+
/>
139+
)}
140+
/>
141+
);
142+
143+
await TestRenderer.act(async () => {
144+
await new Promise((resolve) => setTimeout(resolve, 250));
145+
sessionStore.set({ value: 6 });
146+
sessionStore.set({ value: 7 });
147+
});
148+
149+
expect(renderer.toTree()?.rendered).toEqual(
150+
expect.objectContaining({
151+
props: expect.objectContaining({
152+
title: `Session contains 7`,
153+
}),
154+
})
155+
);
156+
157+
renderer.unmount();
158+
159+
await new Promise((resolve) => setTimeout(resolve, 250));
160+
161+
await sessionStore.load();
162+
expect(sessionStore.get()).toEqual({ value: 7 });
163+
await sessionStore.unload();
164+
});
165+
166+
test(`re-renders when the session is changed internally once`, async () => {
167+
const sessionStore = new SessionStore<TestSession>({ value: 5 }, uuid.v4());
168+
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore);
169+
170+
const renderer = TestRenderer.create(
171+
<SessionStoreManager
172+
loading={<Text>Loading</Text>}
173+
ready={(session, setSession) => (
174+
<Button
175+
title={`Session contains ${session.value}`}
176+
onPress={() => {
177+
setSession({ value: session.value + 1 });
178+
}}
179+
/>
180+
)}
181+
/>
182+
);
183+
184+
await TestRenderer.act(async () => {
185+
await new Promise((resolve) => setTimeout(resolve, 250));
186+
(renderer.toTree()?.rendered as TestRenderer.ReactTestRendererTree).props[
187+
`onPress`
188+
]();
189+
});
190+
191+
expect(renderer.toTree()?.rendered).toEqual(
192+
expect.objectContaining({
193+
props: expect.objectContaining({
194+
title: `Session contains 6`,
195+
}),
196+
})
197+
);
198+
199+
renderer.unmount();
200+
201+
await new Promise((resolve) => setTimeout(resolve, 250));
202+
203+
await sessionStore.load();
204+
expect(sessionStore.get()).toEqual({ value: 6 });
205+
await sessionStore.unload();
206+
});
207+
208+
test(`re-renders when the session is changed internally twice`, async () => {
209+
const sessionStore = new SessionStore<TestSession>({ value: 5 }, uuid.v4());
210+
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore);
211+
212+
const renderer = TestRenderer.create(
213+
<SessionStoreManager
214+
loading={<Text>Loading</Text>}
215+
ready={(session, setSession) => (
216+
<Button
217+
title={`Session contains ${session.value}`}
218+
onPress={() => {
219+
setSession({ value: session.value + 1 });
220+
}}
221+
/>
222+
)}
223+
/>
224+
);
225+
226+
await TestRenderer.act(async () => {
227+
await new Promise((resolve) => setTimeout(resolve, 250));
228+
(renderer.toTree()?.rendered as TestRenderer.ReactTestRendererTree).props[
229+
`onPress`
230+
]();
231+
(renderer.toTree()?.rendered as TestRenderer.ReactTestRendererTree).props[
232+
`onPress`
233+
]();
234+
});
235+
236+
expect(renderer.toTree()?.rendered).toEqual(
237+
expect.objectContaining({
238+
props: expect.objectContaining({
239+
title: `Session contains 7`,
240+
}),
241+
})
242+
);
243+
244+
renderer.unmount();
245+
246+
await new Promise((resolve) => setTimeout(resolve, 250));
247+
248+
await sessionStore.load();
249+
expect(sessionStore.get()).toEqual({ value: 7 });
250+
await sessionStore.unload();
251+
});

index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { createFlatColorBackgroundComponent } from "./components/createFlatColor
55
export { createHeaderBodyFooterComponent } from "./components/createHeaderBodyFooterComponent";
66
export { createPaddingComponent } from "./components/createPaddingComponent";
77
export { createRoutingComponent } from "./components/createRoutingComponent";
8+
export { createSessionStoreManagerComponent } from "./components/createSessionStoreManagerComponent";
89
export { createStackComponent } from "./components/createStackComponent";
910
export { createTextComponent } from "./components/createTextComponent";
1011
export { SimpleModal } from "./components/SimpleModal";

jest.ts

+2
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ jest.mock(`expo-secure-store`, () => {
111111
return {
112112
async getItemAsync(key: string, options?: unknown): Promise<null | string> {
113113
if (options === undefined) {
114+
await new Promise<void>((resolve) => setTimeout(resolve, 50));
114115
return encryptedStorage.get(key) ?? null;
115116
} else {
116117
throw new Error(
@@ -124,6 +125,7 @@ jest.mock(`expo-secure-store`, () => {
124125
options?: unknown
125126
): Promise<void> {
126127
if (options === undefined) {
128+
await new Promise<void>((resolve) => setTimeout(resolve, 50));
127129
encryptedStorage.set(key, value);
128130
} else {
129131
throw new Error(

readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { createTextComponent } from "react-native-app-helpers";
2323
- [createHeaderBodyFooterComponent](./components/createHeaderBodyFooterComponent/readme.md)
2424
- [createPaddingComponent](./components/createPaddingComponent/readme.md)
2525
- [createRoutingComponent](./components/createRoutingComponent/readme.md)
26+
- [createSessionStoreManagerComponent](./components/createSessionStoreManagerComponent/readme.md)
2627
- [createStackComponent](./components/createStackComponent/readme.md)
2728
- [createTextComponent](./components/createTextComponent/readme.md)
2829
- [SimpleModal](./components/SimpleModal/readme.md)

0 commit comments

Comments
 (0)