@applicaster/zapp-react-native-ui-components 15.0.0-alpha.6351072502 → 15.0.0-alpha.6423012104

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.
@@ -13,7 +13,7 @@ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/he
13
13
  import { ScreenTrackedViewPositionsContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenTrackedViewPositionsContext";
14
14
  import { useEventAlerts } from "./utils/useEventAlerts";
15
15
 
16
- const { log_info } = createLogger({
16
+ const { log_debug } = createLogger({
17
17
  category: "ScreenContainer",
18
18
  subsystem: "General",
19
19
  });
@@ -54,20 +54,15 @@ export const GeneralContentScreen = ({
54
54
  useEffect(() => {
55
55
  if (!riverActionProvidersReady) {
56
56
  if (actionsInitialStateSetters.length > 0) {
57
- log_info(
58
- "ScreenContainer: starting to check river action providers to initialize",
59
- { actionsInitialStateSetters }
60
- );
61
-
62
57
  allSettled(actionsInitialStateSetters).finally(() => {
63
- log_info(
58
+ log_debug(
64
59
  "ScreenContainer: action provider ready, completed. Starting to present screen"
65
60
  );
66
61
 
67
62
  setRiverActionProvidersReady(true);
68
63
  });
69
64
  } else {
70
- log_info(
65
+ log_debug(
71
66
  "ScreenContainer: no action provider to check, completed. Starting to present screen"
72
67
  );
73
68
 
@@ -5,8 +5,6 @@ import {
5
5
  usePlugins,
6
6
  } from "@applicaster/zapp-react-native-redux/hooks";
7
7
  import {
8
- useDimensions,
9
- useIsTablet as isTablet,
10
8
  useNavigation,
11
9
  useRivers,
12
10
  } from "@applicaster/zapp-react-native-utils/reactHooks";
@@ -15,8 +13,8 @@ import { BufferAnimation } from "../PlayerContainer/BufferAnimation";
15
13
  import { PlayerContainer } from "../PlayerContainer";
16
14
  import { useModalSize } from "../VideoModal/hooks";
17
15
  import { ViewStyle } from "react-native";
18
- import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
19
16
  import { findCastPlugin, getPlayer } from "./utils";
17
+ import { useWaitForValidOrientation } from "../Screen/hooks";
20
18
 
21
19
  type Props = {
22
20
  item: ZappEntry;
@@ -31,13 +29,6 @@ type PlayableComponent = {
31
29
  Component: React.ComponentType<any>;
32
30
  };
33
31
 
34
- const dimensionsContext: "window" | "screen" = platformSelect({
35
- android_tv: "window",
36
- amazon: "window",
37
- // eslint-disable-next-line react-hooks/rules-of-hooks
38
- default: isTablet() ? "window" : "screen", // on tablet, window represents correct values, on phone it's not as the screen could be rotated
39
- });
40
-
41
32
  export function HandlePlayable({
42
33
  item,
43
34
  isModal,
@@ -97,27 +88,23 @@ export function HandlePlayable({
97
88
  });
98
89
  }, [casting]);
99
90
 
100
- const { width: screenWidth, height: screenHeight } =
101
- useDimensions(dimensionsContext);
102
-
103
91
  const modalSize = useModalSize();
104
92
 
105
- const style = React.useMemo(
106
- () =>
107
- ({
108
- width: isModal
109
- ? modalSize.width
110
- : mode === "PIP"
111
- ? "100%"
112
- : screenWidth,
113
- height: isModal
114
- ? modalSize.height
115
- : mode === "PIP"
116
- ? "100%"
117
- : screenHeight,
118
- }) as ViewStyle,
119
- [screenWidth, screenHeight, modalSize, isModal, mode]
120
- );
93
+ const isOrientationReady = useWaitForValidOrientation();
94
+
95
+ const style = React.useMemo(() => {
96
+ const isFullScreenReady =
97
+ mode === "PIP" || (mode === "FULLSCREEN" && isOrientationReady);
98
+
99
+ const getDimensionValue = (value: string | number) => {
100
+ return isModal ? value : isFullScreenReady ? "100%" : 0; // do not show player, until full screen mode is ready
101
+ };
102
+
103
+ return {
104
+ width: getDimensionValue(modalSize.width),
105
+ height: getDimensionValue(modalSize.height),
106
+ } as ViewStyle;
107
+ }, [modalSize, isModal, mode, isOrientationReady]);
121
108
 
122
109
  const Component = playable?.Component;
123
110
 
@@ -5,6 +5,14 @@ import {
5
5
 
6
6
  import { CHROMECAST_PLUGIN_ID, YOUTUBE_PLUGIN_ID } from "./const";
7
7
  import { omit } from "@applicaster/zapp-react-native-utils/utils";
8
+ import { getXray } from "@applicaster/zapp-react-native-utils/logger";
9
+
10
+ const { Logger } = getXray();
11
+
12
+ const logger = new Logger(
13
+ "QuickBrick",
14
+ "packages/zapp-react-native-ui-components/Components/HandlePlayable"
15
+ );
8
16
 
9
17
  const getPlayerModuleProperties = (PlayerModule: ZappPlugin) => {
10
18
  if (PlayerModule?.Component && typeof PlayerModule.Component === "object") {
@@ -52,10 +60,25 @@ export const getPlayer = (
52
60
  if (type) {
53
61
  PlayerModule = findPluginByIdentifier(type, plugins)?.module;
54
62
 
63
+ if (!PlayerModule) {
64
+ logger.error({
65
+ message:
66
+ "PlayerModule is undefined – type mapping may be wrong or type not set for player",
67
+ data: {
68
+ type,
69
+ screen_id,
70
+ item_type_value: item?.type?.value,
71
+ },
72
+ });
73
+
74
+ return [null, {}];
75
+ }
76
+
55
77
  return getPlayerWithModuleProperties(PlayerModule);
56
78
  }
57
79
  }
58
80
 
81
+ // TODO: Probably should be removed, Youtube plugin is deprecated
59
82
  if (item?.content?.type === "youtube-id") {
60
83
  PlayerModule = findYoutubePlugin(plugins)?.module;
61
84
 
@@ -70,5 +93,13 @@ export const getPlayer = (
70
93
  )
71
94
  );
72
95
 
96
+ if (!PlayerModule) {
97
+ logger.error({
98
+ message: "PlayerModule is undefined – playable plugin not found",
99
+ });
100
+
101
+ return [null, {}];
102
+ }
103
+
73
104
  return getPlayerWithModuleProperties(PlayerModule);
74
105
  };
@@ -61,7 +61,6 @@ import {
61
61
  PlayerNativeSendCommand,
62
62
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
63
63
  import { useAppData } from "@applicaster/zapp-react-native-redux";
64
- import { useRestrictMobilePlayback } from "./useRestrictMobilePlayback";
65
64
 
66
65
  type Props = {
67
66
  Player: React.ComponentType<any>;
@@ -274,13 +273,6 @@ const PlayerContainerComponent = (props: Props) => {
274
273
  );
275
274
  }, [playerManager.isRegistered()]);
276
275
 
277
- const { isRestricted } = useRestrictMobilePlayback({
278
- player,
279
- entry: item,
280
- pluginConfiguration,
281
- close,
282
- });
283
-
284
276
  const playEntry = (entry) => navigator.replaceTop(entry, { mode });
285
277
 
286
278
  const onPlayNextPerformNextVideoPlay = React.useCallback(() => {
@@ -670,7 +662,7 @@ const PlayerContainerComponent = (props: Props) => {
670
662
  <PlayerFocusableWrapperView
671
663
  nextFocusDown={context.bottomFocusableId}
672
664
  >
673
- {isRestricted ? null : (
665
+ {!Player ? null : (
674
666
  <Player
675
667
  source={{
676
668
  uri,
@@ -23,6 +23,7 @@ import { isLast } from "@applicaster/zapp-react-native-utils/arrayUtils";
23
23
  import { withComponentsMapProvider } from "@applicaster/zapp-react-native-ui-components/Decorators/ComponentsMapWrapper";
24
24
  import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
25
25
  import { useShallow } from "zustand/react/shallow";
26
+ import { emitScrollEndReached } from "@applicaster/zapp-react-native-ui-components/events";
26
27
 
27
28
  import { isAndroidPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
28
29
  import { ComponentsMapHeightContext } from "./ContextProviders/ComponentsMapHeightContext";
@@ -73,6 +74,7 @@ function ComponentsMapComponent(props: Props) {
73
74
 
74
75
  const flatListRef = React.useRef<FlatList | null>(null);
75
76
  const flatListWrapperRef = React.useRef<View | null>(null);
77
+ const hasUserScrolledRef = React.useRef(false);
76
78
  const screenConfig = useScreenConfiguration(riverId);
77
79
  const screenData = useScreenData(riverId);
78
80
  const pullToRefreshEnabled = screenData?.rules?.pull_to_refresh_enabled;
@@ -236,6 +238,8 @@ function ComponentsMapComponent(props: Props) {
236
238
  }, []);
237
239
 
238
240
  const onScroll = React.useCallback((event) => {
241
+ hasUserScrolledRef.current = true;
242
+
239
243
  const {
240
244
  nativeEvent: {
241
245
  contentOffset: { y },
@@ -277,6 +281,7 @@ function ComponentsMapComponent(props: Props) {
277
281
  >
278
282
  <ViewportTracker>
279
283
  <FlatList
284
+ testID="components-map-flat-list"
280
285
  ref={(ref) => {
281
286
  flatListRef.current = ref;
282
287
  }}
@@ -308,6 +313,17 @@ function ComponentsMapComponent(props: Props) {
308
313
  onScrollEndDrag={_onScrollEndDrag}
309
314
  scrollEventThrottle={16}
310
315
  {...scrollViewExtraProps}
316
+ onEndReached={
317
+ /* When wrapped in a parent ScrollView (e.g. tabs),
318
+ this FlatList doesn't scroll so onEndReached can fire repeatedly;
319
+ skip it here and let the parent ScrollView emit scroll-end instead. */
320
+ isScreenWrappedInContainer
321
+ ? undefined
322
+ : () => {
323
+ if (!hasUserScrolledRef.current) return;
324
+ emitScrollEndReached();
325
+ }
326
+ }
311
327
  />
312
328
  </ViewportTracker>
313
329
  </ScreenLoadingMeasurements>
@@ -137,6 +137,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
137
137
  keyExtractor={[Function]}
138
138
  maxToRenderPerBatch={10}
139
139
  onContentSizeChange={[Function]}
140
+ onEndReached={[Function]}
140
141
  onLayout={[Function]}
141
142
  onMomentumScrollBegin={[Function]}
142
143
  onMomentumScrollEnd={[Function]}
@@ -154,6 +155,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
154
155
  }
155
156
  }
156
157
  stickyHeaderIndices={[]}
158
+ testID="components-map-flat-list"
157
159
  viewabilityConfigCallbackPairs={[]}
158
160
  windowSize={12}
159
161
  >
@@ -139,7 +139,13 @@ jest.mock(
139
139
  })
140
140
  );
141
141
 
142
+ jest.mock("@applicaster/zapp-react-native-ui-components/events", () => ({
143
+ ...jest.requireActual("@applicaster/zapp-react-native-ui-components/events"),
144
+ emitScrollEndReached: jest.fn(),
145
+ }));
146
+
142
147
  const { View } = require("react-native");
148
+ const events = require("@applicaster/zapp-react-native-ui-components/events");
143
149
  const { ComponentsMap } = require("../ComponentsMap/ComponentsMap");
144
150
  const theme = require("./theme-mock.json");
145
151
 
@@ -190,4 +196,36 @@ describe("componentsMap", () => {
190
196
 
191
197
  expect(toJSON()).toMatchSnapshot();
192
198
  });
199
+
200
+ it("calls emitScrollEndReached when onScroll was called and isScreenWrappedInContainer is false", () => {
201
+ themeSpy = jest
202
+ .spyOn(themeUtils, "useTheme")
203
+ .mockImplementation(() => () => theme);
204
+
205
+ events.emitScrollEndReached.mockClear();
206
+
207
+ const { getByTestId } = render(
208
+ <Provider store={store}>
209
+ <ComponentsMap
210
+ {...props}
211
+ isScreenWrappedInContainer={false}
212
+ feed={{ entry: [] }}
213
+ />
214
+ </Provider>
215
+ );
216
+
217
+ const flatList = getByTestId("components-map-flat-list");
218
+
219
+ flatList.props.onScroll({
220
+ nativeEvent: {
221
+ contentOffset: { y: 0 },
222
+ layoutMeasurement: { height: 100 },
223
+ contentSize: { height: 200 },
224
+ },
225
+ });
226
+
227
+ flatList.props.onEndReached();
228
+
229
+ expect(events.emitScrollEndReached).toHaveBeenCalledTimes(1);
230
+ });
193
231
  });
@@ -11,6 +11,7 @@ const mockIsOrientationCompatible = jest.fn(() => true);
11
11
  jest.mock("react-native-safe-area-context", () => ({
12
12
  ...jest.requireActual("react-native-safe-area-context"),
13
13
  useSafeAreaInsets: () => ({ top: 44 }),
14
+ useSafeAreaFrame: () => ({ x: 0, y: 0, width: 375, height: 812 }),
14
15
  }));
15
16
 
16
17
  jest.mock("../../RouteManager", () => ({
@@ -5,15 +5,85 @@ import {
5
5
  } from "@applicaster/zapp-react-native-utils/appUtils/orientationHelper";
6
6
  import {
7
7
  useCurrentScreenData,
8
- useDimensions,
9
8
  useRoute,
10
9
  useIsTablet,
10
+ useIsScreenActive,
11
11
  } from "@applicaster/zapp-react-native-utils/reactHooks";
12
12
  import { useMemo, useEffect, useState } from "react";
13
+ import { useSafeAreaFrame } from "react-native-safe-area-context";
14
+
15
+ type MemoizedSafeAreaFrameWithActiveStateOptions = {
16
+ updateForInactiveScreens?: boolean;
17
+ isActive: boolean;
18
+ };
19
+
20
+ /**
21
+ * Base hook that wraps useSafeAreaFrame with memoization and inactive screen filtering.
22
+ * Requires isActive to be passed explicitly - use useMemoizedSafeAreaFrame for automatic detection.
23
+ *
24
+ * @param options.updateForInactiveScreens - If false, frame won't update when screen is inactive (default: true)
25
+ * @param options.isActive - Whether the screen is currently active
26
+ * @returns The memoized safe area frame { x, y, width, height }
27
+ */
28
+ export const useMemoizedSafeAreaFrameWithActiveState = (
29
+ options: MemoizedSafeAreaFrameWithActiveStateOptions
30
+ ) => {
31
+ const { updateForInactiveScreens = true, isActive } = options;
32
+ const frame = useSafeAreaFrame();
33
+
34
+ const [memoFrame, setMemoFrame] = useState(frame);
35
+
36
+ useEffect(() => {
37
+ const shouldUpdate = isActive || updateForInactiveScreens;
38
+
39
+ const dimensionsChanged =
40
+ frame.width !== memoFrame.width || frame.height !== memoFrame.height;
41
+
42
+ if (shouldUpdate && dimensionsChanged) {
43
+ setMemoFrame(frame);
44
+ }
45
+ }, [
46
+ frame.width,
47
+ frame.height,
48
+ isActive,
49
+ updateForInactiveScreens,
50
+ frame,
51
+ memoFrame.width,
52
+ memoFrame.height,
53
+ ]);
54
+
55
+ return memoFrame;
56
+ };
57
+
58
+ type MemoizedSafeAreaFrameOptions = {
59
+ updateForInactiveScreens?: boolean;
60
+ };
61
+
62
+ /**
63
+ * Hook that wraps useSafeAreaFrame with memoization and inactive screen filtering.
64
+ * Uses useIsScreenActive() internally to determine active state - use useMemoizedSafeAreaFrameWithActiveState
65
+ * if you need to pass isActive explicitly.
66
+ *
67
+ * @param options.updateForInactiveScreens - If false, frame won't update when screen is inactive (default: true)
68
+ * @returns The memoized safe area frame { x, y, width, height }
69
+ */
70
+ export const useMemoizedSafeAreaFrame = (
71
+ options: MemoizedSafeAreaFrameOptions = {}
72
+ ) => {
73
+ const { updateForInactiveScreens = true } = options;
74
+ const isActive = useIsScreenActive();
75
+
76
+ return useMemoizedSafeAreaFrameWithActiveState({
77
+ updateForInactiveScreens,
78
+ isActive,
79
+ });
80
+ };
13
81
 
14
82
  export const useWaitForValidOrientation = () => {
15
- const { width: screenWidth, height } = useDimensions("screen", {
16
- fullDimensions: true,
83
+ // Use memoized safe area frame to synchronize with Scene's dimension source
84
+ // This prevents race conditions where the orientation check passes before
85
+ // Scene's memoFrame has updated to the new dimensions
86
+ const { width: screenWidth, height } = useMemoizedSafeAreaFrame({
17
87
  updateForInactiveScreens: false,
18
88
  });
19
89
 
@@ -92,7 +92,13 @@ export function Screen(_props: Props) {
92
92
  const isActive = useIsScreenActive();
93
93
 
94
94
  const ref = React.useRef(null);
95
- const isReady = useWaitForValidOrientation();
95
+ const isOrientationReady = useWaitForValidOrientation();
96
+
97
+ // Playable screens handle their own orientation via the native player plugin,
98
+ // so we skip the orientation wait gate to avoid a deadlock where the screen
99
+ // waits for landscape but blocks rendering that would trigger the rotation.
100
+ const isPlayableRoute = pathname?.includes("/playable");
101
+ const isReady = isOrientationReady || isPlayableRoute;
96
102
 
97
103
  React.useEffect(() => {
98
104
  if (ref.current && isActive && isReady) {
@@ -57,7 +57,6 @@ export function ScreenResolverComponent(props: Props) {
57
57
  const { hookPlugin } = screenData || {};
58
58
 
59
59
  const plugins = usePlugins();
60
- const rivers = useRivers();
61
60
 
62
61
  const components = useAppSelector(selectComponents);
63
62
 
@@ -65,12 +64,6 @@ export function ScreenResolverComponent(props: Props) {
65
64
  videoModalState: { mode },
66
65
  } = useNavigation();
67
66
 
68
- const [, setScreenContext] = ZappPipesScreenContext.useZappPipesContext();
69
-
70
- React.useEffect(() => {
71
- setScreenContext(rivers[screenId]);
72
- }, [rivers, screenId, setScreenContext]);
73
-
74
67
  const parentCallback = props.resultCallback;
75
68
 
76
69
  const screenAction = useCallbackActions(
@@ -150,6 +143,17 @@ export function ScreenResolverComponent(props: Props) {
150
143
  return null;
151
144
  }
152
145
 
153
- export const ScreenResolver = ZappPipesScreenContext.withProvider(
154
- ScreenResolverComponent
155
- );
146
+ function withDefaultScreenContext(Component: React.ComponentType<any>) {
147
+ return function WithDefaultScreenContext(props: any) {
148
+ const screenId = props.screenId;
149
+ const rivers = useRivers();
150
+
151
+ return (
152
+ <ZappPipesScreenContext.Provider initialContextValue={rivers[screenId]}>
153
+ <Component {...props} />
154
+ </ZappPipesScreenContext.Provider>
155
+ );
156
+ };
157
+ }
158
+
159
+ export const ScreenResolver = withDefaultScreenContext(ScreenResolverComponent);
@@ -1,9 +1,9 @@
1
- import React, { useEffect } from "react";
1
+ import React from "react";
2
2
  import { equals } from "ramda";
3
3
  import { Animated, ViewProps, ViewStyle } from "react-native";
4
- import { useSafeAreaFrame } from "react-native-safe-area-context";
5
4
 
6
5
  import { useScreenOrientationHandler } from "@applicaster/zapp-react-native-ui-components/Components/Screen/orientationHandler";
6
+ import { useMemoizedSafeAreaFrameWithActiveState } from "@applicaster/zapp-react-native-ui-components/Components/Screen/hooks";
7
7
 
8
8
  import { PathnameContext } from "../../Contexts/PathnameContext";
9
9
  import { ScreenDataContext } from "../../Contexts/ScreenDataContext";
@@ -94,19 +94,13 @@ function SceneComponent({
94
94
  isActive,
95
95
  });
96
96
 
97
- const frame = useSafeAreaFrame();
98
-
99
- const [memoFrame, setMemoFrame] = React.useState(frame);
100
-
101
- useEffect(() => {
102
- if (isActive) {
103
- setMemoFrame((oldFrame) =>
104
- oldFrame.width === frame.width && oldFrame.height === frame.height
105
- ? oldFrame
106
- : frame
107
- );
108
- }
109
- }, [isActive, frame.width, frame.height]);
97
+ // Use shared memoized frame hook - synchronized with useWaitForValidOrientation
98
+ // to prevent race conditions during orientation changes
99
+ // Pass isActive from props since Scene knows its active state from Transitioner
100
+ const memoFrame = useMemoizedSafeAreaFrameWithActiveState({
101
+ updateForInactiveScreens: false,
102
+ isActive,
103
+ });
110
104
 
111
105
  const isAnimating = animating && overlayStyle;
112
106
 
@@ -22,8 +22,6 @@ ReactNative.UIManager.measureLayout = jest.fn(
22
22
  }
23
23
  );
24
24
 
25
- ReactNative.findNodeHandle = () => 1234;
26
-
27
25
  const viewportEventsManager = new ViewportEvents(true);
28
26
 
29
27
  const TestComponent = (props) => {
@@ -32,7 +32,7 @@ function getTestDimensions(testDimensions) {
32
32
  return (process.env.NODE_ENV === "test" && testDimensions) || null;
33
33
  }
34
34
 
35
- function ViewportAwareComponent(props: Props, ref) {
35
+ function ViewportAwareComponent(props: Props, forwardedRef) {
36
36
  const viewportEvents = useViewportEventsContext();
37
37
 
38
38
  const {
@@ -47,13 +47,22 @@ function ViewportAwareComponent(props: Props, ref) {
47
47
  getTestDimensions(testDimensions) || initialDimensions
48
48
  );
49
49
 
50
+ const localRef = React.useRef(null);
51
+
50
52
  const [viewportChangeEvent, setViewportChangeEvent] = React.useState(null);
51
53
 
52
- function assignRef(_ref) {
53
- if (_ref && !ref) {
54
- ref = _ref;
55
- }
56
- }
54
+ const assignRef = React.useCallback(
55
+ (_ref) => {
56
+ localRef.current = _ref;
57
+
58
+ if (typeof forwardedRef === "function") {
59
+ forwardedRef(_ref);
60
+ } else if (forwardedRef) {
61
+ forwardedRef.current = _ref;
62
+ }
63
+ },
64
+ [forwardedRef]
65
+ );
57
66
 
58
67
  const checkInViewport = (viewportChangeEvent, layoutEvent) => {
59
68
  const inVerticalViewport = Utils.isInViewport(
@@ -94,7 +103,7 @@ function ViewportAwareComponent(props: Props, ref) {
94
103
  const onViewportChange = (viewportChangeEvent) => {
95
104
  setViewportChangeEvent(viewportChangeEvent);
96
105
 
97
- const nodeHandle = findNodeHandle(ref);
106
+ const nodeHandle = findNodeHandle(localRef.current);
98
107
 
99
108
  if (!nodeHandle) {
100
109
  return;
@@ -88,11 +88,29 @@ const createStore = () =>
88
88
  }))
89
89
  );
90
90
 
91
+ const createScreenComponentsStore = () =>
92
+ create(subscribeWithSelector<Record<string, unknown>>((_) => ({})));
93
+
91
94
  type ScreenContextType = {
92
95
  _navBarStore: ReturnType<typeof createStore>;
93
96
  _stateStore: ReturnType<typeof createStateStore>;
94
97
  navBar: NavBarState;
95
98
  legacyFormatScreenData: LegacyNavigationScreenData | null;
99
+ /**
100
+ * Zustand store for component-level state within a screen.
101
+ *
102
+ * **Purpose:** Persists state across component mount/unmount cycles (e.g., during virtualization)
103
+ * and enables state sharing between components using the same key within the same screen.
104
+ *
105
+ * **Lifecycle:** Tied to the screen/route — recreated on each route change.
106
+ *
107
+ * @example
108
+ * // Used by useComponentScreenState hook:
109
+ * const store = useScreenContextV2()._componentStateStore;
110
+ * store.setState({ 'my-key': value });
111
+ * const value = store.getState()['my-key'];
112
+ */
113
+ _componentStateStore: ReturnType<typeof createScreenComponentsStore>;
96
114
  };
97
115
 
98
116
  export const ScreenContext = createContext<ScreenContextType>({
@@ -107,6 +125,7 @@ export const ScreenContext = createContext<ScreenContextType>({
107
125
  setSummary: (_subtitle) => void 0,
108
126
  },
109
127
  legacyFormatScreenData: {} as LegacyNavigationScreenData,
128
+ _componentStateStore: createScreenComponentsStore(),
110
129
  });
111
130
 
112
131
  export function ScreenContextProvider({
@@ -160,6 +179,14 @@ export function ScreenContextProvider({
160
179
  return navBarState;
161
180
  }, []);
162
181
 
182
+ // Component state store - recreated when pathname changes (route change).
183
+ // Unlike _navBarStore and _stateStore (cached via refs), this store
184
+ // resets only when pathname changes to provide a clean state for the new route.
185
+ const componentStateStore = useMemo(
186
+ () => createScreenComponentsStore(),
187
+ [pathname]
188
+ );
189
+
163
190
  const screenNavBarState = getScreenNavBarState()(
164
191
  useShallow((state) => ({
165
192
  visible: state.visible,
@@ -212,8 +239,9 @@ export function ScreenContextProvider({
212
239
  _stateStore: getScreenState(),
213
240
  navBar: navBarState,
214
241
  legacyFormatScreenData: routeScreenData,
242
+ _componentStateStore: componentStateStore,
215
243
  }),
216
- [navBarState, screenData, routeScreenData]
244
+ [navBarState, screenData, routeScreenData, componentStateStore]
217
245
  )}
218
246
  >
219
247
  {children}