@remix-run/router 0.0.0-experimental-a077dc2d → 0.0.0-experimental-e192105b

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v0.0.0-experimental-a077dc2d
2
+ * @remix-run/router v0.0.0-experimental-e192105b
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1741,6 +1741,9 @@
1741
1741
  // Should the current navigation enable document.startViewTransition?
1742
1742
  let pendingViewTransitionEnabled = false;
1743
1743
 
1744
+ // Should the current navigation use flushSync for state updates?
1745
+ let pendingFlushSync = false;
1746
+
1744
1747
  // Store applied view transitions so we can apply them on POP
1745
1748
  let appliedViewTransitions = new Map();
1746
1749
 
@@ -1785,6 +1788,9 @@
1785
1788
  // Most recent href/match for fetcher.load calls for fetchers
1786
1789
  let fetchLoadMatches = new Map();
1787
1790
 
1791
+ // Ref-count mounted fetchers so we know when it's ok to clean them up
1792
+ let activeFetchers = new Map();
1793
+
1788
1794
  // Fetchers that have requested a delete when using v7_fetcherPersist,
1789
1795
  // they'll be officially removed after they return to idle
1790
1796
  let deletedFetchers = new Set();
@@ -1851,6 +1857,8 @@
1851
1857
  blockers.set(blockerKey, IDLE_BLOCKER);
1852
1858
  updateState({
1853
1859
  blockers
1860
+ }, {
1861
+ flushSync: false
1854
1862
  });
1855
1863
  }
1856
1864
  });
@@ -1899,29 +1907,42 @@
1899
1907
  }
1900
1908
 
1901
1909
  // Update our state and notify the calling context of the change
1902
- function updateState(newState, viewTransitionOpts) {
1910
+ function updateState(newState, opts) {
1903
1911
  state = _extends({}, state, newState);
1904
- subscribers.forEach(subscriber => subscriber(state, {
1905
- unstable_viewTransitionOpts: viewTransitionOpts
1906
- }));
1907
1912
 
1908
- // Remove idle fetchers from state since we only care about in-flight fetchers.
1913
+ // Prep fetcher cleanup so we can tell the UI which fetcher data entries
1914
+ // can be removed
1915
+ let completedFetchers = [];
1916
+ let deletedFetchersKeys = [];
1909
1917
  if (future.v7_fetcherPersist) {
1910
1918
  state.fetchers.forEach((fetcher, key) => {
1911
1919
  if (fetcher.state === "idle") {
1912
1920
  if (deletedFetchers.has(key)) {
1913
- // If the fetcher has unmounted and called router.deleteFetcher(),
1914
- // we can totally delete the fetcher
1915
- deleteFetcher(key);
1921
+ // Unmounted from the UI and can be totally removed
1922
+ deletedFetchersKeys.push(key);
1916
1923
  } else {
1917
- // Otherwise, it must still be mounted in the UI so we just remove
1918
- // it from state now that we've handed off the data to the React
1919
- // layer. Things such as fetchLoadMatches remain for revalidation.
1920
- state.fetchers.delete(key);
1924
+ // Returned to idle but still mounted in the UI, so semi-remains for
1925
+ // revalidations and such
1926
+ completedFetchers.push(key);
1921
1927
  }
1922
1928
  }
1923
1929
  });
1924
1930
  }
1931
+
1932
+ // Iterate over a local copy so that if flushSync is used and we end up
1933
+ // removing and adding a new subscriber due to the useCallback dependencies,
1934
+ // we don't get ourselves into a loop calling the new subscriber immediately
1935
+ [...subscribers].forEach(subscriber => subscriber(state, {
1936
+ deletedFetchers: deletedFetchersKeys,
1937
+ unstable_viewTransitionOpts: opts.viewTransitionOpts,
1938
+ unstable_flushSync: opts.flushSync
1939
+ }));
1940
+
1941
+ // Remove idle fetchers from state since we only care about in-flight fetchers.
1942
+ if (future.v7_fetcherPersist) {
1943
+ completedFetchers.forEach(key => state.fetchers.delete(key));
1944
+ deletedFetchersKeys.forEach(key => deleteFetcher(key));
1945
+ }
1925
1946
  }
1926
1947
 
1927
1948
  // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
@@ -2021,12 +2042,16 @@
2021
2042
  restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),
