@tanstack/react-router 0.0.1-beta.211 → 0.0.1-beta.213

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.
@@ -38,6 +38,8 @@ export type RouterContext<TRouteTree extends AnyRoute> = {
38
38
  history: RouterHistory;
39
39
  load: LoadFn;
40
40
  buildLocation: BuildLocationFn<TRouteTree>;
41
+ subscribe: Router<TRouteTree>['subscribe'];
42
+ resetNextScrollRef: React.MutableRefObject<boolean>;
41
43
  };
42
44
  export declare const routerContext: React.Context<RouterContext<any>>;
43
45
  export declare class SearchParamError extends Error {
@@ -16,6 +16,7 @@ export * from './route';
16
16
  export * from './routeInfo';
17
17
  export * from './router';
18
18
  export * from './RouterProvider';
19
+ export * from './scroll-restoration';
19
20
  export * from './searchParams';
20
21
  export * from './useBlocker';
21
22
  export * from './useNavigate';
@@ -58,7 +58,7 @@ export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
58
58
  matches: RouteMatch<TRouteTree>[];
59
59
  pendingMatches: RouteMatch<TRouteTree>[];
60
60
  location: ParsedLocation<FullSearchSchema<TRouteTree>>;
61
- resolvedLocation: undefined | ParsedLocation<FullSearchSchema<TRouteTree>>;
61
+ resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>;
62
62
  lastUpdated: number;
63
63
  }
64
64
  export type ListenerFn<TEvent extends RouterEvent> = (event: TEvent) => void;
@@ -90,14 +90,20 @@ export declare const componentTypes: readonly ["component", "errorComponent", "p
90
90
  export type RouterEvents = {
91
91
  onBeforeLoad: {
92
92
  type: 'onBeforeLoad';
93
- from: undefined | ParsedLocation;
94
- to: ParsedLocation;
93
+ fromLocation: ParsedLocation;
94
+ toLocation: ParsedLocation;
95
95
  pathChanged: boolean;
96
96
  };
97
97
  onLoad: {
98
98
  type: 'onLoad';
99
- from: undefined | ParsedLocation;
100
- to: ParsedLocation;
99
+ fromLocation: ParsedLocation;
100
+ toLocation: ParsedLocation;
101
+ pathChanged: boolean;
102
+ };
103
+ onResolved: {
104
+ type: 'onResolved';
105
+ fromLocation: ParsedLocation;
106
+ toLocation: ParsedLocation;
101
107
  pathChanged: boolean;
102
108
  };
103
109
  };
@@ -0,0 +1,6 @@
1
+ import { ParsedLocation } from './location';
2
+ export type ScrollRestorationOptions = {
3
+ getKey?: (location: ParsedLocation) => string;
4
+ };
5
+ export declare function useScrollRestoration(options?: ScrollRestorationOptions): void;
6
+ export declare function ScrollRestoration(props: ScrollRestorationOptions): null;
@@ -147,8 +147,10 @@
147
147
  if (!state) {
148
148
  state = {};
149
149
  }
150
- state.key = createRandomKey();
151
- return state;
150
+ return {
151
+ ...state,
152
+ key: createRandomKey()
153
+ };
152
154
  }
153
155
 
