@tanstack/react-router 0.0.1-beta.69 → 0.0.1-beta.70

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.
@@ -59,6 +59,24 @@
59
59
  throw new Error(value);
60
60
  }
61
61
 
62
+ function warning(condition, message) {
63
+ {
64
+ if (condition) {
65
+ return;
66
+ }
67
+
68
+ var text = "Warning: " + message;
69
+
70
+ if (typeof console !== 'undefined') {
71
+ console.warn(text);
72
+ }
73
+
74
+ try {
75
+ throw Error(text);
76
+ } catch (x) {}
77
+ }
78
+ }
79
+
62
80
  /**
63
81
  * store
64
82
  *
@@ -261,15 +279,6 @@
261
279
  function last(arr) {
262
280
  return arr[arr.length - 1];
263
281
  }
264
- function warning(cond, message) {
265
- if (cond) {
266
- if (typeof console !== 'undefined') console.warn(message);
267
- try {
268
- throw new Error(message);
269
- } catch {}
270
- }
271
- return true;
272
- }
273
282
  function isFunction(d) {
274
283
  return typeof d === 'function';
275
284
  }
@@ -481,6 +490,9 @@
481
490
  const baseSegments = parsePathname(from);
482
491
  const to = `${matchLocation.to ?? '$'}`;
483
492
  const routeSegments = parsePathname(to);
493
+ if (last(baseSegments)?.value === '/') {
494
+ baseSegments.pop();
495
+ }
484
496
  const params = {};
485
497
  let isMatch = (() => {
486
498
  for (let i = 0; i < Math.max(baseSegments.length, routeSegments.length); i++) {
@@ -686,211 +698,6 @@
686
698
  // const config = rootRoute.addChildren([aRoute.addChildren([bRoute])])
687
699
  // // ^?
688
700
 
689
- const componentTypes = ['component', 'errorComponent', 'pendingComponent'];
690
- class RouteMatch {
691
- abortController = new AbortController();
692
- onLoaderDataListeners = new Set();
693
- constructor(router, route, opts) {
694
- Object.assign(this, {
695
- route,
696
- router,
697
- id: opts.id,
698
- pathname: opts.pathname,
699
- params: opts.params,
700
- store: new Store({
701
- updatedAt: 0,
702
- routeSearch: {},
703
- search: {},
704
- status: 'idle'
705
- }, {
706
- onUpdate: next => {
707
- this.state = next;
708
- }
709
- })
710
- });
711
- this.state = this.store.state;
712
- componentTypes.map(async type => {
713
- const component = this.route.options[type];
714
- if (typeof this[type] !== 'function') {
715
- this[type] = component;
716
- }
717
- });
718
- if (this.state.status === 'idle' && !this.#hasLoaders()) {
719
- this.store.setState(s => ({
720
- ...s,
721
- status: 'success'
722
- }));
723
- }
724
- }
725
- #hasLoaders = () => {
726
- return !!(this.route.options.onLoad || componentTypes.some(d => this.route.options[d]?.preload));
727
- };
728
- __commit = () => {
729
- const {
730
- routeSearch,
731
- search,
732
- context,
733
- routeContext
734
- } = this.#resolveInfo({
735
- location: this.router.state.currentLocation
736
- });
737
- this.context = context;
738
- this.routeContext = routeContext;
739
- this.store.setState(s => ({
740
- ...s,
741
- routeSearch: replaceEqualDeep(s.routeSearch, routeSearch),
742
- search: replaceEqualDeep(s.search, search)
743
- }));
744
- };
745
- cancel = () => {
746
- this.abortController?.abort();
747
- };
748
- #resolveSearchInfo = opts => {
749
- // Validate the search params and stabilize them
750
- const parentSearchInfo = this.parentMatch ? this.parentMatch.#resolveSearchInfo(opts) : {
751
- search: opts.location.search,
752
- routeSearch: opts.location.search
753
- };
754
- try {
755
- const validator = typeof this.route.options.validateSearch === 'object' ? this.route.options.validateSearch.parse : this.route.options.validateSearch;
756
- const routeSearch = validator?.(parentSearchInfo.search) ?? {};
757
- const search = {
758
- ...parentSearchInfo.search,
759
- ...routeSearch
760
- };
761
- return {
762
- routeSearch,
763
- search
764
- };
765
- } catch (err) {
766
- this.route.options.onValidateSearchError?.(err);
767
- const error = new Error('Invalid search params found', {
768
- cause: err
769
- });
770
- error.code = 'INVALID_SEARCH_PARAMS';
771
- throw error;
772
- }
773
- };
774
- #resolveInfo = opts => {
775
- const {
776
- search,
777
- routeSearch
778
- } = this.#resolveSearchInfo(opts);
779
- const routeContext = this.route.options.getContext?.({
780
- parentContext: this.parentMatch?.routeContext ?? {},
781
- context: this.parentMatch?.context ?? this.router?.options.context ?? {},
782
- params: this.params,
783
- search
784
- }) || {};
785
- const context = {
786
- ...(this.parentMatch?.context ?? this.router?.options.context),
787
- ...routeContext
788
- };
789
- return {
790
- routeSearch,
791
- search,
792
- context,
793
- routeContext
794
- };
795
- };
796
- __load = async opts => {
797
- this.parentMatch = opts.parentMatch;
798
- let info;
799
- try {
800
- info = this.#resolveInfo(opts);
801
- } catch (err) {
802
- this.route.options.onError?.(err);
803
- this.store.setState(s => ({
804
- ...s,
805
- status: 'error',
806
- error: err
807
- }));
808
-
809
- // Do not proceed with loading the route
810
- return;
811
- }
812
- const {
813
- routeSearch,
814
- search,
815
- context,
816
- routeContext
817
- } = info;
818
-
819
- // If the match is invalid, errored or idle, trigger it to load
820
- if (this.state.status === 'pending') {
821
- return;
822
- }
823
-
824
- // TODO: Should load promises be tracked based on location?
825
- this.__loadPromise = Promise.resolve().then(async () => {
826
- const loadId = '' + Date.now() + Math.random();
827
- this.#latestId = loadId;
828
- const checkLatest = () => {
829
- return loadId !== this.#latestId ? this.__loadPromise : undefined;
830
- };
831
- let latestPromise;
832
-
833
- // If the match was in an error state, set it
834
- // to a loading state again. Otherwise, keep it
835
- // as loading or resolved
836
- if (this.state.status === 'idle') {
837
- this.store.setState(s => ({
838
- ...s,
839
- status: 'pending'
840
- }));
841
- }
842
- const componentsPromise = (async () => {
843
- // then run all component and data loaders in parallel
844
- // For each component type, potentially load it asynchronously
845
-
846
- await Promise.all(componentTypes.map(async type => {
847
- const component = this.route.options[type];
848
- if (this[type]?.preload) {
849
- this[type] = await this.router.options.loadComponent(component);
850
- }
851
- }));
852
- })();
853
- const dataPromise = Promise.resolve().then(() => {
854
- if (this.route.options.onLoad) {
855
- return this.route.options.onLoad({
856
- params: this.params,
857
- routeSearch,
858
- search,
859
- signal: this.abortController.signal,
860
- preload: !!opts?.preload,
861
- routeContext: routeContext,
862
- context: context
863
- });
864
- }
865
- return;
866
- });
867
- try {
868
- await Promise.all([componentsPromise, dataPromise]);
869
- if (latestPromise = checkLatest()) return await latestPromise;
870
- this.store.setState(s => ({
871
- ...s,
872
- error: undefined,
873
- status: 'success',
874
- updatedAt: Date.now()
875
- }));
876
- } catch (err) {
877
- this.route.options.onLoadError?.(err);
878
- this.route.options.onError?.(err);
879
- this.store.setState(s => ({
880
- ...s,
881
- error: err,
882
- status: 'error',
883
- updatedAt: Date.now()
884
- }));
885
- } finally {
886
- delete this.__loadPromise;
887
- }
888
- });
889
- return this.__loadPromise;
890
- };
891
- #latestId = '';
892
- }
893
-
894
701
  const defaultParseSearch = parseSearchWith(JSON.parse);
895
702
  const defaultStringifySearch = stringifySearchWith(JSON.stringify);
896
703
  function parseSearchWith(parser) {
@@ -991,9 +798,9 @@
991
798
  mount = () => {
992
799
  // Mount only does anything on the client
993
800
  if (!isServer) {
994
- // If the router matches are empty, load the matches
801
+ // If the router matches are empty, start loading the matches
995
802
  if (!this.state.currentMatches.length) {
996
- this.load();
803
+ this.safeLoad();
997
804
  }
998
805
  const visibilityChangeEvent = 'visibilitychange';
999
806
  const focusEvent = 'focus';
@@ -1031,7 +838,7 @@
1031
838
  currentLocation: parsedLocation
1032
839
  }));
1033
840
  this.#unsubHistory = this.history.listen(() => {
1034
- this.load({
841
+ this.safeLoad({
1035
842
  next: this.#parseLocation(this.state.latestLocation)
1036
843
  });
1037
844
  });
@@ -1063,6 +870,12 @@
1063
870
  match.cancel();
1064
871
  });
1065
872
  };
873
+ safeLoad = opts => {
874
+ this.load(opts).catch(err => {
875
+ console.warn(err);
876
+ invariant(false, 'Encountered an error during router.load()! ☝️.');
877
+ });
878
+ };
1066
879
  load = async opts => {
1067
880
  let now = Date.now();
1068
881
  const startedAt = now;
@@ -1093,14 +906,10 @@
1093
906
  });
1094
907
 
1095
908
  // Load the matches
1096
- try {
1097
- await this.loadMatches(matches, this.state.pendingLocation
1098
- // opts
1099
- );
1100
- } catch (err) {
1101
- console.warn(err);
1102
- invariant(false, 'Matches failed to load due to error above ☝️. Navigation cancelled!');
1103
- }
909
+ await this.loadMatches(matches, this.state.pendingLocation
910
+ // opts
911
+ );
912
+
1104
913
  if (this.startedLoadingAt !== startedAt) {
1105
914
  // Ignore side-effects of outdated side-effects
1106
915
  return this.navigationPromise;
@@ -1529,7 +1338,7 @@
1529
1338
  };
1530
1339
  };
1531
1340
  #onFocus = () => {
1532
- this.load();
1341
+ this.safeLoad();
1533
1342
  };
1534
1343
  #buildLocation = (dest = {}) => {
1535
1344
  dest.fromCurrent = dest.fromCurrent ?? dest.to === '';
@@ -1592,9 +1401,6 @@
1592
1401
  id,
1593
1402
  ...next.state
1594
1403
  });
1595
-
1596
- // this.load(this.#parseLocation(this.state.latestLocation))
1597
-
1598
1404
  return this.navigationPromise = new Promise(resolve => {
1599
1405
  const previousNavigationResolve = this.resolveNavigation;
1600
1406
  this.resolveNavigation = () => {
@@ -1619,6 +1425,229 @@
1619
1425
  function isCtrlEvent(e) {
1620
1426
  return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
1621
1427
  }
1428
+ function redirect(opts) {
1429
+ opts.isRedirect = true;
1430
+ return opts;
1431
+ }
1432
+ function isRedirect(obj) {
1433
+ return !!obj?.isRedirect;
1434
+ }
1435
+
1436
+ const componentTypes = ['component', 'errorComponent', 'pendingComponent'];
1437
+ class RouteMatch {
1438
+ abortController = new AbortController();
1439
+ onLoaderDataListeners = new Set();
1440
+ constructor(router, route, opts) {
1441
+ Object.assign(this, {
1442
+ route,
1443
+ router,
1444
+ id: opts.id,
1445
+ pathname: opts.pathname,
1446
+ params: opts.params,
1447
+ store: new Store({
1448
+ updatedAt: 0,
1449
+ routeSearch: {},
1450
+ search: {},
1451
+ status: 'idle'
1452
+ }, {
1453
+ onUpdate: next => {
1454
+ this.state = next;
1455
+ }
1456
+ })
1457
+ });
1458
+ this.state = this.store.state;
1459
+ componentTypes.map(async type => {
1460
+ const component = this.route.options[type];
1461
+ if (typeof this[type] !== 'function') {
1462
+ this[type] = component;
1463
+ }
1464
+ });
1465
+ if (this.state.status === 'idle' && !this.#hasLoaders()) {
1466
+ this.store.setState(s => ({
1467
+ ...s,
1468
+ status: 'success'
1469
+ }));
1470
+ }
1471
+ }
1472
+ #hasLoaders = () => {
1473
+ return !!(this.route.options.onLoad || componentTypes.some(d => this.route.options[d]?.preload));
1474
+ };
1475
+ __commit = () => {
1476
+ const {
1477
+ routeSearch,
1478
+ search,
1479
+ context,
1480
+ routeContext
1481
+ } = this.#resolveInfo({
1482
+ location: this.router.state.currentLocation
1483
+ });
1484
+ this.context = context;
1485
+ this.routeContext = routeContext;
1486
+ this.store.setState(s => ({
1487
+ ...s,
1488
+ routeSearch: replaceEqualDeep(s.routeSearch, routeSearch),
1489
+ search: replaceEqualDeep(s.search, search)
1490
+ }));
1491
+ };
1492
+ cancel = () => {
1493
+ this.abortController?.abort();
1494
+ };
1495
+ #resolveSearchInfo = opts => {
1496
+ // Validate the search params and stabilize them
1497
+ const parentSearchInfo = this.parentMatch ? this.parentMatch.#resolveSearchInfo(opts) : {
1498
+ search: opts.location.search,
1499
+ routeSearch: opts.location.search
1500
+ };
1501
+ try {
1502
+ const validator = typeof this.route.options.validateSearch === 'object' ? this.route.options.validateSearch.parse : this.route.options.validateSearch;
1503
+ const routeSearch = validator?.(parentSearchInfo.search) ?? {};
1504
+ const search = {
1505
+ ...parentSearchInfo.search,
1506
+ ...routeSearch
1507
+ };
1508
+ return {
1509
+ routeSearch,
1510
+ search
1511
+ };
1512
+ } catch (err) {
1513
+ if (isRedirect(err)) {
1514
+ throw err;
1515
+ }
1516
+ this.route.options.onValidateSearchError?.(err);
1517
+ const error = new Error('Invalid search params found', {
1518
+ cause: err
1519
+ });
1520
+ error.code = 'INVALID_SEARCH_PARAMS';
1521
+ throw error;
1522
+ }
1523
+ };
1524
+ #resolveInfo = opts => {
1525
+ const {
1526
+ search,
1527
+ routeSearch
1528
+ } = this.#resolveSearchInfo(opts);
1529
+ const routeContext = this.route.options.getContext?.({
1530
+ parentContext: this.parentMatch?.routeContext ?? {},
1531
+ context: this.parentMatch?.context ?? this.router?.options.context ?? {},
1532
+ params: this.params,
1533
+ search
1534
+ }) || {};
1535
+ const context = {
1536
+ ...(this.parentMatch?.context ?? this.router?.options.context),
1537
+ ...routeContext
1538
+ };
1539
+ return {
1540
+ routeSearch,
1541
+ search,
1542
+ context,
1543
+ routeContext
1544
+ };
1545
+ };
1546
+ __load = async opts => {
1547
+ this.parentMatch = opts.parentMatch;
1548
+ let info;
1549
+ try {
1550
+ info = this.#resolveInfo(opts);
1551
+ } catch (err) {
1552
+ if (isRedirect(err)) {
1553
+ this.router.navigate(err);
1554
+ return;
1555
+ }
1556
+ this.route.options.onError?.(err);
1557
+ this.store.setState(s => ({
1558
+ ...s,
1559
+ status: 'error',
1560
+ error: err
1561
+ }));
1562
+
1563
+ // Do not proceed with loading the route
1564
+ return;
1565
+ }
1566
+ const {
1567
+ routeSearch,
1568
+ search,
1569
+ context,
1570
+ routeContext
1571
+ } = info;
1572
+
1573
+ // If the match is invalid, errored or idle, trigger it to load
1574
+ if (this.state.status === 'pending') {
1575
+ return;
1576
+ }
1577
+
1578
+ // TODO: Should load promises be tracked based on location?
1579
+ this.__loadPromise = Promise.resolve().then(async () => {
1580
+ const loadId = '' + Date.now() + Math.random();
1581
+ this.#latestId = loadId;
1582
+ const checkLatest = () => {
1583
+ return loadId !== this.#latestId ? this.__loadPromise : undefined;
1584
+ };
1585
+ let latestPromise;
1586
+
1587
+ // If the match was in an error state, set it
1588
+ // to a loading state again. Otherwise, keep it
1589
+ // as loading or resolved
1590
+ if (this.state.status === 'idle') {
1591
+ this.store.setState(s => ({
1592
+ ...s,
1593
+ status: 'pending'
1594
+ }));
1595
+ }
1596
+ const componentsPromise = (async () => {
1597
+ // then run all component and data loaders in parallel
1598
+ // For each component type, potentially load it asynchronously
1599
+
1600
+ await Promise.all(componentTypes.map(async type => {
1601
+ const component = this.route.options[type];
1602
+ if (this[type]?.preload) {
1603
+ this[type] = await this.router.options.loadComponent(component);
1604
+ }
1605
+ }));
1606
+ })();
1607
+ const dataPromise = Promise.resolve().then(() => {
1608
+ if (this.route.options.onLoad) {
1609
+ return this.route.options.onLoad({
1610
+ params: this.params,
1611
+ routeSearch,
1612
+ search,
1613
+ signal: this.abortController.signal,
1614
+ preload: !!opts?.preload,
1615
+ routeContext: routeContext,
1616
+ context: context
1617
+ });
1618
+ }
1619
+ return;
1620
+ });
1621
+ try {
1622
+ await Promise.all([componentsPromise, dataPromise]);
1623
+ if (latestPromise = checkLatest()) return await latestPromise;
1624
+ this.store.setState(s => ({
1625
+ ...s,
1626
+ error: undefined,
1627
+ status: 'success',
1628
+ updatedAt: Date.now()
1629
+ }));
1630
+ } catch (err) {
1631
+ if (isRedirect(err)) {
1632
+ this.router.navigate(err);
1633
+ return;
1634
+ }
1635
+ this.route.options.onLoadError?.(err);
1636
+ this.route.options.onError?.(err);
1637
+ this.store.setState(s => ({
1638
+ ...s,
1639
+ error: err,
1640
+ status: 'error',
1641
+ updatedAt: Date.now()
1642
+ }));
1643
+ } finally {
1644
+ delete this.__loadPromise;
1645
+ }
1646
+ });
1647
+ return this.__loadPromise;
1648
+ };
1649
+ #latestId = '';
1650
+ }
1622
1651
 
1623
1652
  /**
1624
1653
  * react-store
@@ -1826,12 +1855,17 @@
1826
1855
  router.update(rest);
1827
1856
  const currentMatches = useStore(router.store, s => s.currentMatches);
1828
1857
  React__namespace.useEffect(router.mount, [router]);
1829
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(routerContext.Provider, {
1858
+ return /*#__PURE__*/React__namespace.createElement(routerContext.Provider, {
1830
1859
  value: {
1831
1860
  router: router
1832
1861
  }
1833
1862
  }, /*#__PURE__*/React__namespace.createElement(matchesContext.Provider, {
1834
1863
  value: [undefined, ...currentMatches]
1864
+ }, /*#__PURE__*/React__namespace.createElement(CatchBoundary, {
1865
+ errorComponent: ErrorComponent,
1866
+ onCatch: () => {
1867
+ warning(false, `Error in router! Consider setting an 'errorComponent' in your RootRoute! 👍`);
1868
+ }
1835
1869
  }, /*#__PURE__*/React__namespace.createElement(Outlet, null))));