2022
2043
  preventScrollReset,
2023
2044
  blockers
2024
- }), viewTransitionOpts);
2045
+ }), {
2046
+ viewTransitionOpts,
2047
+ flushSync: pendingFlushSync
2048
+ });
2025
2049
 
2026
2050
  // Reset stateful navigation vars
2027
2051
  pendingAction = Action.Pop;
2028
2052
  pendingPreventScrollReset = false;
2029
2053
  pendingViewTransitionEnabled = false;
2054
+ pendingFlushSync = false;
2030
2055
  isUninterruptedRevalidation = false;
2031
2056
  isRevalidationRequired = false;
2032
2057
  cancelledDeferredRoutes = [];
@@ -2067,6 +2092,7 @@
2067
2092
  historyAction = Action.Replace;
2068
2093
  }
2069
2094
  let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
2095
+ let flushSync = (opts && opts.unstable_flushSync) === true;
2070
2096
  let blockerKey = shouldBlockNavigation({
2071
2097
  currentLocation,
2072
2098
  nextLocation,
@@ -2092,6 +2118,8 @@
2092
2118
  blockers.set(blockerKey, IDLE_BLOCKER);
2093
2119
  updateState({
2094
2120
  blockers
2121
+ }, {
2122
+ flushSync
2095
2123
  });
2096
2124
  }
2097
2125
  });
@@ -2104,7 +2132,8 @@
2104
2132
  pendingError: error,
2105
2133
  preventScrollReset,
2106
2134
  replace: opts && opts.replace,
2107
- enableViewTransition: opts && opts.unstable_viewTransition
2135
+ enableViewTransition: opts && opts.unstable_viewTransition,
2136
+ flushSync
2108
2137
  });
2109
2138
  }
2110
2139
 
@@ -2115,6 +2144,8 @@
2115
2144
  interruptActiveLoads();
2116
2145
  updateState({
2117
2146
  revalidation: "loading"
2147
+ }, {
2148
+ flushSync: false
2118
2149
  });
2119
2150
 
2120
2151
  // If we're currently submitting an action, we don't need to start a new
@@ -2158,6 +2189,7 @@
2158
2189
  saveScrollPosition(state.location, state.matches);
2159
2190
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2160
2191
  pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true;
2192
+ pendingFlushSync = (opts && opts.flushSync) === true;
2161
2193
  let routesToUse = inFlightDataRoutes || dataRoutes;
2162
2194
  let loadingNavigation = opts && opts.overrideNavigation;
2163
2195
  let matches = matchRoutes(routesToUse, location, basename);
@@ -2263,6 +2295,8 @@
2263
2295
  let navigation = getSubmittingNavigation(location, submission);
2264
2296
  updateState({
2265
2297
  navigation
2298
+ }, {
2299
+ flushSync: pendingFlushSync
2266
2300
  });
2267
2301
 
2268
2302
  // Call our action and get the result
@@ -2390,7 +2424,9 @@
2390
2424
  actionData
2391
2425
  } : {}, revalidatingFetchers.length > 0 ? {
2392
2426
  fetchers: new Map(state.fetchers)
2393
- } : {}));
2427
+ } : {}), {
2428
+ flushSync: pendingFlushSync
2429
+ });
2394
2430
  }
2395
2431
  revalidatingFetchers.forEach(rf => {
2396
2432
  if (fetchControllers.has(rf.key)) {
@@ -2473,9 +2509,6 @@
2473
2509
  fetchers: new Map(state.fetchers)
2474
2510
  } : {});
2475
2511
  }
2476
- function getFetcher(key) {
2477
- return state.fetchers.get(key) || IDLE_FETCHER;
2478
- }
2479
2512
 
2480
2513
  // Trigger a fetcher load/submit for the given fetcher key
2481
2514
  function fetch(key, routeId, href, opts) {
@@ -2483,13 +2516,16 @@
2483
2516
  throw new Error("router.fetch() was called during the server render, but it shouldn't be. " + "You are likely calling a useFetcher() method in the body of your component. " + "Try moving it to a useEffect or a callback.");
2484
2517
  }
2485
2518
  if (fetchControllers.has(key)) abortFetcher(key);
