@applicaster/zapp-react-native-utils 15.0.0-alpha.5485312433 → 15.0.0-alpha.5531846549
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/README.md +0 -6
- package/actionsExecutor/ActionExecutorContext.tsx +83 -6
- package/appUtils/HooksManager/index.ts +35 -0
- package/appUtils/focusManager/treeDataStructure/Tree/__tests__/Tree.test.js +46 -0
- package/appUtils/focusManager/treeDataStructure/Tree/index.js +18 -18
- package/appUtils/focusManagerAux/utils/index.ts +2 -4
- package/appUtils/focusManagerAux/utils/utils.ios.ts +6 -3
- package/appUtils/localizationsHelper.ts +4 -0
- package/appUtils/playerManager/index.ts +9 -0
- package/appUtils/playerManager/player.ts +1 -1
- package/appUtils/playerManager/playerNative.ts +2 -1
- package/appUtils/playerManager/usePlayer.tsx +5 -3
- package/cellUtils/__tests__/cellUtils.test.ts +39 -0
- package/cellUtils/index.ts +11 -1
- package/dateUtils/__tests__/dayjs.test.ts +0 -3
- package/dateUtils/index.ts +2 -0
- package/manifestUtils/_internals/__tests__/index.test.js +41 -0
- package/manifestUtils/_internals/index.js +33 -0
- package/manifestUtils/defaultManifestConfigurations/player.js +6 -16
- package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +49 -0
- package/manifestUtils/fieldUtils/index.js +54 -0
- package/manifestUtils/index.js +2 -0
- package/manifestUtils/keys.js +228 -0
- package/manifestUtils/mobileAction/button/__tests__/mobileActionButton.test.js +168 -0
- package/manifestUtils/mobileAction/button/index.js +140 -0
- package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +102 -0
- package/manifestUtils/mobileAction/container/index.js +73 -0
- package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +127 -0
- package/manifestUtils/mobileAction/groups/defaults.js +76 -0
- package/manifestUtils/mobileAction/groups/index.js +80 -0
- package/manifestUtils/platformIsTV.js +3 -1
- package/numberUtils/__tests__/toNumber.test.ts +27 -12
- package/numberUtils/__tests__/toPositiveNumber.test.ts +32 -4
- package/numberUtils/index.ts +5 -1
- package/package.json +3 -3
- package/reactHooks/analytics/__tests__/useSendAnalyticsOnPress.test.ts +537 -0
- package/reactHooks/cell-click/index.ts +2 -1
- package/reactHooks/dev/__tests__/useReRenderLog.test.ts +188 -0
- package/reactHooks/device/useIsTablet.tsx +14 -19
- package/reactHooks/events/index.ts +20 -0
- package/reactHooks/feed/index.ts +0 -2
- package/reactHooks/feed/useInflatedUrl.ts +1 -1
- package/reactHooks/index.ts +2 -0
- package/reactHooks/resolvers/useComponentResolver.ts +11 -1
- package/reactHooks/state/__tests__/useComponentScreenState.test.ts +246 -0
- package/reactHooks/state/index.ts +2 -0
- package/reactHooks/state/useComponentScreenState.ts +45 -0
- package/reactHooks/ui/__tests__/useFadeOutWhenBlurred.test.ts +580 -0
- package/rectUtils/__tests__/index.test.ts +549 -0
- package/rectUtils/index.ts +2 -2
- package/refreshUtils/RefreshCoordinator/__tests__/refreshCoordinator.test.ts +206 -0
- package/refreshUtils/RefreshCoordinator/index.ts +245 -0
- package/refreshUtils/RefreshCoordinator/utils/__tests__/getDataRefreshConfig.test.ts +104 -0
- package/refreshUtils/RefreshCoordinator/utils/index.ts +29 -0
- package/screenPickerUtils/__tests__/index.test.ts +333 -0
- package/screenPickerUtils/index.ts +5 -0
- package/screenUtils/index.ts +3 -0
- package/time/__tests__/BackgroundTimer.test.ts +156 -0
- package/time/__tests__/Timer.test.ts +236 -0
- package/typeGuards/__tests__/isString.test.ts +21 -0
- package/typeGuards/index.ts +4 -0
- package/utils/__tests__/clone.test.ts +158 -0
- package/utils/__tests__/path.test.ts +7 -0
- package/utils/clone.ts +7 -0
- package/utils/index.ts +2 -1
- package/reactHooks/componentsMap/index.ts +0 -55
- package/reactHooks/feed/__tests__/useFeedRefresh.test.tsx +0 -75
- package/reactHooks/feed/useFeedRefresh.tsx +0 -65
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react-native";
|
|
2
|
+
import { useReRenderLog } from "../index";
|
|
3
|
+
|
|
4
|
+
describe("useReRenderLog", () => {
|
|
5
|
+
let consoleLogSpy: jest.SpyInstance;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
consoleLogSpy = jest.spyOn(console, "log").mockImplementation();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
consoleLogSpy.mockRestore();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should log on initial render", () => {
|
|
16
|
+
const message = "Test Component";
|
|
17
|
+
|
|
18
|
+
renderHook(() => useReRenderLog(message));
|
|
19
|
+
|
|
20
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
21
|
+
expect.stringContaining("<<RE-RENDERED>>")
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
25
|
+
expect.stringContaining("<<COUNT: 1>>")
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
29
|
+
expect.stringContaining(message)
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should increment count on each re-render", () => {
|
|
34
|
+
const message = "Counter Test";
|
|
35
|
+
|
|
36
|
+
const { rerender } = renderHook(() => useReRenderLog(message));
|
|
37
|
+
|
|
38
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
39
|
+
expect.stringContaining("<<COUNT: 1>>")
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
rerender();
|
|
43
|
+
|
|
44
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
45
|
+
expect.stringContaining("<<COUNT: 2>>")
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
rerender();
|
|
49
|
+
|
|
50
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
51
|
+
expect.stringContaining("<<COUNT: 3>>")
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should log correct message format", () => {
|
|
56
|
+
const message = "MyComponent";
|
|
57
|
+
|
|
58
|
+
renderHook(() => useReRenderLog(message));
|
|
59
|
+
|
|
60
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
61
|
+
"<<RE-RENDERED>><<COUNT: 1>>: MyComponent"
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should track renders independently for different hook instances", () => {
|
|
66
|
+
const message1 = "Component1";
|
|
67
|
+
const message2 = "Component2";
|
|
68
|
+
|
|
69
|
+
const { rerender: rerender1 } = renderHook(() => useReRenderLog(message1));
|
|
70
|
+
|
|
71
|
+
const { rerender: rerender2 } = renderHook(() => useReRenderLog(message2));
|
|
72
|
+
|
|
73
|
+
consoleLogSpy.mockClear();
|
|
74
|
+
|
|
75
|
+
rerender1();
|
|
76
|
+
|
|
77
|
+
expect(consoleLogSpy).toHaveBeenLastCalledWith(
|
|
78
|
+
expect.stringContaining("Component1")
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
rerender2();
|
|
82
|
+
|
|
83
|
+
expect(consoleLogSpy).toHaveBeenLastCalledWith(
|
|
84
|
+
expect.stringContaining("Component2")
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should handle empty string message", () => {
|
|
89
|
+
renderHook(() => useReRenderLog(""));
|
|
90
|
+
|
|
91
|
+
expect(consoleLogSpy).toHaveBeenCalledWith("<<RE-RENDERED>><<COUNT: 1>>: ");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should handle message with special characters", () => {
|
|
95
|
+
const message = "Test!@#$%^&*()";
|
|
96
|
+
|
|
97
|
+
renderHook(() => useReRenderLog(message));
|
|
98
|
+
|
|
99
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
100
|
+
expect.stringContaining(message)
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should handle long messages", () => {
|
|
105
|
+
const message = "A".repeat(1000);
|
|
106
|
+
|
|
107
|
+
renderHook(() => useReRenderLog(message));
|
|
108
|
+
|
|
109
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
110
|
+
expect.stringContaining(message)
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should persist count across multiple re-renders", () => {
|
|
115
|
+
const message = "Persistence Test";
|
|
116
|
+
|
|
117
|
+
const { rerender } = renderHook(() => useReRenderLog(message));
|
|
118
|
+
|
|
119
|
+
for (let i = 2; i <= 10; i++) {
|
|
120
|
+
rerender();
|
|
121
|
+
|
|
122
|
+
expect(consoleLogSpy).toHaveBeenLastCalledWith(
|
|
123
|
+
expect.stringContaining(`<<COUNT: ${i}>>`)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should log on every render without dependencies", () => {
|
|
129
|
+
const message = "Every Render";
|
|
130
|
+
|
|
131
|
+
const { rerender } = renderHook(() => useReRenderLog(message));
|
|
132
|
+
|
|
133
|
+
const initialCallCount = consoleLogSpy.mock.calls.length;
|
|
134
|
+
|
|
135
|
+
rerender();
|
|
136
|
+
rerender();
|
|
137
|
+
rerender();
|
|
138
|
+
|
|
139
|
+
// Should have 4 total calls (initial + 3 rerenders)
|
|
140
|
+
expect(consoleLogSpy).toHaveBeenCalledTimes(initialCallCount + 3);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should handle numeric messages", () => {
|
|
144
|
+
renderHook(() => useReRenderLog(123 as any));
|
|
145
|
+
|
|
146
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("123"));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should handle object messages", () => {
|
|
150
|
+
const obj = { key: "value" };
|
|
151
|
+
|
|
152
|
+
renderHook(() => useReRenderLog(obj as any));
|
|
153
|
+
|
|
154
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should maintain separate refs for each instance", () => {
|
|
158
|
+
const { rerender: rerender1 } = renderHook(() =>
|
|
159
|
+
useReRenderLog("Instance1")
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const { rerender: rerender2 } = renderHook(() =>
|
|
163
|
+
useReRenderLog("Instance2")
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
consoleLogSpy.mockClear();
|
|
167
|
+
|
|
168
|
+
// Render instance 1 twice more
|
|
169
|
+
rerender1();
|
|
170
|
+
rerender1();
|
|
171
|
+
|
|
172
|
+
// Render instance 2 once more
|
|
173
|
+
rerender2();
|
|
174
|
+
|
|
175
|
+
const calls = consoleLogSpy.mock.calls;
|
|
176
|
+
|
|
177
|
+
// Instance 1 should be at count 2 and 3
|
|
178
|
+
expect(calls[0][0]).toContain("Instance1");
|
|
179
|
+
expect(calls[0][0]).toContain("<<COUNT: 2>>");
|
|
180
|
+
|
|
181
|
+
expect(calls[1][0]).toContain("Instance1");
|
|
182
|
+
expect(calls[1][0]).toContain("<<COUNT: 3>>");
|
|
183
|
+
|
|
184
|
+
// Instance 2 should be at count 2
|
|
185
|
+
expect(calls[2][0]).toContain("Instance2");
|
|
186
|
+
expect(calls[2][0]).toContain("<<COUNT: 2>>");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
/* eslint react-native/split-platform-components: off */
|
|
2
2
|
|
|
3
3
|
import { Platform, PlatformIOSStatic, NativeModules } from "react-native";
|
|
4
|
-
import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
5
|
-
import { F as alwaysFalse } from "ramda";
|
|
6
4
|
import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
|
|
7
5
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
6
|
+
export const useIsTablet = (): boolean => {
|
|
7
|
+
const platform = Platform.OS;
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
};
|
|
9
|
+
if (platform === "ios") {
|
|
10
|
+
const { isPad } = Platform as PlatformIOSStatic;
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
return Boolean(isPad);
|
|
13
|
+
}
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
if (platform === "android") {
|
|
16
|
+
const initialProps =
|
|
17
|
+
NativeModules.QuickBrickCommunicationModule?.initialProps;
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
android_tv: alwaysFalse,
|
|
26
|
-
tvos: alwaysFalse,
|
|
27
|
-
default: alwaysFalse,
|
|
28
|
-
});
|
|
19
|
+
return toBooleanWithDefaultFalse(initialProps?.is_tablet);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return false;
|
|
23
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { scrollEndReached$ } from "@applicaster/zapp-react-native-ui-components/events";
|
|
3
|
+
import { filter } from "rxjs/operators";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Subscribe to scroll-end-reached and call loadNextData only when this item is last in the river.
|
|
7
|
+
* Use in Grid/List so they load next page when the user scrolls to the end (river or tabs scroll container).
|
|
8
|
+
*/
|
|
9
|
+
export function useScrollEndReachedLoadNext(
|
|
10
|
+
isLast: boolean,
|
|
11
|
+
loadNextData: () => void
|
|
12
|
+
): void {
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
const sub = scrollEndReached$.pipe(filter(() => isLast)).subscribe(() => {
|
|
15
|
+
loadNextData();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return () => sub.unsubscribe();
|
|
19
|
+
}, [isLast, loadNextData]);
|
|
20
|
+
}
|
package/reactHooks/feed/index.ts
CHANGED
package/reactHooks/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/// <reference types="@applicaster/applicaster-types" />
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
|
|
4
|
+
import { coreLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
5
|
+
|
|
4
6
|
import { findComponentByType } from "@applicaster/zapp-react-native-utils/pluginUtils";
|
|
5
7
|
import {
|
|
6
8
|
usePlugins,
|
|
@@ -23,7 +25,7 @@ export function useComponentResolver(
|
|
|
23
25
|
|
|
24
26
|
const components = useAppSelector(selectComponents);
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
const component = React.useMemo(
|
|
27
29
|
() =>
|
|
28
30
|
findComponentByType({
|
|
29
31
|
components,
|
|
@@ -33,4 +35,12 @@ export function useComponentResolver(
|
|
|
33
35
|
}),
|
|
34
36
|
watchers
|
|
35
37
|
);
|
|
38
|
+
|
|
39
|
+
if (!component) {
|
|
40
|
+
coreLogger.warn({
|
|
41
|
+
message: `useComponentResolver: Component of type ${componentType} not found`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return component;
|
|
36
46
|
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { renderHook, act } from "@testing-library/react-native";
|
|
2
|
+
import { useComponentScreenState } from "../useComponentScreenState";
|
|
3
|
+
import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
|
|
4
|
+
|
|
5
|
+
import { create } from "zustand";
|
|
6
|
+
|
|
7
|
+
const createMockStore = () => create(() => ({}));
|
|
8
|
+
|
|
9
|
+
// Mock the external dependencies
|
|
10
|
+
jest.mock(
|
|
11
|
+
"@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext",
|
|
12
|
+
() => ({
|
|
13
|
+
useScreenContextV2: jest.fn(),
|
|
14
|
+
})
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const mockUseScreenContextV2 = useScreenContextV2 as jest.MockedFunction<
|
|
18
|
+
typeof useScreenContextV2
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
describe("useComponentScreenState", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should return initial value when no state exists for componentId", () => {
|
|
27
|
+
const mockStore = createMockStore();
|
|
28
|
+
|
|
29
|
+
mockUseScreenContextV2.mockReturnValue({
|
|
30
|
+
_componentStateStore: mockStore,
|
|
31
|
+
} as any);
|
|
32
|
+
|
|
33
|
+
const initialValue = "default-value";
|
|
34
|
+
const componentId = "test-component";
|
|
35
|
+
|
|
36
|
+
const { result } = renderHook(() =>
|
|
37
|
+
useComponentScreenState(componentId, initialValue)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(result.current[0]).toBe(initialValue);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should return stored value when state exists for componentId", () => {
|
|
44
|
+
const mockStore = createMockStore();
|
|
45
|
+
mockStore.setState({ "existing-component": "stored-value" });
|
|
46
|
+
|
|
47
|
+
mockUseScreenContextV2.mockReturnValue({
|
|
48
|
+
_componentStateStore: mockStore,
|
|
49
|
+
} as any);
|
|
50
|
+
|
|
51
|
+
const { result } = renderHook(() =>
|
|
52
|
+
useComponentScreenState("existing-component", "default")
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(result.current[0]).toBe("stored-value");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should update state when setter is called", () => {
|
|
59
|
+
const mockStore = createMockStore();
|
|
60
|
+
|
|
61
|
+
mockUseScreenContextV2.mockReturnValue({
|
|
62
|
+
_componentStateStore: mockStore,
|
|
63
|
+
} as any);
|
|
64
|
+
|
|
65
|
+
const { result } = renderHook(() =>
|
|
66
|
+
useComponentScreenState("update-test", "initial")
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Verify initial value
|
|
70
|
+
expect(result.current[0]).toBe("initial");
|
|
71
|
+
|
|
72
|
+
// Update the value
|
|
73
|
+
act(() => {
|
|
74
|
+
result.current[1]("updated-value");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Verify updated value
|
|
78
|
+
expect(result.current[0]).toBe("updated-value");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should handle different data types correctly", () => {
|
|
82
|
+
const mockStore = createMockStore();
|
|
83
|
+
|
|
84
|
+
mockUseScreenContextV2.mockReturnValue({
|
|
85
|
+
_componentStateStore: mockStore,
|
|
86
|
+
} as any);
|
|
87
|
+
|
|
88
|
+
// Test with number
|
|
89
|
+
const { result: numberResult } = renderHook(() =>
|
|
90
|
+
useComponentScreenState<number>("number-test", 42)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(numberResult.current[0]).toBe(42);
|
|
94
|
+
|
|
95
|
+
// Test with boolean
|
|
96
|
+
const { result: boolResult } = renderHook(() =>
|
|
97
|
+
useComponentScreenState<boolean>("bool-test", true)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(boolResult.current[0]).toBe(true);
|
|
101
|
+
|
|
102
|
+
// Test with object
|
|
103
|
+
const testObj = { key: "value" };
|
|
104
|
+
|
|
105
|
+
const { result: objResult } = renderHook(() =>
|
|
106
|
+
useComponentScreenState<{ key: string }>("obj-test", testObj)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(objResult.current[0]).toEqual(testObj);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should maintain separate states for different componentIds", () => {
|
|
113
|
+
const mockStore = createMockStore();
|
|
114
|
+
|
|
115
|
+
mockUseScreenContextV2.mockReturnValue({
|
|
116
|
+
_componentStateStore: mockStore,
|
|
117
|
+
} as any);
|
|
118
|
+
|
|
119
|
+
const { result: firstResult } = renderHook(() =>
|
|
120
|
+
useComponentScreenState("first", "first-initial")
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const { result: secondResult } = renderHook(() =>
|
|
124
|
+
useComponentScreenState("second", "second-initial")
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Verify initial values are separate
|
|
128
|
+
expect(firstResult.current[0]).toBe("first-initial");
|
|
129
|
+
expect(secondResult.current[0]).toBe("second-initial");
|
|
130
|
+
|
|
131
|
+
// Update first component's state
|
|
132
|
+
act(() => {
|
|
133
|
+
firstResult.current[1]("first-updated");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Verify only first component's state changed
|
|
137
|
+
expect(firstResult.current[0]).toBe("first-updated");
|
|
138
|
+
expect(secondResult.current[0]).toBe("second-initial");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should return a memoized setter function", () => {
|
|
142
|
+
const mockStore = createMockStore();
|
|
143
|
+
|
|
144
|
+
mockUseScreenContextV2.mockReturnValue({
|
|
145
|
+
_componentStateStore: mockStore,
|
|
146
|
+
} as any);
|
|
147
|
+
|
|
148
|
+
const { result, rerender } = renderHook(
|
|
149
|
+
({ id, initial }) => useComponentScreenState(id, initial),
|
|
150
|
+
{
|
|
151
|
+
initialProps: { id: "memo-test", initial: "initial" },
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const firstSetter = result.current[1];
|
|
156
|
+
|
|
157
|
+
// Rerender with same props
|
|
158
|
+
rerender({ id: "memo-test", initial: "initial" });
|
|
159
|
+
|
|
160
|
+
const secondSetter = result.current[1];
|
|
161
|
+
|
|
162
|
+
// Setter should be the same instance due to useCallback
|
|
163
|
+
expect(firstSetter).toBe(secondSetter);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should update setter when componentId changes", () => {
|
|
167
|
+
const mockStore = createMockStore();
|
|
168
|
+
|
|
169
|
+
mockUseScreenContextV2.mockReturnValue({
|
|
170
|
+
_componentStateStore: mockStore,
|
|
171
|
+
} as any);
|
|
172
|
+
|
|
173
|
+
const { result, rerender } = renderHook(
|
|
174
|
+
({ id, initial }) => useComponentScreenState(id, initial),
|
|
175
|
+
{
|
|
176
|
+
initialProps: { id: "old-id", initial: "initial" },
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const firstSetter = result.current[1];
|
|
181
|
+
|
|
182
|
+
// Rerender with different componentId
|
|
183
|
+
rerender({ id: "new-id", initial: "initial" });
|
|
184
|
+
|
|
185
|
+
const secondSetter = result.current[1];
|
|
186
|
+
|
|
187
|
+
// Setter should be different because componentId changed
|
|
188
|
+
expect(firstSetter).not.toBe(secondSetter);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should call store.setState with correct parameters", () => {
|
|
192
|
+
const mockStore = createMockStore();
|
|
193
|
+
const setStateSpy = jest.spyOn(mockStore, "setState");
|
|
194
|
+
|
|
195
|
+
mockUseScreenContextV2.mockReturnValue({
|
|
196
|
+
_componentStateStore: mockStore,
|
|
197
|
+
} as any);
|
|
198
|
+
|
|
199
|
+
const { result } = renderHook(() =>
|
|
200
|
+
useComponentScreenState("state-test", "initial")
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const newValue = "new-value";
|
|
204
|
+
|
|
205
|
+
act(() => {
|
|
206
|
+
result.current[1](newValue);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(setStateSpy).toHaveBeenCalledWith({
|
|
210
|
+
"state-test": newValue,
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should handle falsy initial values correctly", () => {
|
|
215
|
+
const mockStore = createMockStore();
|
|
216
|
+
|
|
217
|
+
mockUseScreenContextV2.mockReturnValue({
|
|
218
|
+
_componentStateStore: mockStore,
|
|
219
|
+
} as any);
|
|
220
|
+
|
|
221
|
+
// Test with falsy values
|
|
222
|
+
const { result: nullResult } = renderHook(() =>
|
|
223
|
+
useComponentScreenState("null-test", null)
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(nullResult.current[0]).toBeNull();
|
|
227
|
+
|
|
228
|
+
const { result: falseResult } = renderHook(() =>
|
|
229
|
+
useComponentScreenState("false-test", false)
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expect(falseResult.current[0]).toBe(false);
|
|
233
|
+
|
|
234
|
+
const { result: zeroResult } = renderHook(() =>
|
|
235
|
+
useComponentScreenState("zero-test", 0)
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(zeroResult.current[0]).toBe(0);
|
|
239
|
+
|
|
240
|
+
const { result: emptyStrResult } = renderHook(() =>
|
|
241
|
+
useComponentScreenState("empty-test", "")
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
expect(emptyStrResult.current[0]).toBe("");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
|
|
3
|
+
import { useStore } from "zustand";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A custom hook that provides persistent state storage across component re-mounts
|
|
7
|
+
* using the screen's component state store.
|
|
8
|
+
*
|
|
9
|
+
* @see {@link DOCS/adr/0010-useComponentScreenState.md} - ADR explaining why useComponentScreenState is used for List/Hero/Grid/Gallery components
|
|
10
|
+
*
|
|
11
|
+
* The state is shared across all components using the same componentId within the same screen context.
|
|
12
|
+
* If no value exists for the componentId, the initial value is returned as a fallback but is not persisted in the store.
|
|
13
|
+
* The initial value must be explicitly set using the returned setter function to persist it.
|
|
14
|
+
*
|
|
15
|
+
* @template T - The type of value being stored
|
|
16
|
+
* @param componentId - Unique identifier for this state in the component state store
|
|
17
|
+
* @param initialValue - The default value if no value exists for the componentId
|
|
18
|
+
* @returns A tuple containing the current value and a memoized setter function
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* const [count, setCount] = useComponentScreenState<number>('counter', 0);
|
|
23
|
+
* const [name, setName] = useComponentScreenState<string>('username', 'Anonymous');
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const useComponentScreenState = <T = unknown>(
|
|
27
|
+
componentId: string,
|
|
28
|
+
initialValue: T
|
|
29
|
+
): [T, (value: T) => void] => {
|
|
30
|
+
const store = useScreenContextV2()._componentStateStore;
|
|
31
|
+
|
|
32
|
+
const value = useStore(
|
|
33
|
+
store,
|
|
34
|
+
(state) => (state[componentId] as T | undefined) ?? initialValue
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const setValue = React.useCallback(
|
|
38
|
+
(nextValue: T) => {
|
|
39
|
+
store.setState({ [componentId]: nextValue });
|
|
40
|
+
},
|
|
41
|
+
[componentId, store]
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return [value, setValue] as const;
|
|
45
|
+
};
|