@sigmela/router 0.3.3 → 0.3.5

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.
@@ -4,9 +4,10 @@ import { StackRenderer } from "../StackRenderer.js";
4
4
  import { DrawerContext } from "./DrawerContext.js";
5
5
  import { useRouter } from "../RouterContext.js";
6
6
  import { ScreenStackItem } from 'react-native-screens';
7
- import { Pressable, StyleSheet, View, Text } from 'react-native';
7
+ import { Platform, Pressable, StyleSheet, View, Text } from 'react-native';
8
8
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
9
9
  import { useCallback, useSyncExternalStore, memo, useEffect, useState, useMemo, startTransition } from 'react';
10
+ const EMPTY_HISTORY = [];
10
11
  import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
11
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
13
  const TIMING_CONFIG = {
@@ -19,7 +20,10 @@ const DrawerStackRenderer = /*#__PURE__*/memo(({
19
20
  }) => {
20
21
  const router = useRouter();
21
22
  const stackId = stack.getId();
22
- const [history, setHistory] = useState(() => router.getStackHistory(stackId));
23
+
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
27
  useEffect(() => {
24
28
  const update = () => {
25
29
  startTransition(() => {
@@ -124,9 +128,11 @@ export const RenderDrawer = /*#__PURE__*/memo(({
124
128
  useEffect(() => {
125
129
  const key = tabs[index]?.tabKey;
126
130
  if (key) {
127
- setVisited(prev => prev[key] ? prev : {
128
- ...prev,
129
- [key]: true
131
+ startTransition(() => {
132
+ setVisited(prev => prev[key] ? prev : {
133
+ ...prev,
134
+ [key]: true
135
+ });
130
136
  });
131
137
  }
132
138
  }, [tabs, index]);
@@ -11,7 +11,9 @@ function useStackHistory(router, stackId) {
11
11
  const [history, setHistory] = useState(() => stackId ? router.getStackHistory(stackId) : EMPTY_HISTORY);
12
12
  useEffect(() => {
13
13
  if (!stackId) {
14
- setHistory(EMPTY_HISTORY);
14
+ startTransition(() => {
15
+ setHistory(EMPTY_HISTORY);
16
+ });
15
17
  return;
16
18
  }
17
19
  const update = () => {
@@ -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';
8
- import { StyleSheet } from 'react-native';
7
+ import { memo, useMemo, useRef, useState, useEffect, startTransition } from 'react';
8
+ import { Platform, 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.
@@ -38,12 +39,15 @@ export const RenderSplitView = /*#__PURE__*/memo(({
38
39
  const primaryId = splitView.primary.getId();
39
40
  const secondaryId = splitView.secondary.getId();
40
41
 
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));
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));
46
49
  useEffect(() => {
50
+ hydratedRef.current = true;
47
51
  const updatePrimary = () => {
48
52
  startTransition(() => {
49
53
  setPrimaryHistory(router.getStackHistory(primaryId));
@@ -66,11 +70,14 @@ export const RenderSplitView = /*#__PURE__*/memo(({
66
70
  };
67
71
  }, [router, primaryId, secondaryId]);
68
72
 
69
- // Fallback: if primary is empty, seed with first route
73
+ // Fallback: if primary is empty, seed with first route.
74
+ // On Android, skip the seed during the initial empty mount (before useEffect
75
+ // hydrates history) so no ScreenStackItems mount before the parent Fragment attaches.
70
76
  const primaryHistoryToRender = useMemo(() => {
71
77
  if (primaryHistory.length > 0) {
72
78
  return primaryHistory;
73
79
  }
80
+ if (!hydratedRef.current) return [];
74
81
  const first = splitView.primary.getFirstRoute();
75
82
  if (!first) return [];
76
83
  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
+ // 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));
140
144
  useEffect(() => {
141
145
  const update = () => {
142
146
  startTransition(() => {
@@ -176,6 +180,20 @@ export const RenderTabBar = /*#__PURE__*/memo(({
176
180
  index,
177
181
  config
178
182
  } = snapshot;
183
+
184
+ // Local focus state driven by startTransition so that isFocused changes
185
+ // are deferred until the native Fragment lifecycle completes (Android Fabric).
186
+ const [focusedTabKey, setFocusedTabKey] = useState(() => tabs[index]?.tabKey);
187
+
188
+ // Sync focusedTabKey when index changes from external sources (programmatic navigation).
189
+ useEffect(() => {
190
+ const key = tabs[index]?.tabKey;
191
+ if (key && key !== focusedTabKey) {
192
+ startTransition(() => {
193
+ setFocusedTabKey(key);
194
+ });
195
+ }
196
+ }, [tabs, index, focusedTabKey]);
179
197
  const {
180
198
  iconColor,
181
199
  iconColorActive,
@@ -199,7 +217,10 @@ export const RenderTabBar = /*#__PURE__*/memo(({
199
217
  const tabIndex = tabs.findIndex(route => route.tabKey === tabKey);
200
218
  if (tabIndex === -1) return;
201
219
 
202
- // Keep native Tabs.Host focus state as the source of truth.
220
+ // Defer isFocused update so the native Fragment has time to attach.
221
+ startTransition(() => {
222
+ setFocusedTabKey(tabKey);
223
+ });
203
224
  if (tabIndex !== index) {
204
225
  tabBar.onIndexChange(tabIndex);
205
226
  }
@@ -355,7 +376,7 @@ export const RenderTabBar = /*#__PURE__*/memo(({
355
376
  experimentalControlNavigationStateInJS: config.experimentalControlNavigationStateInJS,
356
377
  ...containerProps,
357
378
  children: tabs.map((tab, i) => {
358
- const isFocused = tab.tabKey === tabs[index]?.tabKey;
379
+ const isFocused = tab.tabKey === focusedTabKey;
359
380
  const stack = tabBar.stacks[tab.tabKey];
360
381
  const node = tabBar.nodes[tab.tabKey];
361
382
  const Screen = tabBar.screens[tab.tabKey];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigmela/router",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "React Native Router",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",