@tanstack/react-router 0.0.1-beta.210 → 0.0.1-beta.212

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
@@ -1623,7 +1633,6 @@
1623
1633
  preload: !!preload,
1624
1634
  context: parentContext,
1625
1635
  location: state.location,
1626
- // TODO: This might need to be latestLocationRef.current...?
1627
1636
  navigate: opts => navigate({
1628
1637
  ...opts,
1629
1638
  from: match.pathname
@@ -1743,16 +1752,16 @@
1743
1752
  const load = useStableCallback(async () => {
1744
1753
  const promise = new Promise(async (resolve, reject) => {
1745
1754
  const next = latestLocationRef.current;
1746
- const prevLocation = state.resolvedLocation || state.location;
1747
- const pathDidChange = !!(next && prevLocation.href !== next.href);
1755
+ const prevLocation = state.resolvedLocation;
1756
+ const pathDidChange = prevLocation.href !== next.href;
1748
1757
  let latestPromise;
1749
1758
 
1750
1759
  // Cancel any pending matches
1751
1760
  cancelMatches(state);
1752
1761
  router.emit({
1753
1762
  type: 'onBeforeLoad',
1754
- from: prevLocation,
1755
- to: next ?? state.location,
1763
+ fromLocation: prevLocation,
1764
+ toLocation: next,
1756
1765
  pathChanged: pathDidChange
1757
1766
  });
1758
1767
 
@@ -1760,10 +1769,13 @@
1760
1769
  let matches = matchRoutes(next.pathname, next.search, {
1761
1770
  debug: true
1762
1771
  });
1772
+ const previousMatches = state.matches;
1763
1773
 
1764
1774
  // Ingest the new matches
1765
1775
  setState(s => ({
1766
1776
  ...s,
1777
+ status: 'pending',
1778
+ location: next,
1767
1779
  matches
1768
1780
  }));
1769
1781
  try {
@@ -1782,17 +1794,9 @@
1782
1794
  if (latestPromise = checkLatest(promise)) {
1783
1795
  return latestPromise;
1784
1796
  }
1785
-
1786
- // TODO:
1787
- // const exitingMatchIds = previousMatches.filter(
1788
- // (id) => !state.pendingMatches.includes(id),
1789
- // )
1790
- // const enteringMatchIds = state.pendingMatches.filter(
1791
- // (id) => !previousMatches.includes(id),
1792
- // )
1793
- // const stayingMatchIds = previousMatches.filter((id) =>
1794
- // state.pendingMatches.includes(id),
1795
- // )
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))
1796
1800
 
1797
1801
  // setState((s) => ({
1798
1802
  // ...s,
@@ -1800,23 +1804,17 @@
1800
1804
  // resolvedLocation: s.location,
1801
1805
  // }))
1802
1806
 
1803
- // TODO:
1804
- // ;(
1805
- // [
1806
- // [exitingMatchIds, 'onLeave'],
1807
- // [enteringMatchIds, 'onEnter'],
1808
- // [stayingMatchIds, 'onTransition'],
1809
- // ] as const
1810
- // ).forEach(([matches, hook]) => {
1811
- // matches.forEach((match) => {
1812
- // const route = this.getRoute(match.routeId)
1813
- // route.options[hook]?.(match)
1814
- // })
1815
- // })
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
+ });
1816
1814
  router.emit({
1817
1815
  type: 'onLoad',
1818
- from: prevLocation,
1819
- to: next,
1816
+ fromLocation: prevLocation,
1817
+ toLocation: next,
1820
1818
  pathChanged: pathDidChange
1821
1819
  });
1822
1820
  resolve();
@@ -1949,20 +1947,17 @@
1949
1947
  disabled
1950
1948
  };
1951
1949
  });
1952
- const latestLocationRef = React__namespace.useRef(state.location);
1953
1950
  React__namespace.useLayoutEffect(() => {
1954
1951
  const unsub = history.subscribe(() => {
1955
1952
  latestLocationRef.current = parseLocation(latestLocationRef.current);
1956
- setState(s => ({
1957
- ...s,
1958
- status: 'pending'
1959
- }));
1960
1953
  if (state.location !== latestLocationRef.current) {
1961
- try {
1962
- load();
1963
- } catch (err) {
1964
- console.error(err);
1965
- }
1954
+ startReactTransition(() => {
1955
+ try {
1956
+ load();
1957
+ } catch (err) {
1958
+ console.error(err);
1959
+ }
1960
+ });
1966
1961
  }
1967
1962
  });
1968
1963
  const nextLocation = buildLocation({
@@ -2030,7 +2025,9 @@
2030
2025
  options,
2031
2026
  history,
2032
2027
  load,
2033
- buildLocation
2028
+ buildLocation,
2029
+ subscribe: router.subscribe,
2030
+ resetNextScrollRef
2034
2031
  };
2035
2032
  return /*#__PURE__*/React__namespace.createElement(routerContext.Provider, {
2036
2033
  value: routerContextValue
@@ -2416,6 +2413,7 @@
2416
2413
  preloadDelay,
2417
2414
  replace,
2418
2415
  startTransition,
2416
+ resetScroll,
2419
2417
  // element props
2420
2418
  style,
2421
2419
  className,
@@ -2495,6 +2493,151 @@
2495
2493
  }));
2496
2494
  });
2497
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
+
2498
2641
  function useBlocker(message, condition = true) {
2499
2642
  const {
2500
2643
  history
@@ -2545,7 +2688,7 @@
2545
2688
  const match = useMatch({
2546
2689
  strict: false
2547
2690
  });
2548
- useLayoutEffect(() => {
2691
+ useLayoutEffect$1(() => {
2549
2692
  navigate({
2550
2693
  from: props.to ? match.pathname : undefined,
2551
2694
  ...props
@@ -2570,6 +2713,7 @@
2570
2713
  exports.Route = Route;
2571
2714
  exports.Router = Router;
2572
2715
  exports.RouterProvider = RouterProvider;
2716
+ exports.ScrollRestoration = ScrollRestoration;
2573
2717
  exports.SearchParamError = SearchParamError;
2574
2718
  exports.cleanPath = cleanPath;
2575
2719
  exports.componentTypes = componentTypes;
@@ -2613,7 +2757,7 @@
2613
2757
  exports.trimPathRight = trimPathRight;
2614
2758
  exports.typedNavigate = typedNavigate;
2615
2759
  exports.useBlocker = useBlocker;
2616
- exports.useLayoutEffect = useLayoutEffect;
2760
+ exports.useLayoutEffect = useLayoutEffect$1;
2617
2761
  exports.useLinkProps = useLinkProps;
2618
2762
  exports.useMatch = useMatch;
2619
2763
  exports.useMatchRoute = useMatchRoute;
@@ -2623,6 +2767,7 @@
2623
2767
  exports.useRouteContext = useRouteContext;
2624
2768
  exports.useRouter = useRouter;
2625
2769
  exports.useRouterState = useRouterState;
2770
+ exports.useScrollRestoration = useScrollRestoration;
2626
2771
  exports.useSearch = useSearch;
2627
2772
  exports.useStableCallback = useStableCallback;
2628
2773
  exports.warning = warning;