154
156
  /**
@@ -600,7 +602,7 @@
600
602
  select: match => opts?.select ? opts.select(match.context) : match.context
601
603
  });
602
604
  }
603
- const useLayoutEffect = typeof window !== 'undefined' ? React__namespace.useLayoutEffect : React__namespace.useEffect;
605
+ const useLayoutEffect$1 = typeof window !== 'undefined' ? React__namespace.useLayoutEffect : React__namespace.useEffect;
604
606
 
605
607
  function joinPaths(paths) {
606
608
  return cleanPath(paths.filter(Boolean).join('/'));
@@ -1102,8 +1104,8 @@
1102
1104
  function getInitialRouterState(location) {
1103
1105
  return {
1104
1106
  status: 'idle',
1105
- resolvedLocation: undefined,
1106
- location: location,
1107
+ resolvedLocation: location,
1108
+ location,
1107
1109
  matches: [],
1108
1110
  pendingMatches: [],
1109
1111
  lastUpdated: Date.now()
@@ -1123,7 +1125,7 @@
1123
1125
  };
1124
1126
  const history = React__namespace.useState(() => options.history ?? createBrowserHistory())[0];
1125
1127
  const tempLocationKeyRef = React__namespace.useRef(`${Math.round(Math.random() * 10000000)}`);
1126
- const resetNextScrollRef = React__namespace.useRef(false);
1128
+ const resetNextScrollRef = React__namespace.useRef(true);
1127
1129
  const navigateTimeoutRef = React__namespace.useRef(null);
1128
1130
  const latestLoadPromiseRef = React__namespace.useRef(Promise.resolve());
1129
1131
  const checkLatest = promise => {
@@ -1163,14 +1165,22 @@
1163
1165
  }
1164
1166
  return location;
1165
1167
  });
1166
- const [preState, setState] = React__namespace.useState(() => getInitialRouterState(parseLocation()));
1168
+ const latestLocationRef = React__namespace.useRef(parseLocation());
1169
+ const [preState, setState] = React__namespace.useState(() => getInitialRouterState(latestLocationRef.current));
1167
1170
  const [isTransitioning, startReactTransition] = React__namespace.useTransition();
1168
1171
  const state = React__namespace.useMemo(() => ({
1169
1172
  ...preState,
1170
- status: isTransitioning ? 'pending' : 'idle'
1173
+ status: isTransitioning ? 'pending' : 'idle',
1174
+ location: isTransitioning ? latestLocationRef.current : preState.location
1171
1175
  }), [preState, isTransitioning]);
1172
1176
  React__namespace.useLayoutEffect(() => {
1173
1177
  if (!isTransitioning && state.resolvedLocation !== state.location) {
1178
+ router.emit({
1179
+ type: 'onResolved',
1180
+ fromLocation: state.resolvedLocation,
1181
+ toLocation: state.location,
1182
+ pathChanged: state.location.href !== state.resolvedLocation?.href
1183
+ });
1174
1184
  setState(s => ({
1175
1185
  ...s,
1176
1186
  resolvedLocation: s.location
@@ -1742,16 +1752,16 @@
1742
1752
  const load = useStableCallback(async () => {
1743
1753
  const promise = new Promise(async (resolve, reject) => {
1744
1754
  const next = latestLocationRef.current;
1745
- const prevLocation = state.resolvedLocation || state.location;
1746
- const pathDidChange = !!(next && prevLocation.href !== next.href);
1755
+ const prevLocation = state.resolvedLocation;
1756
+ const pathDidChange = prevLocation.href !== next.href;
1747
1757
  let latestPromise;
1748
1758
 
1749
1759
  // Cancel any pending matches
1750
1760
  cancelMatches(state);
1751
1761
  router.emit({
1752
1762
  type: 'onBeforeLoad',
1753
- from: prevLocation,
1754
- to: next ?? state.location,
1763
+ fromLocation: prevLocation,
1764
+ toLocation: next,
1755
1765
  pathChanged: pathDidChange
1756
1766
  });
1757
1767
 
@@ -1759,10 +1769,13 @@
1759
1769
  let matches = matchRoutes(next.pathname, next.search, {
1760
1770
  debug: true
1761
1771
  });
1772
+ const previousMatches = state.matches;
1762
1773
 
1763
1774
  // Ingest the new matches
1764
1775
  setState(s => ({
1765
1776
  ...s,
1777
+ status: 'pending',
1778
+ location: next,
1766
1779
  matches
1767
1780
  }));
1768
1781
  try {
@@ -1781,17 +1794,9 @@
1781
1794
  if (latestPromise = checkLatest(promise)) {
1782
1795
  return latestPromise;
1783
1796
  }
1784
-
1785
- // TODO:
1786
- // const exitingMatchIds = previousMatches.filter(
1787
- // (id) => !state.pendingMatches.includes(id),
1788
- // )
1789
- // const enteringMatchIds = state.pendingMatches.filter(
1790
- // (id) => !previousMatches.includes(id),
1791
- // )
1792
- // const stayingMatchIds = previousMatches.filter((id) =>
1793
- // state.pendingMatches.includes(id),
1794
- // )
1797
+ const exitingMatchIds = previousMatches.filter(id => !state.pendingMatches.includes(id));
1798
+ const enteringMatchIds = state.pendingMatches.filter(id => !previousMatches.includes(id));
1799
+ const stayingMatchIds = previousMatches.filter(id => state.pendingMatches.includes(id))
1795
1800
 
1796
1801
  // setState((s) => ({
1797
1802
  // ...s,
@@ -1799,23 +1804,17 @@
1799
1804
  // resolvedLocation: s.location,
1800
1805
  // }))
1801
1806
 
1802
- // TODO:
1803
- // ;(
1804
- // [
1805
- // [exitingMatchIds, 'onLeave'],
1806
- // [enteringMatchIds, 'onEnter'],
1807
- // [stayingMatchIds, 'onTransition'],
1808
- // ] as const
1809
- // ).forEach(([matches, hook]) => {
1810
- // matches.forEach((match) => {
1811
- // const route = this.getRoute(match.routeId)
1812
- // route.options[hook]?.(match)
1813
- // })
1814
- // })
1807
+ //
1808
+ ;
1809
+ [[exitingMatchIds, 'onLeave'], [enteringMatchIds, 'onEnter'], [stayingMatchIds, 'onTransition']].forEach(([matches, hook]) => {
1810
+ matches.forEach(match => {
1811
+ looseRoutesById[match.routeId].options[hook]?.(match);
1812
+ });
1813
+ });
1815
1814
  router.emit({
1816
1815
  type: 'onLoad',
1817
- from: prevLocation,
1818
- to: next,
1816
+ fromLocation: prevLocation,
1817
+ toLocation: next,
1819
1818
  pathChanged: pathDidChange
1820
1819
  });
1821
1820
  resolve();
@@ -1948,20 +1947,17 @@
1948
1947
  disabled
1949
1948
  };
1950
1949
  });
1951
- const latestLocationRef = React__namespace.useRef(state.location);
1952
1950
  React__namespace.useLayoutEffect(() => {
1953
1951
  const unsub = history.subscribe(() => {
1954
1952
  latestLocationRef.current = parseLocation(latestLocationRef.current);
1955
- setState(s => ({
1956
- ...s,
1957
- status: 'pending'
1958
- }));
1959
1953
  if (state.location !== latestLocationRef.current) {
1960
- try {
1961
- load();
1962
- } catch (err) {
1963
- console.error(err);
1964
- }
1954
+ startReactTransition(() => {
1955
+ try {
1956
+ load();
1957
+ } catch (err) {
1958
+ console.error(err);
1959
+ }
1960
+ });
1965
1961
  }
1966
1962
  });
1967
1963
  const nextLocation = buildLocation({
@@ -2029,7 +2025,9 @@
2029
2025
  options,
2030
2026
  history,
2031
2027
  load,
2032
- buildLocation
2028
+ buildLocation,
2029
+ subscribe: router.subscribe,
2030
+ resetNextScrollRef
2033
2031
  };
2034
2032
  return /*#__PURE__*/React__namespace.createElement(routerContext.Provider, {
2035
2033
  value: routerContextValue
@@ -2415,6 +2413,7 @@
2415
2413
  preloadDelay,
2416
2414
  replace,
2417
2415
  startTransition,
2416
+ resetScroll,
2418
2417
  // element props
2419
2418
  style,
2420
2419
  className,
@@ -2494,6 +2493,151 @@
2494
2493
  }));