2519
+ let flushSync = (opts && opts.unstable_flushSync) === true;
2486
2520
  let routesToUse = inFlightDataRoutes || dataRoutes;
2487
2521
  let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, routeId, opts == null ? void 0 : opts.relative);
2488
2522
  let matches = matchRoutes(routesToUse, normalizedPath, basename);
2489
2523
  if (!matches) {
2490
2524
  setFetcherError(key, routeId, getInternalRouterError(404, {
2491
2525
  pathname: normalizedPath
2492
- }));
2526
+ }), {
2527
+ flushSync
2528
+ });
2493
2529
  return;
2494
2530
  }
2495
2531
  let {
@@ -2498,13 +2534,15 @@
2498
2534
  error
2499
2535
  } = normalizeNavigateOptions(future.v7_normalizeFormMethod, true, normalizedPath, opts);
2500
2536
  if (error) {
2501
- setFetcherError(key, routeId, error);
2537
+ setFetcherError(key, routeId, error, {
2538
+ flushSync
2539
+ });
2502
2540
  return;
2503
2541
  }
2504
2542
  let match = getTargetMatch(matches, path);
2505
2543
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2506
2544
  if (submission && isMutationMethod(submission.formMethod)) {
2507
- handleFetcherAction(key, routeId, path, match, matches, submission);
2545
+ handleFetcherAction(key, routeId, path, match, matches, flushSync, submission);
2508
2546
  return;
2509
2547
  }
2510
2548
 
@@ -2514,12 +2552,12 @@
2514
2552
  routeId,
2515
2553
  path
2516
2554
  });
2517
- handleFetcherLoader(key, routeId, path, match, matches, submission);
2555
+ handleFetcherLoader(key, routeId, path, match, matches, flushSync, submission);
2518
2556
  }
2519
2557
 
2520
2558
  // Call the action for the matched fetcher.submit(), and then handle redirects,
2521
2559
  // errors, and revalidation
2522
- async function handleFetcherAction(key, routeId, path, match, requestMatches, submission) {
2560
+ async function handleFetcherAction(key, routeId, path, match, requestMatches, flushSync, submission) {
2523
2561
  interruptActiveLoads();
2524
2562
  fetchLoadMatches.delete(key);
2525
2563
  if (!match.route.action && !match.route.lazy) {
@@ -2528,16 +2566,16 @@
2528
2566
  pathname: path,
2529
2567
  routeId: routeId
2530
2568
  });
2531
- setFetcherError(key, routeId, error);
2569
+ setFetcherError(key, routeId, error, {
2570
+ flushSync
2571
+ });
2532
2572
  return;
2533
2573
  }
2534
2574
 
2535
2575
  // Put this fetcher into it's submitting state
2536
2576
  let existingFetcher = state.fetchers.get(key);
