@sigmela/router 0.3.4 → 0.3.6

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.
@@ -3,10 +3,10 @@
3
3
  import { StackRenderer } from "../StackRenderer.js";
4
4
  import { DrawerContext } from "./DrawerContext.js";
5
5
  import { useRouter } from "../RouterContext.js";
6
- import { ScreenStackItem } from 'react-native-screens';
7
6
  import { Pressable, StyleSheet, View, Text } from 'react-native';
8
7
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
9
8
  import { useCallback, useSyncExternalStore, memo, useEffect, useState, useMemo, startTransition } from 'react';
9
+ const EMPTY_HISTORY = [];
10
10
  import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
11
11
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  const TIMING_CONFIG = {
@@ -19,7 +19,9 @@ const DrawerStackRenderer = /*#__PURE__*/memo(({
19
19
  }) => {
20
20
  const router = useRouter();
21
21
  const stackId = stack.getId();
22
- const [history, setHistory] = useState(() => router.getStackHistory(stackId));
22
+
23
+ // Start with empty history — useEffect populates via startTransition.
24
+ const [history, setHistory] = useState(EMPTY_HISTORY);
23
25
  useEffect(() => {
24
26
  const update = () => {
25
27
  startTransition(() => {
@@ -133,69 +135,61 @@ export const RenderDrawer = /*#__PURE__*/memo(({
133
135
  }
134
136
  }, [tabs, index]);
135
137
  const CustomDrawer = config.component;
136
- return /*#__PURE__*/_jsx(ScreenStackItem, {
137
- screenId: "root-drawer",
138
- headerConfig: {
139
- hidden: true
140
- },
141
- style: StyleSheet.absoluteFill,
142
- stackAnimation: "slide_from_right",
143
- children: /*#__PURE__*/_jsx(DrawerContext.Provider, {
144
- value: drawer,
145
- children: /*#__PURE__*/_jsxs(View, {
146
- style: styles.container,
147
- children: [/*#__PURE__*/_jsx(Animated.View, {
148
- style: [styles.sidebar, {
149
- width: drawerWidth
150
- }, drawerStyle],
151
- children: CustomDrawer ? /*#__PURE__*/_jsx(CustomDrawer, {
152
- onItemPress: onItemPress,
153
- activeIndex: index,
154
- items: tabs,
155
- isOpen: isOpen,
156
- onClose: handleOverlayPress
157
- }) : /*#__PURE__*/_jsx(View, {
158
- style: [styles.sidebarContent, {
159
- paddingTop: insets.top + 12
160
- }],
161
- children: tabs.map((tab, i) => {
162
- const isActive = i === index;
163
- return /*#__PURE__*/_jsx(Pressable, {
164
- onPress: () => onItemPress(i),
165
- style: [styles.item, isActive && styles.itemActive],
166
- children: /*#__PURE__*/_jsx(Text, {
167
- style: [styles.itemText, isActive && styles.itemTextActive],
168
- children: tab.title
169
- })
170
- }, tab.tabKey);
171
- })
172
- })
173
- }), /*#__PURE__*/_jsx(Animated.View, {
174
- style: [styles.overlay, overlayStyle],
175
- children: /*#__PURE__*/_jsx(Pressable, {
176
- style: StyleSheet.absoluteFill,
177
- onPress: handleOverlayPress
178
- })
179
- }), /*#__PURE__*/_jsx(Animated.View, {
180
- style: [styles.main, mainStyle],
181
- children: tabs.filter(t => visited[t.tabKey]).map(tab => {
182
- const isActive = tab.tabKey === tabs[index]?.tabKey;
183
- const stackForTab = drawer.stacks[tab.tabKey];
184
- const nodeForTab = drawer.nodes[tab.tabKey];
185
- const ScreenForTab = drawer.screens[tab.tabKey];
186
- return /*#__PURE__*/_jsx(View, {
187
- style: [styles.flex, !isActive && styles.hidden],
188
- children: stackForTab ? /*#__PURE__*/_jsx(DrawerStackRenderer, {
189
- appearance: appearance,
190
- stack: stackForTab
191
- }) : nodeForTab ? /*#__PURE__*/_jsx(DrawerNodeRenderer, {
192
- appearance: appearance,
193
- node: nodeForTab
194
- }) : ScreenForTab ? /*#__PURE__*/_jsx(ScreenForTab, {}) : null
195
- }, `drawer-content-${tab.tabKey}`);
138
+ return /*#__PURE__*/_jsx(DrawerContext.Provider, {
139
+ value: drawer,
140
+ children: /*#__PURE__*/_jsxs(View, {
141
+ style: styles.container,
142
+ children: [/*#__PURE__*/_jsx(Animated.View, {
143
+ style: [styles.sidebar, {
144
+ width: drawerWidth
145
+ }, drawerStyle],
146
+ children: CustomDrawer ? /*#__PURE__*/_jsx(CustomDrawer, {
147
+ onItemPress: onItemPress,
148
+ activeIndex: index,
149
+ items: tabs,
150
+ isOpen: isOpen,
151
+ onClose: handleOverlayPress
152
+ }) : /*#__PURE__*/_jsx(View, {
153
+ style: [styles.sidebarContent, {
154
+ paddingTop: insets.top + 12
155
+ }],
156
+ children: tabs.map((tab, i) => {
157
+ const isActive = i === index;
158
+ return /*#__PURE__*/_jsx(Pressable, {
159
+ onPress: () => onItemPress(i),
160
+ style: [styles.item, isActive && styles.itemActive],
161
+ children: /*#__PURE__*/_jsx(Text, {
162
+ style: [styles.itemText, isActive && styles.itemTextActive],
163
+ children: tab.title
164
+ })
165
+ }, tab.tabKey);
196
166
  })
197
- })]
198
- })
167
+ })
168
+ }), /*#__PURE__*/_jsx(Animated.View, {
169
+ style: [styles.overlay, overlayStyle],
170
+ children: /*#__PURE__*/_jsx(Pressable, {
171
+ style: StyleSheet.absoluteFill,
172
+ onPress: handleOverlayPress
173
+ })
174
+ }), /*#__PURE__*/_jsx(Animated.View, {
175
+ style: [styles.main, mainStyle],
176
+ children: tabs.filter(t => visited[t.tabKey]).map(tab => {
177
+ const isActive = tab.tabKey === tabs[index]?.tabKey;
178
+ const stackForTab = drawer.stacks[tab.tabKey];
179
+ const nodeForTab = drawer.nodes[tab.tabKey];
180
+ const ScreenForTab = drawer.screens[tab.tabKey];
181
+ return /*#__PURE__*/_jsx(View, {
182
+ style: [styles.flex, !isActive && styles.hidden],
183
+ children: stackForTab ? /*#__PURE__*/_jsx(DrawerStackRenderer, {
184
+ appearance: appearance,
185
+ stack: stackForTab
186
+ }) : nodeForTab ? /*#__PURE__*/_jsx(DrawerNodeRenderer, {
187
+ appearance: appearance,
188
+ node: nodeForTab
189
+ }) : ScreenForTab ? /*#__PURE__*/_jsx(ScreenForTab, {}) : null
190
+ }, `drawer-content-${tab.tabKey}`);
191
+ })
192
+ })]
199
193
  })
200
194
  });
201
195
  });
@@ -8,7 +8,7 @@ import { memo, useEffect, useRef, useState, startTransition } from 'react';
8
8
  import { jsx as _jsx } from "react/jsx-runtime";
9
9
  const EMPTY_HISTORY = [];
10
10
  function useStackHistory(router, stackId) {
11
- const [history, setHistory] = useState(() => stackId ? router.getStackHistory(stackId) : EMPTY_HISTORY);
11
+ const [history, setHistory] = useState(EMPTY_HISTORY);
12
12
  useEffect(() => {
13
13
  if (!stackId) {
14
14
  startTransition(() => {
@@ -4,9 +4,10 @@ import { ScreenStackItem } from "../ScreenStackItem/index.js";
4
4
  import { ScreenStack } from "../ScreenStack/index.js";
5
5
  import { SplitViewContext } from "./SplitViewContext.js";
6
6
  import { useRouter } from "../RouterContext.js";
7
- import { memo, useMemo, useState, useEffect, startTransition } from 'react';
7
+ import { memo, useMemo, useRef, useState, useEffect, startTransition } from 'react';
8
8
  import { StyleSheet } from 'react-native';
9
9
  import { jsx as _jsx } from "react/jsx-runtime";
10
+ const EMPTY_HISTORY = [];
10
11
  /**
11
12
  * On native, SplitView renders primary and secondary screens in a SINGLE
12
13
  * ScreenStack to get native push/pop animations.
@@ -14,21 +15,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
14
15
  * The combined history is: [...primaryHistory, ...secondaryHistory]
15
16
  * This way, navigating from primary to secondary is a native push.
16
17
  *
17
- * On Android (Fabric) react-native-screens manages each ScreenStackItem via
18
- * an Android Fragment. Fragment lifecycle (onCreateView) is asynchronous,
19
- * but Fabric's layout pass is synchronous. If a new commit is dispatched
20
- * while the previous Fragment transaction is still in flight, Fabric tries
21
- * to insert child views into a Screen whose Fragment isn't attached yet:
22
- *
23
- * "addViewAt: Parent Screen does not have its Fragment attached"
24
- *
25
- * The fix follows the same pattern used in react-native-screens' own
26
- * BottomTabsContainer example: subscribe via useEffect and apply updates
27
- * inside `startTransition`. This marks the update as non-urgent, letting
28
- * React defer the Fabric commit until pending Fragment transactions complete.
29
- *
30
- * See: https://github.com/software-mansion/react-native-screens/blob/ddd1a9e/
31
- * apps/src/shared/gamma/containers/bottom-tabs/BottomTabsContainer.tsx
18
+ * History starts empty and is populated via useEffect + startTransition,
19
+ * following the pattern from react-native-screens' BottomTabsContainer.
20
+ * This lets the parent Screen attach before child ScreenStackItems mount.
32
21
  */
33
22
  export const RenderSplitView = /*#__PURE__*/memo(({
34
23
  splitView,
@@ -38,12 +27,12 @@ export const RenderSplitView = /*#__PURE__*/memo(({
38
27
  const primaryId = splitView.primary.getId();
39
28
  const secondaryId = splitView.secondary.getId();
40
29
 
41
- // Subscribe to both stacks via useEffect + startTransition.
42
- // startTransition defers the Fabric commit so the FragmentManager has
43
- // time to finish pending transactions between commits.
44
- const [primaryHistory, setPrimaryHistory] = useState(() => router.getStackHistory(primaryId));
45
- const [secondaryHistory, setSecondaryHistory] = useState(() => router.getStackHistory(secondaryId));
30
+ // Start with empty history useEffect populates via startTransition.
31
+ const hydratedRef = useRef(false);
32
+ const [primaryHistory, setPrimaryHistory] = useState(EMPTY_HISTORY);
33
+ const [secondaryHistory, setSecondaryHistory] = useState(EMPTY_HISTORY);
46
34
  useEffect(() => {
35
+ hydratedRef.current = true;
47
36
  const updatePrimary = () => {
48
37
  startTransition(() => {
49
38
  setPrimaryHistory(router.getStackHistory(primaryId));
@@ -66,11 +55,14 @@ export const RenderSplitView = /*#__PURE__*/memo(({
66
55
  };
67
56
  }, [router, primaryId, secondaryId]);
68
57
 
69
- // Fallback: if primary is empty, seed with first route
58
+ // Fallback: if primary is empty, seed with first route.
59
+ // Skip the seed during the initial empty mount (before useEffect
60
+ // hydrates history) so no ScreenStackItems mount before the parent Fragment attaches.
70
61
  const primaryHistoryToRender = useMemo(() => {
71
62
  if (primaryHistory.length > 0) {
72
63
  return primaryHistory;
73
64
  }
65
+ if (!hydratedRef.current) return [];
74
66
  const first = splitView.primary.getFirstRoute();
75
67
  if (!first) return [];
76
68
  const activePath = router.getActiveRoute()?.path;
@@ -7,6 +7,7 @@ import { Tabs } from 'react-native-screens';
7
7
  import { Platform, StyleSheet, View } from 'react-native';
8
8
  import { useCallback, useSyncExternalStore, memo, useEffect, useState, useMemo, startTransition } from 'react';
9
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
+ const EMPTY_HISTORY = [];
10
11
  const isImageSource = value => {
11
12
  if (value == null) return false;
12
13
  const valueType = typeof value;
@@ -136,7 +137,10 @@ const TabStackRenderer = /*#__PURE__*/memo(({
136
137
  }) => {
137
138
  const router = useRouter();
138
139
  const stackId = stack.getId();
139
- const [history, setHistory] = useState(() => router.getStackHistory(stackId));
140
+
141
+ // Start with empty history — useEffect populates via startTransition.
142
+ // This avoids mounting ScreenStackItems before parent Fragments attach.
143
+ const [history, setHistory] = useState(EMPTY_HISTORY);
140
144
  useEffect(() => {
141
145
  const update = () => {
142
146
  startTransition(() => {
@@ -145,7 +149,7 @@ const TabStackRenderer = /*#__PURE__*/memo(({
145
149
  };
146
150
  const unsub = router.subscribeStack(stackId, update);
147
151
  update();
148
- return () => unsub();
152
+ return unsub;
149
153
  }, [router, stackId]);
150
154
  return /*#__PURE__*/_jsx(StackRenderer, {
151
155
  appearance: appearance,
@@ -330,9 +334,11 @@ export const RenderTabBar = /*#__PURE__*/memo(({
330
334
  useEffect(() => {
331
335
  const key = tabs[index]?.tabKey;
332
336
  if (key) {
333
- setVisited(prev => prev[key] ? prev : {
334
- ...prev,
335
- [key]: true
337
+ startTransition(() => {
338
+ setVisited(prev => prev[key] ? prev : {
339
+ ...prev,
340
+ [key]: true
341
+ });
336
342
  });
337
343
  }
338
344
  }, [tabs, index]);
@@ -11,21 +11,9 @@ export interface RenderSplitViewProps {
11
11
  * The combined history is: [...primaryHistory, ...secondaryHistory]
12
12
  * This way, navigating from primary to secondary is a native push.
13
13
  *
14
- * On Android (Fabric) react-native-screens manages each ScreenStackItem via
15
- * an Android Fragment. Fragment lifecycle (onCreateView) is asynchronous,
16
- * but Fabric's layout pass is synchronous. If a new commit is dispatched
17
- * while the previous Fragment transaction is still in flight, Fabric tries
18
- * to insert child views into a Screen whose Fragment isn't attached yet:
19
- *
20
- * "addViewAt: Parent Screen does not have its Fragment attached"
21
- *
22
- * The fix follows the same pattern used in react-native-screens' own
23
- * BottomTabsContainer example: subscribe via useEffect and apply updates
24
- * inside `startTransition`. This marks the update as non-urgent, letting
25
- * React defer the Fabric commit until pending Fragment transactions complete.
26
- *
27
- * See: https://github.com/software-mansion/react-native-screens/blob/ddd1a9e/
28
- * apps/src/shared/gamma/containers/bottom-tabs/BottomTabsContainer.tsx
14
+ * History starts empty and is populated via useEffect + startTransition,
15
+ * following the pattern from react-native-screens' BottomTabsContainer.
16
+ * This lets the parent Screen attach before child ScreenStackItems mount.
29
17
  */
30
18
  export declare const RenderSplitView: import("react").NamedExoticComponent<RenderSplitViewProps>;
31
19
  //# sourceMappingURL=RenderSplitView.native.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigmela/router",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "React Native Router",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",