2495
2494
  });
2496
2495
 
2496
+ const useLayoutEffect = typeof window !== 'undefined' ? React__namespace.useLayoutEffect : React__namespace.useEffect;
2497
+ const windowKey = 'window';
2498
+ const delimiter = '___';
2499
+ let weakScrolledElements = new WeakSet();
2500
+ let cache;
2501
+ const sessionsStorage = typeof window !== 'undefined' && window.sessionStorage;
2502
+ const defaultGetKey = location => location.state.key;
2503
+ function useScrollRestoration(options) {
2504
+ const {
2505
+ state,
2506
+ subscribe,
2507
+ resetNextScrollRef
2508
+ } = useRouter();
2509
+ useLayoutEffect(() => {
2510
+ const getKey = options?.getKey || defaultGetKey;
2511
+ if (sessionsStorage) {
2512
+ if (!cache) {
2513
+ cache = (() => {
2514
+ const storageKey = 'tsr-scroll-restoration-v2';
2515
+ const state = JSON.parse(window.sessionStorage.getItem(storageKey) || 'null') || {
2516
+ cached: {},
2517
+ next: {}
2518
+ };
2519
+ return {
2520
+ state,
2521
+ set: updater => {
2522
+ cache.state = functionalUpdate(updater, cache.state);
2523
+ window.sessionStorage.setItem(storageKey, JSON.stringify(cache.state));
2524
+ }
2525
+ };
2526
+ })();
2527
+ }
2528
+ }
2529
+ const {
2530
+ history
2531
+ } = window;
2532
+ if (history.scrollRestoration) {
2533
+ history.scrollRestoration = 'manual';
2534
+ }
2535
+ const onScroll = event => {
2536
+ if (weakScrolledElements.has(event.target)) return;
2537
+ weakScrolledElements.add(event.target);
2538
+ const elementSelector = event.target === document || event.target === window ? windowKey : getCssSelector(event.target);
2539
+ if (!cache.state.next[elementSelector]) {
2540
+ cache.set(c => ({
2541
+ ...c,
2542
+ next: {
2543
+ ...c.next,
2544
+ [elementSelector]: {
2545
+ scrollX: NaN,
2546
+ scrollY: NaN
2547
+ }
2548
+ }
2549
+ }));
2550
+ }
2551
+ };
2552
+ const getCssSelector = el => {
2553
+ let path = [],
2554
+ parent;
2555
+ while (parent = el.parentNode) {
2556
+ path.unshift(`${el.tagName}:nth-child(${[].indexOf.call(parent.children, el) + 1})`);
2557
+ el = parent;
2558
+ }
2559
+ return `${path.join(' > ')}`.toLowerCase();
2560
+ };
2561
+ if (typeof document !== 'undefined') {
2562
+ document.addEventListener('scroll', onScroll, true);
2563
+ }
2564
+ const unsubOnBeforeLoad = subscribe('onBeforeLoad', event => {
2565
+ if (event.pathChanged) {
2566
+ const restoreKey = getKey(event.fromLocation);
2567
+ for (const elementSelector in cache.state.next) {
2568
+ const entry = cache.state.next[elementSelector];
2569
+ if (elementSelector === windowKey) {
2570
+ entry.scrollX = window.scrollX || 0;
2571
+ entry.scrollY = window.scrollY || 0;
2572
+ } else if (elementSelector) {
2573
+ const element = document.querySelector(elementSelector);
2574
+ entry.scrollX = element?.scrollLeft || 0;
2575
+ entry.scrollY = element?.scrollTop || 0;
2576
+ }
2577
+ cache.set(c => {
2578
+ const next = {
2579
+ ...c.next
2580
+ };
2581
+ delete next[elementSelector];
2582
+ return {
2583
+ ...c,
2584
+ next,
2585
+ cached: {
2586
+ ...c.cached,
2587
+ [[restoreKey, elementSelector].join(delimiter)]: entry
2588
+ }
2589
+ };
2590
+ });
2591
+ }
2592
+ }
2593
+ });
2594
+ const unsubOnResolved = subscribe('onResolved', event => {
2595
+ if (event.pathChanged) {
2596
+ if (!resetNextScrollRef.current) {
2597
+ return;
2598
+ }
2599
+ resetNextScrollRef.current = true;
2600
+ const getKey = options?.getKey || defaultGetKey;
2601
+ const restoreKey = getKey(event.toLocation);
2602
+ let windowRestored = false;
2603
+ for (const cacheKey in cache.state.cached) {
2604
+ const entry = cache.state.cached[cacheKey];
2605
+ const [key, elementSelector] = cacheKey.split(delimiter);
2606
+ if (key === restoreKey) {
2607
+ if (elementSelector === windowKey) {
2608
+ windowRestored = true;
2609
+ window.scrollTo(entry.scrollX, entry.scrollY);
2610
+ } else if (elementSelector) {
2611
+ const element = document.querySelector(elementSelector);
2612
+ if (element) {
2613
+ element.scrollLeft = entry.scrollX;
2614
+ element.scrollTop = entry.scrollY;
2615
+ }
2616
+ }
2617
+ }
2618
+ }
2619
+ if (!windowRestored) {
2620
+ window.scrollTo(0, 0);
2621
+ }
2622
+ cache.set(c => ({
2623
+ ...c,
2624
+ next: {}
2625
+ }));
2626
+ weakScrolledElements = new WeakSet();
2627
+ }
2628
+ });
2629
+ return () => {
2630
+ document.removeEventListener('scroll', onScroll);
2631
+ unsubOnBeforeLoad();
2632
+ unsubOnResolved();
2633
+ };
2634
+ }, []);
2635
+ }
2636
+ function ScrollRestoration(props) {
2637
+ useScrollRestoration(props);
2638
+ return null;
2639
+ }
2640
+
2497
2641
  function useBlocker(message, condition = true) {
2498
2642
  const {
2499
2643
  history
@@ -2544,7 +2688,7 @@
2544
2688
  const match = useMatch({
2545
2689
  strict: false
2546
2690
  });
2547
- useLayoutEffect(() => {
2691
+ useLayoutEffect$1(() => {
2548
2692
  navigate({
2549
2693
  from: props.to ? match.pathname : undefined,
2550
2694
  ...props
@@ -2569,6 +2713,7 @@
2569
2713
  exports.Route = Route;
2570
2714
  exports.Router = Router;
2571
2715
  exports.RouterProvider = RouterProvider;
2716
+ exports.ScrollRestoration = ScrollRestoration;
2572
2717
  exports.SearchParamError = SearchParamError;
2573
2718
  exports.cleanPath = cleanPath;
2574
2719
  exports.componentTypes = componentTypes;
@@ -2612,7 +2757,7 @@
2612
2757
  exports.trimPathRight = trimPathRight;
2613
2758
  exports.typedNavigate = typedNavigate;
2614
2759
  exports.useBlocker = useBlocker;
2615
- exports.useLayoutEffect = useLayoutEffect;
2760
+ exports.useLayoutEffect = useLayoutEffect$1;
2616
2761
  exports.useLinkProps = useLinkProps;
2617
2762
  exports.useMatch = useMatch;
2618
2763
  exports.useMatchRoute = useMatchRoute;
@@ -2622,6 +2767,7 @@
2622
2767
  exports.useRouteContext = useRouteContext;
2623
2768
  exports.useRouter = useRouter;
2624
2769
  exports.useRouterState = useRouterState;
2770
+ exports.useScrollRestoration = useScrollRestoration;
2625
2771
  exports.useSearch = useSearch;
2626
2772
  exports.useStableCallback = useStableCallback;
2627
2773
  exports.warning = warning;