1836
1870
  }
1837
1871
  function useRouterContext() {
@@ -1944,23 +1978,24 @@
1944
1978
  }, []);
1945
1979
  const PendingComponent = match.pendingComponent ?? router.options.defaultPendingComponent ?? defaultPending;
1946
1980
  const errorComponent = match.errorComponent ?? router.options.defaultErrorComponent;
1981
+ const ResolvedSuspenseBoundary = match.route.options.wrapInSuspense ?? true ? React__namespace.Suspense : SafeFragment;
1982
+ const ResolvedCatchBoundary = errorComponent ? CatchBoundary : SafeFragment;
1947
1983
  return /*#__PURE__*/React__namespace.createElement(matchesContext.Provider, {
1948
1984
  value: matches
1949
- }, match.route.options.wrapInSuspense ?? true ? /*#__PURE__*/React__namespace.createElement(React__namespace.Suspense, {
1985
+ }, /*#__PURE__*/React__namespace.createElement(ResolvedSuspenseBoundary, {
1950
1986
  fallback: /*#__PURE__*/React__namespace.createElement(PendingComponent, null)
1951
- }, /*#__PURE__*/React__namespace.createElement(CatchBoundary, {
1952
- key: match.route.id,
1953
- errorComponent: errorComponent,
1954
- match: match
1955
- }, /*#__PURE__*/React__namespace.createElement(Inner, {
1956
- match: match
1957
- }))) : /*#__PURE__*/React__namespace.createElement(CatchBoundary, {
1987
+ }, /*#__PURE__*/React__namespace.createElement(ResolvedCatchBoundary, {
1958
1988
  key: match.route.id,
1959
1989
  errorComponent: errorComponent,
1960
- match: match
1990
+ onCatch: () => {
1991
+ warning(false, `Error in route match: ${match.id}`);
1992
+ }
1961
1993
  }, /*#__PURE__*/React__namespace.createElement(Inner, {
1962
1994
  match: match
1963
- })));
1995
+ }))));
1996
+ }
1997
+ function SafeFragment(props) {
1998
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, props.children);
1964
1999
  }
