@ionic/react-router 9.0.7-dev.11790887756.13f9320e → 9.0.7-dev.11791381676.118a7a15

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/dist/index.js CHANGED
@@ -8,6 +8,34 @@ const IonRouteInner = ({ path, index, caseSensitive, element }) => {
8
8
  return React.createElement(Route, { path: path, index: index, caseSensitive: caseSensitive, element: element });
9
9
  };
10
10
 
11
+ /**
12
+ * Compares two route param objects by value, treating a missing object as empty.
13
+ *
14
+ * @param a The first params object.
15
+ * @param b The second params object.
16
+ * @returns True if both have the same keys with equal values.
17
+ */
18
+ const areParamsEqual = (a, b) => {
19
+ const paramsA = a || {};
20
+ const paramsB = b || {};
21
+ const keysA = Object.keys(paramsA);
22
+ const keysB = Object.keys(paramsB);
23
+ if (keysA.length !== keysB.length) {
24
+ return false;
25
+ }
26
+ return keysA.every((key) => {
27
+ const valueA = paramsA[key];
28
+ const valueB = paramsB[key];
29
+ if (Array.isArray(valueA) && Array.isArray(valueB)) {
30
+ if (valueA.length !== valueB.length) {
31
+ return false;
32
+ }
33
+ return valueA.every((entry, idx) => entry === valueB[idx]);
34
+ }
35
+ return valueA === valueB;
36
+ });
37
+ };
38
+
11
39
  /**
12
40
  * The matchPath function is used only for matching paths, not rendering components or elements.
13
41
  * @see https://reactrouter.com/v6/utils/match-path
@@ -694,6 +722,42 @@ const resolveIndexRouteMatch = (viewItem, pathname, parentPath) => {
694
722
  const normalizedBase = normalizePathnameForComparison(previousMatch.pathnameBase || previousMatch.pathname || '');
695
723
  return normalizedPathname === normalizedBase ? previousMatch : null;
696
724
  };
725
+ const shallowEqual = (a, b) => {
726
+ const aRecord = a;
727
+ const bRecord = b;
728
+ const aKeys = Object.keys(aRecord);
729
+ return aKeys.length === Object.keys(bRecord).length && aKeys.every((key) => aRecord[key] === bRecord[key]);
730
+ };
731
+ const isSameMatch = (a, b) => a === b ||
732
+ (a.pathname === b.pathname &&
733
+ a.pathnameBase === b.pathnameBase &&
734
+ areParamsEqual(a.params, b.params) &&
735
+ (a.route === b.route || shallowEqual(a.route, b.route)));
736
+ /**
737
+ * The outlet rebuilds a view's RouteContext on every render, and it renders
738
+ * several times per navigation, so this compares by content to tell whether
739
+ * the view's route state actually changed.
740
+ */
741
+ const isSameRouteContext = (a, b) => {
742
+ const { matches: aMatches, ...aRest } = a;
743
+ const { matches: bMatches, ...bRest } = b;
744
+ return (shallowEqual(aRest, bRest) &&
745
+ aMatches.length === bMatches.length &&
746
+ aMatches.every((match, i) => isSameMatch(match, bMatches[i])));
747
+ };
748
+ /**
749
+ * Provides a view's RouteContext, reusing the previous value while its content
750
+ * is unchanged or while `hold` is set (the view is not the one being shown), so
751
+ * RouteContext-only readers like useParams skip re-rendering. Kept in component
752
+ * state since two outlets with the same id share view items.
753
+ */
754
+ const ViewRouteContextProvider = ({ value, hold, children }) => {
755
+ const [heldValue, setHeldValue] = React.useState(value);
756
+ if (!hold && heldValue !== value && !isSameRouteContext(heldValue, value)) {
757
+ setHeldValue(value);
758
+ }
759
+ return React.createElement(UNSAFE_RouteContext.Provider, { value: heldValue }, children);
760
+ };
697
761
  class ReactRouterViewStack extends ViewStacks {
698
762
  /**
699
763
  * Stores the computed parent path for each outlet.
@@ -894,6 +958,8 @@ class ReactRouterViewStack extends ViewStacks {
894
958
  viewItem.mount = true;
895
959
  viewItem.routeData.match = match;
896
960
  }
961
+ // Set when a more specific sibling owns the pathname, so this view is hidden even though it matches.
962
+ let hiddenBySpecificMatch = false;
897
963
  // Deactivate wildcard (catch-all) and empty-path (default) routes when a more-specific route matches.
898
964
  // This prevents "Not found" or fallback pages from showing alongside valid routes.
899
965
  if (routePath === '*' || routePath === '/*' || routePath === '') {
@@ -950,6 +1016,7 @@ class ReactRouterViewStack extends ViewStacks {
950
1016
  }
951
1017
  }
952
1018
  if (hasSpecificMatch && !isSwipeRevealed(viewItem)) {
1019
+ hiddenBySpecificMatch = true;
953
1020
  // A splat can be the outlet's container page rather than a "not found" fallback.
954
1021
  // Pushed over it is the page underneath, so unmounting it destroys its state and
955
1022
  // leaves nothing for back to reveal. Hiding it below covers that. A view with no
@@ -972,6 +1039,11 @@ class ReactRouterViewStack extends ViewStacks {
972
1039
  viewItem.routeData.match = match;
973
1040
  }
974
1041
  const routeMatch = shouldSkipForDifferentParam ? viewItem.routeData?.match : match || viewItem.routeData?.match;
1042
+ // Hold the RouteContext while the view isn't the one shown: a sibling owns the pathname, it's
1043
+ // parked on another param instance, or it doesn't match. A pathless default only strictly
1044
+ // matches at "/", so a missing match there doesn't mean it's hidden.
1045
+ const isPathlessDefault = !routePath && !viewItem.routeData?.childProps?.index;
1046
+ const holdRouteContext = hiddenBySpecificMatch || !!shouldSkipForDifferentParam || (!match && !isPathlessDefault);
975
1047
  return (React.createElement(UNSAFE_RouteContext.Consumer, { key: `view-context-${viewItem.id}` }, (parentContext) => {
976
1048
  const parentMatches = (parentContext?.matches ?? []);
977
1049
  // Accumulate params from parent matches, with fallback to other outlets
@@ -987,7 +1059,7 @@ class ReactRouterViewStack extends ViewStacks {
987
1059
  ? { ...parentContext, matches: contextMatches }
988
1060
  : { outlet: null, matches: contextMatches, isDataRoute: false };
989
1061
  return (React.createElement(ViewLifeCycleManager, { key: `view-${viewItem.id}`, mount: viewItem.mount, removeView: () => this.remove(viewItem) },
990
- React.createElement(UNSAFE_RouteContext.Provider, { value: routeContextValue }, componentElement)));
1062
+ React.createElement(ViewRouteContextProvider, { value: routeContextValue, hold: holdRouteContext }, componentElement)));
991
1063
  }));
992
1064
  };
993
1065
  /**
@@ -3106,26 +3178,6 @@ const checkIsMultiStepBack = (prevInfo, destinationPathname, history) => {
3106
3178
  }
3107
3179
  return false;
3108
3180
  };
3109
- const areParamsEqual = (a, b) => {
3110
- const paramsA = a || {};
3111
- const paramsB = b || {};
3112
- const keysA = Object.keys(paramsA);
3113
- const keysB = Object.keys(paramsB);
3114
- if (keysA.length !== keysB.length) {
3115
- return false;
3116
- }
3117
- return keysA.every((key) => {
3118
- const valueA = paramsA[key];
3119
- const valueB = paramsB[key];
3120
- if (Array.isArray(valueA) && Array.isArray(valueB)) {
3121
- if (valueA.length !== valueB.length) {
3122
- return false;
3123
- }
3124
- return valueA.every((entry, idx) => entry === valueB[idx]);
3125
- }
3126
- return valueA === valueB;
3127
- });
3128
- };
3129
3181
  const IonRouter = ({ children, registerHistoryListener }) => {
3130
3182
  const location = useLocation();
3131
3183
  const navigate = useNavigate();