@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.
package/dist/router.d.ts CHANGED
@@ -304,7 +304,9 @@ type ViewTransitionOpts = {
304
304
  */
305
305
  export interface RouterSubscriber {
306
306
  (state: RouterState, opts: {
307
+ deletedFetchers: string[];
307
308
  unstable_viewTransitionOpts?: ViewTransitionOpts;
309
+ unstable_flushSync?: boolean;
308
310
  }): void;
309
311
  }
310
312
  /**
@@ -324,6 +326,7 @@ export type RelativeRoutingType = "route" | "path";
324
326
  type BaseNavigateOrFetchOptions = {
325
327
  preventScrollReset?: boolean;
326
328
  relative?: RelativeRoutingType;
329
+ unstable_flushSync?: boolean;
327
330
  };
328
331
  type BaseNavigateOptions = BaseNavigateOrFetchOptions & {
329
332
  replace?: boolean;
package/dist/router.js CHANGED
@@ -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
  *
@@ -1386,6 +1386,8 @@ function createRouter(init) {
1386
1386
  let pendingNavigationController;
1387
1387
  // Should the current navigation enable document.startViewTransition?
1388
1388
  let pendingViewTransitionEnabled = false;
1389
+ // Should the current navigation use flushSync for state updates?
1390
+ let pendingFlushSync = false;
1389
1391
  // Store applied view transitions so we can apply them on POP
1390
1392
  let appliedViewTransitions = new Map();
1391
1393
  // Cleanup function for persisting applied transitions to sessionStorage
@@ -1418,6 +1420,8 @@ function createRouter(init) {
1418
1420
  let fetchRedirectIds = new Set();
1419
1421
  // Most recent href/match for fetcher.load calls for fetchers
1420
1422
  let fetchLoadMatches = new Map();
1423
+ // Ref-count mounted fetchers so we know when it's ok to clean them up
1424
+ let activeFetchers = new Map();
1421
1425
  // Fetchers that have requested a delete when using v7_fetcherPersist,
1422
1426
  // they'll be officially removed after they return to idle
1423
1427
  let deletedFetchers = new Set();
@@ -1479,6 +1483,8 @@ function createRouter(init) {
1479
1483
  blockers.set(blockerKey, IDLE_BLOCKER);
1480
1484
  updateState({
1481
1485
  blockers
1486
+ }, {
1487
+ flushSync: false
1482
1488
  });
1483
1489
  }
1484
1490
  });
@@ -1523,28 +1529,39 @@ function createRouter(init) {
1523
1529
  return () => subscribers.delete(fn);
1524
1530
  }
1525
1531
  // Update our state and notify the calling context of the change
1526
- function updateState(newState, viewTransitionOpts) {
1532
+ function updateState(newState, opts) {
1527
1533
  state = _extends({}, state, newState);
1528
- subscribers.forEach(subscriber => subscriber(state, {
1529
- unstable_viewTransitionOpts: viewTransitionOpts
1530
- }));
1531
- // Remove idle fetchers from state since we only care about in-flight fetchers.
1534
+ // Prep fetcher cleanup so we can tell the UI which fetcher data entries
1535
+ // can be removed
1536
+ let completedFetchers = [];
1537
+ let deletedFetchersKeys = [];
1532
1538
  if (future.v7_fetcherPersist) {
1533
1539
  state.fetchers.forEach((fetcher, key) => {
1534
1540
  if (fetcher.state === "idle") {
1535
1541
  if (deletedFetchers.has(key)) {
1536
- // If the fetcher has unmounted and called router.deleteFetcher(),
1537
- // we can totally delete the fetcher
1538
- deleteFetcher(key);
1542
+ // Unmounted from the UI and can be totally removed
1543
+ deletedFetchersKeys.push(key);
1539
1544
  } else {
1540
- // Otherwise, it must still be mounted in the UI so we just remove
1541
- // it from state now that we've handed off the data to the React
1542
- // layer. Things such as fetchLoadMatches remain for revalidation.
1543
- state.fetchers.delete(key);
1545
+ // Returned to idle but still mounted in the UI, so semi-remains for
1546
+ // revalidations and such
1547
+ completedFetchers.push(key);
1544
1548
  }
1545
1549
  }
1546
1550
  });
1547
1551
  }
1552
+ // Iterate over a local copy so that if flushSync is used and we end up
1553
+ // removing and adding a new subscriber due to the useCallback dependencies,
1554
+ // we don't get ourselves into a loop calling the new subscriber immediately
1555
+ [...subscribers].forEach(subscriber => subscriber(state, {
1556
+ deletedFetchers: deletedFetchersKeys,
1557
+ unstable_viewTransitionOpts: opts.viewTransitionOpts,
1558
+ unstable_flushSync: opts.flushSync
1559
+ }));
1560
+ // Remove idle fetchers from state since we only care about in-flight fetchers.
1561
+ if (future.v7_fetcherPersist) {
1562
+ completedFetchers.forEach(key => state.fetchers.delete(key));
1563
+ deletedFetchersKeys.forEach(key => deleteFetcher(key));
1564
+ }
1548
1565
  }
1549
1566
  // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
1550
1567
  // and setting state.[historyAction/location/matches] to the new route.
@@ -1638,11 +1655,15 @@ function createRouter(init) {
1638
1655
  restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),
1639
1656
  preventScrollReset,
1640
1657
  blockers
1641
- }), viewTransitionOpts);
1658
+ }), {
1659
+ viewTransitionOpts,
1660
+ flushSync: pendingFlushSync
1661
+ });
1642
1662
  // Reset stateful navigation vars
1643
1663
  pendingAction = Action.Pop;
1644
1664
  pendingPreventScrollReset = false;
1645
1665
  pendingViewTransitionEnabled = false;
1666
+ pendingFlushSync = false;
1646
1667
  isUninterruptedRevalidation = false;
1647
1668
  isRevalidationRequired = false;
1648
1669
  cancelledDeferredRoutes = [];
@@ -1681,6 +1702,7 @@ function createRouter(init) {
1681
1702
  historyAction = Action.Replace;
1682
1703
  }
1683
1704
  let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
1705
+ let flushSync = (opts && opts.unstable_flushSync) === true;
1684
1706
  let blockerKey = shouldBlockNavigation({
1685
1707
  currentLocation,
1686
1708
  nextLocation,
@@ -1706,6 +1728,8 @@ function createRouter(init) {
1706
1728
  blockers.set(blockerKey, IDLE_BLOCKER);
1707
1729
  updateState({
1708
1730
  blockers
1731
+ }, {
1732
+ flushSync
1709
1733
  });
1710
1734
  }
1711
1735
  });
@@ -1718,7 +1742,8 @@ function createRouter(init) {
1718
1742
  pendingError: error,
1719
1743
  preventScrollReset,
1720
1744
  replace: opts && opts.replace,
1721
- enableViewTransition: opts && opts.unstable_viewTransition
1745
+ enableViewTransition: opts && opts.unstable_viewTransition,
1746
+ flushSync
1722
1747
  });
1723
1748
  }
1724
1749
  // Revalidate all current loaders. If a navigation is in progress or if this
@@ -1728,6 +1753,8 @@ function createRouter(init) {
1728
1753
  interruptActiveLoads();
1729
1754
  updateState({
1730
1755
  revalidation: "loading"
1756
+ }, {
1757
+ flushSync: false
1731
1758
  });
1732
1759
  // If we're currently submitting an action, we don't need to start a new
1733
1760
  // navigation, we'll just let the follow up loader execution call all loaders
@@ -1766,6 +1793,7 @@ function createRouter(init) {
1766
1793
  saveScrollPosition(state.location, state.matches);
1767
1794
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
1768
1795
  pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true;
1796
+ pendingFlushSync = (opts && opts.flushSync) === true;
1769
1797
  let routesToUse = inFlightDataRoutes || dataRoutes;
1770
1798
  let loadingNavigation = opts && opts.overrideNavigation;
1771
1799
  let matches = matchRoutes(routesToUse, location, basename);
@@ -1863,6 +1891,8 @@ function createRouter(init) {
1863
1891
  let navigation = getSubmittingNavigation(location, submission);
1864
1892
  updateState({
1865
1893
  navigation
1894
+ }, {
1895
+ flushSync: pendingFlushSync
1866
1896
  });
1867
1897
  // Call our action and get the result
1868
1898
  let result;
@@ -1983,7 +2013,9 @@ function createRouter(init) {
1983
2013
  actionData
1984
2014
  } : {}, revalidatingFetchers.length > 0 ? {
1985
2015
  fetchers: new Map(state.fetchers)
1986
- } : {}));
2016
+ } : {}), {
2017
+ flushSync: pendingFlushSync
2018
+ });
1987
2019
  }
1988
2020
  revalidatingFetchers.forEach(rf => {
1989
2021
  if (fetchControllers.has(rf.key)) {
@@ -2061,22 +2093,22 @@ function createRouter(init) {
2061
2093
  fetchers: new Map(state.fetchers)
2062
2094
  } : {});
2063
2095
  }
2064
- function getFetcher(key) {
2065
- return state.fetchers.get(key) || IDLE_FETCHER;
2066
- }
2067
2096
  // Trigger a fetcher load/submit for the given fetcher key
2068
2097
  function fetch(key, routeId, href, opts) {
2069
2098
  if (isServer) {
2070
2099
  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.");
2071
2100
  }
2072
2101
  if (fetchControllers.has(key)) abortFetcher(key);
2102
+ let flushSync = (opts && opts.unstable_flushSync) === true;
2073
2103
  let routesToUse = inFlightDataRoutes || dataRoutes;
2074
2104
  let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, routeId, opts == null ? void 0 : opts.relative);
2075
2105
  let matches = matchRoutes(routesToUse, normalizedPath, basename);
2076
2106
  if (!matches) {
2077
2107
  setFetcherError(key, routeId, getInternalRouterError(404, {
2078
2108
  pathname: normalizedPath
2079
- }));
2109
+ }), {
2110
+ flushSync
2111
+ });
2080
2112
  return;
2081
2113
  }
2082
2114
  let {
@@ -2085,13 +2117,15 @@ function createRouter(init) {
2085
2117
  error
2086
2118
  } = normalizeNavigateOptions(future.v7_normalizeFormMethod, true, normalizedPath, opts);
2087
2119
  if (error) {
2088
- setFetcherError(key, routeId, error);
2120
+ setFetcherError(key, routeId, error, {
2121
+ flushSync
2122
+ });
2089
2123
  return;
2090
2124
  }
2091
2125
  let match = getTargetMatch(matches, path);
2092
2126
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2093
2127
  if (submission && isMutationMethod(submission.formMethod)) {
2094
- handleFetcherAction(key, routeId, path, match, matches, submission);
2128
+ handleFetcherAction(key, routeId, path, match, matches, flushSync, submission);
2095
2129
  return;
2096
2130
  }
2097
2131
  // Store off the match so we can call it's shouldRevalidate on subsequent
@@ -2100,11 +2134,11 @@ function createRouter(init) {
2100
2134
  routeId,
2101
2135
  path
2102
2136
  });
2103
- handleFetcherLoader(key, routeId, path, match, matches, submission);
2137
+ handleFetcherLoader(key, routeId, path, match, matches, flushSync, submission);
2104
2138
  }
2105
2139
  // Call the action for the matched fetcher.submit(), and then handle redirects,
2106
2140
  // errors, and revalidation
2107
- async function handleFetcherAction(key, routeId, path, match, requestMatches, submission) {
2141
+ async function handleFetcherAction(key, routeId, path, match, requestMatches, flushSync, submission) {
2108
2142
  interruptActiveLoads();
2109
2143
  fetchLoadMatches.delete(key);
2110
2144
  if (!match.route.action && !match.route.lazy) {
@@ -2113,15 +2147,15 @@ function createRouter(init) {
2113
2147
  pathname: path,
2114
2148
  routeId: routeId
2115
2149
  });
2116
- setFetcherError(key, routeId, error);
2150
+ setFetcherError(key, routeId, error, {
2151
+ flushSync
2152
+ });
2117
2153
  return;
2118
2154
  }
2119
2155
  // Put this fetcher into it's submitting state
2120
2156
  let existingFetcher = state.fetchers.get(key);
2121
- let fetcher = getSubmittingFetcher(submission, existingFetcher);
2122
- state.fetchers.set(key, fetcher);
2123
- updateState({
2124
- fetchers: new Map(state.fetchers)
2157
+ updateFetcherState(key, getSubmittingFetcher(submission, existingFetcher), {
2158
+ flushSync
2125
2159
  });
2126
2160
  // Call the action for the fetcher
2127
2161
  let abortController = new AbortController();
@@ -2130,13 +2164,19 @@ function createRouter(init) {
2130
2164
  let originatingLoadId = incrementingLoadId;
2131
2165
  let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename);
2132
2166
  if (fetchRequest.signal.aborted) {
2133
- // We can delete this so long as we weren't aborted by ou our own fetcher
2167
+ // We can delete this so long as we weren't aborted by our own fetcher
2134
2168
  // re-submit which would have put _new_ controller is in fetchControllers
2135
2169
  if (fetchControllers.get(key) === abortController) {
2136
2170
  fetchControllers.delete(key);
2137
2171
  }
2138
2172
  return;
2139
2173
  }
2174
+ if (deletedFetchers.has(key)) {
2175
+ updateFetcherState(key, getDoneFetcher(undefined), {
2176
+ flushSync
2177
+ });
2178
+ return;
2179
+ }
2140
2180
  if (isRedirectResult(actionResult)) {
2141
2181
  fetchControllers.delete(key);
2142
2182
  if (pendingNavigationLoadId > originatingLoadId) {
@@ -2144,18 +2184,14 @@ function createRouter(init) {
2144
2184
  // should take precedence over this redirect navigation. We already
2145
2185
  // set isRevalidationRequired so all loaders for the new route should
2146
2186
  // fire unless opted out via shouldRevalidate
2147
- let doneFetcher = getDoneFetcher(undefined);
2148
- state.fetchers.set(key, doneFetcher);
2149
- updateState({
2150
- fetchers: new Map(state.fetchers)
2187
+ updateFetcherState(key, getDoneFetcher(undefined), {
2188
+ flushSync
2151
2189
  });
2152
2190
  return;
2153
2191
  } else {
2154
2192
  fetchRedirectIds.add(key);
2155
- let loadingFetcher = getLoadingFetcher(submission);
2156
- state.fetchers.set(key, loadingFetcher);
2157
- updateState({
2158
- fetchers: new Map(state.fetchers)
2193
+ updateFetcherState(key, getLoadingFetcher(submission), {
2194
+ flushSync
2159
2195
  });
2160
2196
  return startRedirectNavigation(state, actionResult, {
2161
2197
  fetcherSubmission: submission
@@ -2164,7 +2200,9 @@ function createRouter(init) {
2164
2200
  }
2165
2201
  // Process any non-redirect errors thrown
2166
2202
  if (isErrorResult(actionResult)) {
2167
- setFetcherError(key, routeId, actionResult.error);
2203
+ setFetcherError(key, routeId, actionResult.error, {
2204
+ flushSync
2205
+ });
2168
2206
  return;
2169
2207
  }
2170
2208
  if (isDeferredResult(actionResult)) {
@@ -2204,6 +2242,8 @@ function createRouter(init) {
2204
2242
  });
2205
2243
  updateState({
2206
2244
  fetchers: new Map(state.fetchers)
2245
+ }, {
2246
+ flushSync
2207
2247
  });
2208
2248
  let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach(rf => abortFetcher(rf.key));
2209
2249
  abortController.signal.addEventListener("abort", abortPendingFetchRevalidations);
@@ -2262,18 +2302,17 @@ function createRouter(init) {
2262
2302
  errors,
2263
2303
  loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors),
2264
2304
  fetchers: new Map(state.fetchers)
2305
+ }, {
2306
+ flushSync
2265
2307
  });
2266
2308
  isRevalidationRequired = false;
2267
2309
  }
2268
2310
  }
2269
2311
  // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
2270
- async function handleFetcherLoader(key, routeId, path, match, matches, submission) {
2312
+ async function handleFetcherLoader(key, routeId, path, match, matches, flushSync, submission) {
2271
2313
  let existingFetcher = state.fetchers.get(key);
2272
- // Put this fetcher into it's loading state
2273
- let loadingFetcher = getLoadingFetcher(submission, existingFetcher ? existingFetcher.data : undefined);
2274
- state.fetchers.set(key, loadingFetcher);
2275
- updateState({
2276
- fetchers: new Map(state.fetchers)
2314
+ updateFetcherState(key, getLoadingFetcher(submission, existingFetcher ? existingFetcher.data : undefined), {
2315
+ flushSync
2277
2316
  });
2278
2317
  // Call the loader for this fetcher route match
2279
2318
  let abortController = new AbortController();
@@ -2296,15 +2335,19 @@ function createRouter(init) {
2296
2335
  if (fetchRequest.signal.aborted) {
2297
2336
  return;
2298
2337
  }
2338
+ if (deletedFetchers.has(key)) {
2339
+ updateFetcherState(key, getDoneFetcher(undefined), {
2340
+ flushSync
2341
+ });
2342
+ return;
2343
+ }
2299
2344
  // If the loader threw a redirect Response, start a new REPLACE navigation
2300
2345
  if (isRedirectResult(result)) {
2301
2346
  if (pendingNavigationLoadId > originatingLoadId) {
2302
2347
  // A new navigation was kicked off after our loader started, so that
2303
2348
  // should take precedence over this redirect navigation
2304
- let doneFetcher = getDoneFetcher(undefined);
2305
- state.fetchers.set(key, doneFetcher);
2306
- updateState({
2307
- fetchers: new Map(state.fetchers)
2349
+ updateFetcherState(key, getDoneFetcher(undefined), {
2350
+ flushSync
2308
2351
  });
2309
2352
  return;
2310
2353
  } else {
@@ -2315,25 +2358,15 @@ function createRouter(init) {
2315
2358
  }
2316
2359
  // Process any non-redirect errors thrown
2317
2360
  if (isErrorResult(result)) {
2318
- let boundaryMatch = findNearestBoundary(state.matches, routeId);
2319
- state.fetchers.delete(key);
2320
- // TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch -
2321
- // do we need to behave any differently with our non-redirect errors?
2322
- // What if it was a non-redirect Response?
2323
- updateState({
2324
- fetchers: new Map(state.fetchers),
2325
- errors: {
2326
- [boundaryMatch.route.id]: result.error
2327
- }
2361
+ setFetcherError(key, routeId, result.error, {
2362
+ flushSync
2328
2363
  });
2329
2364
  return;
2330
2365
  }
2331
2366
  invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
2332
2367
  // Put the fetcher back into an idle state
2333
- let doneFetcher = getDoneFetcher(result.data);
2334
- state.fetchers.set(key, doneFetcher);
2335
- updateState({
2336
- fetchers: new Map(state.fetchers)
2368
+ updateFetcherState(key, getDoneFetcher(result.data), {
2369
+ flushSync
2337
2370
  });
2338
2371
  }
2339
2372
  /**
@@ -2469,7 +2502,15 @@ function createRouter(init) {
2469
2502
  }
2470
2503
  });
2471
2504
  }
2472
- function setFetcherError(key, routeId, error) {
2505
+ function updateFetcherState(key, fetcher, opts) {
2506
+ state.fetchers.set(key, fetcher);
2507
+ updateState({
2508
+ fetchers: new Map(state.fetchers)
2509
+ }, {
2510
+ flushSync: opts.flushSync
2511
+ });
2512
+ }
2513
+ function setFetcherError(key, routeId, error, opts) {
2473
2514
  let boundaryMatch = findNearestBoundary(state.matches, routeId);
2474
2515
  deleteFetcher(key);
2475
2516
  updateState({
@@ -2477,8 +2518,21 @@ function createRouter(init) {
2477
2518
  [boundaryMatch.route.id]: error
2478
2519
  },
2479
2520
  fetchers: new Map(state.fetchers)
2521
+ }, {
2522
+ flushSync: opts.flushSync
2480
2523
  });
2481
2524
  }
2525
+ function getFetcher(key) {
2526
+ if (future.v7_fetcherPersist) {
2527
+ activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
2528
+ // If this fetcher was previously marked for deletion, unmark it since we
2529
+ // have a new instance
2530
+ if (deletedFetchers.has(key)) {
2531
+ deletedFetchers.delete(key);
2532
+ }
2533
+ }
2534
+ return state.fetchers.get(key) || IDLE_FETCHER;
2535
+ }
2482
2536
  function deleteFetcher(key) {
2483
2537
  let fetcher = state.fetchers.get(key);
2484
2538
  // Don't abort the controller if this is a deletion of a fetcher.submit()
@@ -2495,12 +2549,20 @@ function createRouter(init) {
2495
2549
  }
2496
2550
  function deleteFetcherAndUpdateState(key) {
2497
2551
  if (future.v7_fetcherPersist) {
2498
- deletedFetchers.add(key);
2552
+ let count = (activeFetchers.get(key) || 0) - 1;
2553
+ if (count <= 0) {
2554
+ activeFetchers.delete(key);
2555
+ deletedFetchers.add(key);
2556
+ } else {
2557
+ activeFetchers.set(key, count);
2558
+ }
2499
2559
  } else {
2500
2560
  deleteFetcher(key);
2501
2561
  }
2502
2562
  updateState({
2503
2563
  fetchers: new Map(state.fetchers)
2564
+ }, {
2565
+ flushSync: false
2504
2566
  });
2505
2567
  }
2506
2568
  function abortFetcher(key) {
@@ -2568,6 +2630,8 @@ function createRouter(init) {
2568
2630
  blockers.set(key, newBlocker);
2569
2631
  updateState({
2570
2632
  blockers
2633
+ }, {
2634
+ flushSync: false
2571
2635
  });
2572
2636
  }
2573
2637
  function shouldBlockNavigation(_ref2) {
@@ -2631,6 +2695,8 @@ function createRouter(init) {
2631
2695
  if (y != null) {
2632
2696
  updateState({
2633
2697
  restoreScrollPosition: y
2698
+ }, {
2699
+ flushSync: false
2634
2700
  });
2635
2701
  }
2636
2702
  }