1965
2000
 
1966
2001
  // This is the messiest thing ever... I'm either seriously tired (likely) or
@@ -1973,7 +2008,7 @@
1973
2008
  info: undefined
1974
2009
  };
1975
2010
  componentDidCatch(error, info) {
1976
- console.error(`Error in route match: ${this.props.match.id}`);
2011
+ this.props.onCatch(error, info);
1977
2012
  console.error(error);
1978
2013
  this.setState({
1979
2014
  error,
@@ -1990,7 +2025,7 @@
1990
2025
  function CatchBoundaryInner(props) {
1991
2026
  const [activeErrorState, setActiveErrorState] = React__namespace.useState(props.errorState);
1992
2027
  const router = useRouterContext();
1993
- const errorComponent = props.errorComponent ?? DefaultErrorBoundary;
2028
+ const errorComponent = props.errorComponent ?? ErrorComponent;
1994
2029
  const prevKeyRef = React__namespace.useRef('');
1995
2030
  React__namespace.useEffect(() => {
1996
2031
  if (activeErrorState) {
@@ -2011,7 +2046,7 @@
2011
2046
  }
2012
2047
  return props.children;
2013
2048
  }
2014
- function DefaultErrorBoundary({
2049
+ function ErrorComponent({
2015
2050
  error
2016
2051
  }) {
2017
2052
  return /*#__PURE__*/React__namespace.createElement("div", {
@@ -2065,7 +2100,7 @@
2065
2100
  // return (children ?? null) as ReactNode
2066
2101
  // }
2067
2102
 
2068
- exports.DefaultErrorBoundary = DefaultErrorBoundary;
2103
+ exports.ErrorComponent = ErrorComponent;
2069
2104
  exports.Link = Link;
2070
2105
  exports.MatchRoute = MatchRoute;
2071
2106
  exports.Navigate = Navigate;
@@ -2089,6 +2124,7 @@
2089
2124
  exports.interpolatePath = interpolatePath;
2090
2125
  exports.invariant = invariant;
2091
2126
  exports.isPlainObject = isPlainObject;
2127
+ exports.isRedirect = isRedirect;
2092
2128
  exports.joinPaths = joinPaths;
2093
2129
  exports.last = last;
2094
2130
  exports.lazy = lazy;
@@ -2099,6 +2135,7 @@
2099
2135
  exports.parseSearchWith = parseSearchWith;
2100
2136
  exports.partialDeepEqual = partialDeepEqual;
2101
2137
  exports.pick = pick;
2138
+ exports.redirect = redirect;
2102
2139
  exports.replaceEqualDeep = replaceEqualDeep;
2103
2140
  exports.resolvePath = resolvePath;
2104
2141
  exports.rootRouteId = rootRouteId;