@sigmela/router 0.3.5 → 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,8 +3,7 @@
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
- import { Platform, Pressable, StyleSheet, View, Text } from 'react-native';
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';
10
9
  const EMPTY_HISTORY = [];
@@ -21,9 +20,8 @@ const DrawerStackRenderer = /*#__PURE__*/memo(({
21
20
  const router = useRouter();
22
21
  const stackId = stack.getId();
23
22
 
24
- // On Android Fabric, start with empty history so the parent Screen's
25
- // Fragment can attach before child ScreenStackItems mount.
26
- const [history, setHistory] = useState(Platform.OS === 'android' ? EMPTY_HISTORY : () => router.getStackHistory(stackId));
23
+ // Start with empty history useEffect populates via startTransition.
24
+ const [history, setHistory] = useState(EMPTY_HISTORY);
27
25
  useEffect(() => {
28
26
  const update = () => {
29
27
  startTransition(() => {
@@ -137,69 +135,61 @@ export const RenderDrawer = /*#__PURE__*/memo(({
137
135
  }
138
136
  }, [tabs, index]);
139
137
  const CustomDrawer = config.component;
140
- return /*#__PURE__*/_jsx(ScreenStackItem, {
141
- screenId: "root-drawer",
142
- headerConfig: {
143
- hidden: true
144
- },
145
- style: StyleSheet.absoluteFill,
146
- stackAnimation: "slide_from_right",
147
- children: /*#__PURE__*/_jsx(DrawerContext.Provider, {
148
- value: drawer,
149
- children: /*#__PURE__*/_jsxs(View, {
150
- style: styles.container,
151
- children: [/*#__PURE__*/_jsx(Animated.View, {
152
- style: [styles.sidebar, {
153
- width: drawerWidth
154
- }, drawerStyle],
155
- children: CustomDrawer ? /*#__PURE__*/_jsx(CustomDrawer, {
156
- onItemPress: onItemPress,
157
- activeIndex: index,
158
- items: tabs,
159
- isOpen: isOpen,
160
- onClose: handleOverlayPress
161
- }) : /*#__PURE__*/_jsx(View, {
162
- style: [styles.sidebarContent, {
163
- paddingTop: insets.top + 12
164
- }],
165
- children: tabs.map((tab, i) => {
166
- const isActive = i === index;
167
- return /*#__PURE__*/_jsx(Pressable, {
168
- onPress: () => onItemPress(i),
169
- style: [styles.item, isActive && styles.itemActive],
170
- children: /*#__PURE__*/_jsx(Text, {
171
- style: [styles.itemText, isActive && styles.itemTextActive],
172
- children: tab.title
173
- })
174
- }, tab.tabKey);
175
- })
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);
176
166
  })
177
- }), /*#__PURE__*/_jsx(Animated.View, {
178
- style: [styles.overlay, overlayStyle],
179
- children: /*#__PURE__*/_jsx(Pressable, {
180
- style: StyleSheet.absoluteFill,
181
- onPress: handleOverlayPress
182
- })
183
- }), /*#__PURE__*/_jsx(Animated.View, {
184
- style: [styles.main, mainStyle],
185
- children: tabs.filter(t => visited[t.tabKey]).map(tab => {
186
- const isActive = tab.tabKey === tabs[index]?.tabKey;
187
- const stackForTab = drawer.stacks[tab.tabKey];
188
- const nodeForTab = drawer.nodes[tab.tabKey];
189
- const ScreenForTab = drawer.screens[tab.tabKey];
190
- return /*#__PURE__*/_jsx(View, {
191
- style: [styles.flex, !isActive && styles.hidden],
192
- children: stackForTab ? /*#__PURE__*/_jsx(DrawerStackRenderer, {
193
- appearance: appearance,
194
- stack: stackForTab
195
- }) : nodeForTab ? /*#__PURE__*/_jsx(DrawerNodeRenderer, {
196
- appearance: appearance,
197
- node: nodeForTab
198
- }) : ScreenForTab ? /*#__PURE__*/_jsx(ScreenForTab, {}) : null
199
- }, `drawer-content-${tab.tabKey}`);
200
- })
201
- })]
202
- })
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
+ })]
203
193
  })
204
194
  });
205
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(() => {
@@ -5,7 +5,7 @@ import { ScreenStack } from "../ScreenStack/index.js";
5
5
  import { SplitViewContext } from "./SplitViewContext.js";
6
6
  import { useRouter } from "../RouterContext.js";
7
7
  import { memo, useMemo, useRef, useState, useEffect, startTransition } from 'react';
8
- import { Platform, StyleSheet } from 'react-native';
8
+ import { StyleSheet } from 'react-native';
9
9
  import { jsx as _jsx } from "react/jsx-runtime";
10
10
  const EMPTY_HISTORY = [];
11
11
  /**
@@ -15,21 +15,9 @@ const EMPTY_HISTORY = [];
15
15
  * The combined history is: [...primaryHistory, ...secondaryHistory]
16
16
  * This way, navigating from primary to secondary is a native push.
17
17
  *
18
- * On Android (Fabric) react-native-screens manages each ScreenStackItem via
19
- * an Android Fragment. Fragment lifecycle (onCreateView) is asynchronous,
20
- * but Fabric's layout pass is synchronous. If a new commit is dispatched
21
- * while the previous Fragment transaction is still in flight, Fabric tries
22
- * to insert child views into a Screen whose Fragment isn't attached yet:
23
- *
24
- * "addViewAt: Parent Screen does not have its Fragment attached"
25
- *
26
- * The fix follows the same pattern used in react-native-screens' own
27
- * BottomTabsContainer example: subscribe via useEffect and apply updates
28
- * inside `startTransition`. This marks the update as non-urgent, letting
29
- * React defer the Fabric commit until pending Fragment transactions complete.
30
- *
31
- * See: https://github.com/software-mansion/react-native-screens/blob/ddd1a9e/
32
- * 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.
33
21
  */
34
22
  export const RenderSplitView = /*#__PURE__*/memo(({
35
23
  splitView,
@@ -39,13 +27,10 @@ export const RenderSplitView = /*#__PURE__*/memo(({
39
27
  const primaryId = splitView.primary.getId();
40
28
  const secondaryId = splitView.secondary.getId();
41
29
 
42
- // On Android Fabric, start with empty history so the parent Screen's
43
- // Fragment can attach before child ScreenStackItems mount. The useEffect
44
- // below populates via startTransition on the next commit.
45
- const isAndroid = Platform.OS === 'android';
46
- const hydratedRef = useRef(!isAndroid);
47
- const [primaryHistory, setPrimaryHistory] = useState(isAndroid ? EMPTY_HISTORY : () => router.getStackHistory(primaryId));
48
- const [secondaryHistory, setSecondaryHistory] = useState(isAndroid ? EMPTY_HISTORY : () => 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);
49
34
  useEffect(() => {
50
35
  hydratedRef.current = true;
51
36
  const updatePrimary = () => {
@@ -71,7 +56,7 @@ export const RenderSplitView = /*#__PURE__*/memo(({
71
56
  }, [router, primaryId, secondaryId]);
72
57
 
73
58
  // Fallback: if primary is empty, seed with first route.
74
- // On Android, skip the seed during the initial empty mount (before useEffect
59
+ // Skip the seed during the initial empty mount (before useEffect
75
60
  // hydrates history) so no ScreenStackItems mount before the parent Fragment attaches.
76
61
  const primaryHistoryToRender = useMemo(() => {
77
62
  if (primaryHistory.length > 0) {
@@ -138,9 +138,9 @@ const TabStackRenderer = /*#__PURE__*/memo(({
138
138
  const router = useRouter();
139
139
  const stackId = stack.getId();
140
140
 
141
- // On Android Fabric, start with empty history so the parent Screen's
142
- // Fragment can attach before child ScreenStackItems mount.
143
- const [history, setHistory] = useState(Platform.OS === 'android' ? EMPTY_HISTORY : () => router.getStackHistory(stackId));
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);
144
144
  useEffect(() => {
145
145
  const update = () => {
146
146
  startTransition(() => {
@@ -149,7 +149,7 @@ const TabStackRenderer = /*#__PURE__*/memo(({
149
149
  };
150
150
  const unsub = router.subscribeStack(stackId, update);
151
151
  update();
152
- return () => unsub();
152
+ return unsub;
153
153
  }, [router, stackId]);
154
154
  return /*#__PURE__*/_jsx(StackRenderer, {
155
155
  appearance: appearance,
@@ -334,9 +334,11 @@ export const RenderTabBar = /*#__PURE__*/memo(({
334
334
  useEffect(() => {
335
335
  const key = tabs[index]?.tabKey;
336
336
  if (key) {
337
- setVisited(prev => prev[key] ? prev : {
338
- ...prev,
339
- [key]: true
337
+ startTransition(() => {
338
+ setVisited(prev => prev[key] ? prev : {
339
+ ...prev,
340
+ [key]: true
341
+ });
340
342
  });
341
343
  }
342
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.5",
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",