2537
- let fetcher = getSubmittingFetcher(submission, existingFetcher);
2538
- state.fetchers.set(key, fetcher);
2539
- updateState({
2540
- fetchers: new Map(state.fetchers)
2577
+ updateFetcherState(key, getSubmittingFetcher(submission, existingFetcher), {
2578
+ flushSync
2541
2579
  });
2542
2580
 
2543
2581
  // Call the action for the fetcher
@@ -2547,13 +2585,19 @@
2547
2585
  let originatingLoadId = incrementingLoadId;
2548
2586
  let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename);
2549
2587
  if (fetchRequest.signal.aborted) {
2550
- // We can delete this so long as we weren't aborted by ou our own fetcher
2588
+ // We can delete this so long as we weren't aborted by our own fetcher
2551
2589
  // re-submit which would have put _new_ controller is in fetchControllers
2552
2590
  if (fetchControllers.get(key) === abortController) {
2553
2591
  fetchControllers.delete(key);
2554
2592
  }
2555
2593
  return;
2556
2594
  }
2595
+ if (deletedFetchers.has(key)) {
2596
+ updateFetcherState(key, getDoneFetcher(undefined), {
2597
+ flushSync
2598
+ });
2599
+ return;
2600
+ }
2557
2601
  if (isRedirectResult(actionResult)) {
2558
2602
  fetchControllers.delete(key);
2559
2603
  if (pendingNavigationLoadId > originatingLoadId) {
@@ -2561,18 +2605,14 @@
2561
2605
  // should take precedence over this redirect navigation. We already
2562
2606
  // set isRevalidationRequired so all loaders for the new route should
2563
2607
  // fire unless opted out via shouldRevalidate
2564
- let doneFetcher = getDoneFetcher(undefined);
2565
- state.fetchers.set(key, doneFetcher);
2566
- updateState({
2567
- fetchers: new Map(state.fetchers)
2608
+ updateFetcherState(key, getDoneFetcher(undefined), {
2609
+ flushSync
2568
2610
  });
2569
2611
  return;
2570
2612
  } else {
2571
2613
  fetchRedirectIds.add(key);
2572
- let loadingFetcher = getLoadingFetcher(submission);
2573
- state.fetchers.set(key, loadingFetcher);
2574
- updateState({
2575
- fetchers: new Map(state.fetchers)
2614
+ updateFetcherState(key, getLoadingFetcher(submission), {
2615
+ flushSync
2576
2616
  });
2577
2617
  return startRedirectNavigation(state, actionResult, {
2578
2618
  fetcherSubmission: submission
@@ -2582,7 +2622,9 @@
2582
2622
 
2583
2623
  // Process any non-redirect errors thrown
2584
2624
  if (isErrorResult(actionResult)) {
2585
- setFetcherError(key, routeId, actionResult.error);
2625
+ setFetcherError(key, routeId, actionResult.error, {
2626
+ flushSync
2627
+ });
2586
2628
  return;
2587
2629
  }
2588
2630
  if (isDeferredResult(actionResult)) {
@@ -2624,6 +2666,8 @@
2624
2666
  });
2625
2667
  updateState({
2626
2668
  fetchers: new Map(state.fetchers)
2669
+ }, {
2670
+ flushSync
2627
2671
  });
2628
2672
  let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach(rf => abortFetcher(rf.key));
2629
2673
  abortController.signal.addEventListener("abort", abortPendingFetchRevalidations);
@@ -2685,19 +2729,18 @@
2685
2729
  errors,
2686
2730
  loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors),
2687
2731
  fetchers: new Map(state.fetchers)
2732
+ }, {
2733
+ flushSync
2688
2734
  });
2689
2735
  isRevalidationRequired = false;
2690
2736
  }
2691
2737
  }
2692
2738
 
2693
2739
  // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
2694
- async function handleFetcherLoader(key, routeId, path, match, matches, submission) {
2740
+ async function handleFetcherLoader(key, routeId, path, match, matches, flushSync, submission) {
2695
2741
  let existingFetcher = state.fetchers.get(key);
2696
- // Put this fetcher into it's loading state
2697
- let loadingFetcher = getLoadingFetcher(submission, existingFetcher ? existingFetcher.data : undefined);
2698
- state.fetchers.set(key, loadingFetcher);
2699
- updateState({
2700
- fetchers: new Map(state.fetchers)
2742
+ updateFetcherState(key, getLoadingFetcher(submission, existingFetcher ? existingFetcher.data : undefined), {
2743
+ flushSync
2701
2744
  });
2702
2745
 
2703
2746
  // Call the loader for this fetcher route match
@@ -2723,16 +2766,20 @@
2723
2766
  if (fetchRequest.signal.aborted) {
2724
2767
  return;
2725
2768
  }
2769
+ if (deletedFetchers.has(key)) {
2770
+ updateFetcherState(key, getDoneFetcher(undefined), {
2771
+ flushSync
2772
+ });
2773
+ return;
2774
+ }
2726
2775
 
2727
2776
  // If the loader threw a redirect Response, start a new REPLACE navigation
2728
2777
  if (isRedirectResult(result)) {
2729
2778
  if (pendingNavigationLoadId > originatingLoadId) {
2730
2779
  // A new navigation was kicked off after our loader started, so that
2731
2780
  // should take precedence over this redirect navigation
2732
- let doneFetcher = getDoneFetcher(undefined);
2733
- state.fetchers.set(key, doneFetcher);
2734
- updateState({
2735
- fetchers: new Map(state.fetchers)
2781
+ updateFetcherState(key, getDoneFetcher(undefined), {
2782
+ flushSync
2736
2783
  });
2737
2784
  return;
2738
2785
  } else {
@@ -2744,26 +2791,16 @@
2744
2791
 
2745
2792
  // Process any non-redirect errors thrown
2746
2793
  if (isErrorResult(result)) {
2747
- let boundaryMatch = findNearestBoundary(state.matches, routeId);
2748
- state.fetchers.delete(key);
2749
- // TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch -
2750
- // do we need to behave any differently with our non-redirect errors?
2751
- // What if it was a non-redirect Response?
2752
- updateState({
2753
- fetchers: new Map(state.fetchers),
2754
- errors: {
2755
- [boundaryMatch.route.id]: result.error
2756
- }
2794
+ setFetcherError(key, routeId, result.error, {
2795
+ flushSync
2757
2796
  });
2758
2797
  return;
2759
2798
  }
2760
2799
  invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
2761
2800
 
2762
2801
  // Put the fetcher back into an idle state
2763
- let doneFetcher = getDoneFetcher(result.data);
2764
- state.fetchers.set(key, doneFetcher);
2765
- updateState({
2766
- fetchers: new Map(state.fetchers)
2802
+ updateFetcherState(key, getDoneFetcher(result.data), {
2803
+ flushSync
2767
2804
  });
2768
2805
  }
2769
2806
 
@@ -2905,7 +2942,15 @@
2905
2942
  }
2906
2943
  });
2907
2944
  }
2908
- function setFetcherError(key, routeId, error) {
2945
+ function updateFetcherState(key, fetcher, opts) {
2946
+ state.fetchers.set(key, fetcher);
2947
+ updateState({
2948
+ fetchers: new Map(state.fetchers)
2949
+ }, {
2950
+ flushSync: opts.flushSync
2951
+ });
2952
+ }
2953
+ function setFetcherError(key, routeId, error, opts) {
2909
2954
  let boundaryMatch = findNearestBoundary(state.matches, routeId);
2910
2955
  deleteFetcher(key);
2911
2956
  updateState({
@@ -2913,8 +2958,21 @@
2913
2958
  [boundaryMatch.route.id]: error
2914
2959
  },
2915
2960
  fetchers: new Map(state.fetchers)
2961
+ }, {
2962
+ flushSync: opts.flushSync
2916
2963
  });
2917
2964
  }
2965
+ function getFetcher(key) {
2966
+ if (future.v7_fetcherPersist) {
2967
+ activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
2968
+ // If this fetcher was previously marked for deletion, unmark it since we
2969
+ // have a new instance
2970
+ if (deletedFetchers.has(key)) {
2971
+ deletedFetchers.delete(key);
2972
+ }
2973
+ }
2974
+ return state.fetchers.get(key) || IDLE_FETCHER;
2975
+ }
2918
2976
  function deleteFetcher(key) {
2919
2977
  let fetcher = state.fetchers.get(key);
2920
2978
  // Don't abort the controller if this is a deletion of a fetcher.submit()
@@ -2931,12 +2989,20 @@
2931
2989
  }
2932
2990
  function deleteFetcherAndUpdateState(key) {
2933
2991
  if (future.v7_fetcherPersist) {
2934
- deletedFetchers.add(key);
2992
+ let count = (activeFetchers.get(key) || 0) - 1;
2993
+ if (count <= 0) {
2994
+ activeFetchers.delete(key);
2995
+ deletedFetchers.add(key);
2996
+ } else {
2997
+ activeFetchers.set(key, count);
2998
+ }
2935
2999
  } else {
2936
3000
  deleteFetcher(key);
2937
3001
  }
2938
3002
  updateState({
2939
3003
  fetchers: new Map(state.fetchers)
3004
+ }, {
3005
+ flushSync: false
2940
3006
  });
2941
3007
  }
2942
3008
  function abortFetcher(key) {
@@ -3006,6 +3072,8 @@
3006
3072
  blockers.set(key, newBlocker);
3007
3073
  updateState({
3008
3074
  blockers
3075
+ }, {
3076
+ flushSync: false
3009
3077
  });
3010
3078
  }
3011
3079
  function shouldBlockNavigation(_ref2) {
@@ -3073,6 +3141,8 @@
3073
3141
  if (y != null) {
3074
3142
  updateState({
3075
3143
  restoreScrollPosition: y
3144
+ }, {
3145
+ flushSync: false
3076
3146
  });
3077
3147
  }
3078
3148
  }