react-router-dom 6.18.0 → 6.19.0-pre.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # `react-router-dom`
2
2
 
3
+ ## 6.19.0-pre.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add `unstable_flushSync` option to `useNavigate`/`useSumbit`/`fetcher.load`/`fetcher.submit` to opt-out of `React.startTransition` and into `ReactDOM.flushSync` for state updates ([#11005](https://github.com/remix-run/react-router/pull/11005))
8
+ - Allow `unstable_usePrompt` to accept a `BlockerFunction` in addition to a `boolean` ([#10991](https://github.com/remix-run/react-router/pull/10991))
9
+
10
+ ### Patch Changes
11
+
12
+ - Fix issue where a changing fetcher `key` in a `useFetcher` that remains mounted wasn't getting picked up ([#11009](https://github.com/remix-run/react-router/pull/11009))
13
+ - Fix `useFormAction` which was incorrectly inheriting the `?index` query param from child route `action` submissions ([#11025](https://github.com/remix-run/react-router/pull/11025))
14
+ - Fix `NavLink` `active` logic when `to` location has a trailing slash ([#10734](https://github.com/remix-run/react-router/pull/10734))
15
+ - Updated dependencies:
16
+ - `react-router@6.19.0-pre.0`
17
+ - `@remix-run/router@1.12.0-pre.0`
18
+
3
19
  ## 6.18.0
4
20
 
5
21
  ### Minor Changes
package/dist/dom.d.ts CHANGED
@@ -85,6 +85,10 @@ export interface SubmitOptions {
85
85
  * navigation when using the <ScrollRestoration> component
86
86
  */
87
87
  preventScrollReset?: boolean;
88
+ /**
89
+ * Enable flushSync for this navigation's state updates
90
+ */
91
+ unstable_flushSync?: boolean;
88
92
  /**
89
93
  * Enable view transitions on this submission navigation
90
94
  */
package/dist/index.d.ts CHANGED
@@ -4,13 +4,13 @@
4
4
  */
5
5
  import * as React from "react";
6
6
  import type { FutureConfig, Location, NavigateOptions, RelativeRoutingType, RouteObject, RouterProviderProps, To } from "react-router";
7
- import type { Fetcher, FormEncType, FormMethod, FutureConfig as RouterFutureConfig, GetScrollRestorationKeyFunction, History, HTMLFormMethod, HydrationState, Router as RemixRouter, V7_FormMethod } from "@remix-run/router";
7
+ import type { Fetcher, FormEncType, FormMethod, FutureConfig as RouterFutureConfig, GetScrollRestorationKeyFunction, History, HTMLFormMethod, HydrationState, Router as RemixRouter, V7_FormMethod, BlockerFunction } from "@remix-run/router";
8
8
  import type { SubmitOptions, ParamKeyValuePair, URLSearchParamsInit, SubmitTarget } from "./dom";
9
9
  import { createSearchParams } from "./dom";
10
10
  export type { FormEncType, FormMethod, GetScrollRestorationKeyFunction, ParamKeyValuePair, SubmitOptions, URLSearchParamsInit, V7_FormMethod, };
11
11
  export { createSearchParams };
12
12
  export type { ActionFunction, ActionFunctionArgs, AwaitProps, unstable_Blocker, unstable_BlockerFunction, DataRouteMatch, DataRouteObject, ErrorResponse, Fetcher, Hash, IndexRouteObject, IndexRouteProps, JsonFunction, LazyRouteFunction, LayoutRouteProps, LoaderFunction, LoaderFunctionArgs, Location, MemoryRouterProps, NavigateFunction, NavigateOptions, NavigateProps, Navigation, Navigator, NonIndexRouteObject, OutletProps, Params, ParamParseKey, Path, PathMatch, Pathname, PathPattern, PathRouteProps, RedirectFunction, RelativeRoutingType, RouteMatch, RouteObject, RouteProps, RouterProps, RouterProviderProps, RoutesProps, Search, ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, To, UIMatch, } from "react-router";
13
- export { AbortedDeferredError, Await, MemoryRouter, Navigate, NavigationType, Outlet, Route, Router, Routes, createMemoryRouter, createPath, createRoutesFromChildren, createRoutesFromElements, defer, isRouteErrorResponse, generatePath, json, matchPath, matchRoutes, parsePath, redirect, redirectDocument, renderMatches, resolvePath, useActionData, useAsyncError, useAsyncValue, unstable_useBlocker, useHref, useInRouterContext, useLoaderData, useLocation, useMatch, useMatches, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRouteLoaderData, useRoutes, } from "react-router";
13
+ export { AbortedDeferredError, Await, MemoryRouter, Navigate, NavigationType, Outlet, Route, Router, Routes, createMemoryRouter, createPath, createRoutesFromChildren, createRoutesFromElements, defer, isRouteErrorResponse, generatePath, json, matchPath, matchRoutes, parsePath, redirect, redirectDocument, renderMatches, resolvePath, useActionData, useAsyncError, useAsyncValue, useBlocker, useHref, useInRouterContext, useLoaderData, useLocation, useMatch, useMatches, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRouteLoaderData, useRoutes, } from "react-router";
14
14
  /** @internal */
15
15
  export { UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext, UNSAFE_NavigationContext, UNSAFE_LocationContext, UNSAFE_RouteContext, UNSAFE_useRouteId, } from "react-router";
16
16
  declare global {
@@ -31,6 +31,7 @@ type ViewTransitionContextObject = {
31
31
  isTransitioning: false;
32
32
  } | {
33
33
  isTransitioning: true;
34
+ flushSync: boolean;
34
35
  currentLocation: Location;
35
36
  nextLocation: Location;
36
37
  };
@@ -111,7 +112,6 @@ export interface NavLinkProps extends Omit<LinkProps, "className" | "style" | "c
111
112
  className?: string | ((props: NavLinkRenderProps) => string | undefined);
112
113
  end?: boolean;
113
114
  style?: React.CSSProperties | ((props: NavLinkRenderProps) => React.CSSProperties | undefined);
114
- unstable_viewTransition?: boolean;
115
115
  }
116
116
  /**
117
117
  * A `<Link>` wrapper that knows if it's "active" or not.
@@ -252,7 +252,9 @@ export declare function useFormAction(action?: string, { relative }?: {
252
252
  export type FetcherWithComponents<TData> = Fetcher<TData> & {
253
253
  Form: React.ForwardRefExoticComponent<FetcherFormProps & React.RefAttributes<HTMLFormElement>>;
254
254
  submit: FetcherSubmitFunction;
255
- load: (href: string) => void;
255
+ load: (href: string, opts?: {
256
+ unstable_flushSync?: boolean;
257
+ }) => void;
256
258
  };
257
259
  /**
258
260
  * Interacts with route loaders and actions without causing a navigation. Great
@@ -295,8 +297,8 @@ export declare function useBeforeUnload(callback: (event: BeforeUnloadEvent) =>
295
297
  * very incorrectly in some cases) across browsers if user click addition
296
298
  * back/forward navigations while the confirm is open. Use at your own risk.
297
299
  */
298
- declare function usePrompt({ when, message }: {
299
- when: boolean;
300
+ declare function usePrompt({ when, message, }: {
301
+ when: boolean | BlockerFunction;
300
302
  message: string;
301
303
  }): void;
302
304
  export { usePrompt as unstable_usePrompt };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * React Router DOM v6.18.0
2
+ * React Router DOM v6.19.0-pre.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -9,8 +9,9 @@
9
9
  * @license MIT
10
10
  */
11
11
  import * as React from 'react';
12
- import { UNSAFE_mapRouteProperties, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext, Router, UNSAFE_useRoutesImpl, UNSAFE_NavigationContext, useHref, useResolvedPath, useLocation, useNavigate, createPath, UNSAFE_useRouteId, UNSAFE_RouteContext, useMatches, useNavigation, unstable_useBlocker } from 'react-router';
13
- export { AbortedDeferredError, Await, MemoryRouter, Navigate, NavigationType, Outlet, Route, Router, Routes, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext, UNSAFE_LocationContext, UNSAFE_NavigationContext, UNSAFE_RouteContext, UNSAFE_useRouteId, createMemoryRouter, createPath, createRoutesFromChildren, createRoutesFromElements, defer, generatePath, isRouteErrorResponse, json, matchPath, matchRoutes, parsePath, redirect, redirectDocument, renderMatches, resolvePath, unstable_useBlocker, useActionData, useAsyncError, useAsyncValue, useHref, useInRouterContext, useLoaderData, useLocation, useMatch, useMatches, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRouteLoaderData, useRoutes } from 'react-router';
12
+ import * as ReactDOM from 'react-dom';
13
+ import { UNSAFE_mapRouteProperties, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext, Router, UNSAFE_useRoutesImpl, UNSAFE_NavigationContext, useHref, useResolvedPath, useLocation, useNavigate, createPath, UNSAFE_useRouteId, UNSAFE_RouteContext, useMatches, useNavigation, useBlocker } from 'react-router';
14
+ export { AbortedDeferredError, Await, MemoryRouter, Navigate, NavigationType, Outlet, Route, Router, Routes, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext, UNSAFE_LocationContext, UNSAFE_NavigationContext, UNSAFE_RouteContext, UNSAFE_useRouteId, createMemoryRouter, createPath, createRoutesFromChildren, createRoutesFromElements, defer, generatePath, isRouteErrorResponse, json, matchPath, matchRoutes, parsePath, redirect, redirectDocument, renderMatches, resolvePath, useActionData, useAsyncError, useAsyncValue, useBlocker, useHref, useInRouterContext, useLoaderData, useLocation, useMatch, useMatches, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRouteLoaderData, useRoutes } from 'react-router';
14
15
  import { stripBasename, UNSAFE_warning, createRouter, createBrowserHistory, createHashHistory, UNSAFE_ErrorResponseImpl, UNSAFE_invariant, joinPaths, IDLE_FETCHER, matchPath } from '@remix-run/router';
15
16
 
16
17
  function _extends() {
@@ -325,6 +326,8 @@ if (process.env.NODE_ENV !== "production") {
325
326
  */
326
327
  const START_TRANSITION = "startTransition";
327
328
  const startTransitionImpl = React[START_TRANSITION];
329
+ const FLUSH_SYNC = "flushSync";
330
+ const flushSyncImpl = ReactDOM[FLUSH_SYNC];
328
331
  function startTransitionSafe(cb) {
329
332
  if (startTransitionImpl) {
330
333
  startTransitionImpl(cb);
@@ -332,6 +335,13 @@ function startTransitionSafe(cb) {
332
335
  cb();
333
336
  }
334
337
  }
338
+ function flushSyncSafe(cb) {
339
+ if (flushSyncImpl) {
340
+ flushSyncImpl(cb);
341
+ } else {
342
+ cb();
343
+ }
344
+ }
335
345
  class Deferred {
336
346
  constructor() {
337
347
  this.status = "pending";
@@ -382,6 +392,7 @@ function RouterProvider(_ref) {
382
392
  let setState = React.useCallback((newState, _ref2) => {
383
393
  let {
384
394
  deletedFetchers,
395
+ unstable_flushSync: flushSync,
385
396
  unstable_viewTransitionOpts: viewTransitionOpts
386
397
  } = _ref2;
387
398
  deletedFetchers.forEach(key => fetcherData.current.delete(key));
@@ -390,13 +401,56 @@ function RouterProvider(_ref) {
390
401
  fetcherData.current.set(key, fetcher.data);
391
402
  }
392
403
  });
393
- if (!viewTransitionOpts || router.window == null || typeof router.window.document.startViewTransition !== "function") {
394
- // Mid-navigation state update, or startViewTransition isn't available
395
- optInStartTransition(() => setStateImpl(newState));
396
- } else if (transition && renderDfd) {
404
+ let isViewTransitionUnavailable = router.window == null || typeof router.window.document.startViewTransition !== "function";
405
+ // If this isn't a view transition or it's not available in this browser,
406
+ // just update and be done with it
407
+ if (!viewTransitionOpts || isViewTransitionUnavailable) {
408
+ if (flushSync) {
409
+ flushSyncSafe(() => setStateImpl(newState));
410
+ } else {
411
+ optInStartTransition(() => setStateImpl(newState));
412
+ }
413
+ return;
414
+ }
415
+ // flushSync + startViewTransition
416
+ if (flushSync) {
417
+ // Flush through the context to mark DOM elements as transition=ing
418
+ flushSyncSafe(() => {
419
+ // Cancel any pending transitions
420
+ if (transition) {
421
+ renderDfd && renderDfd.resolve();
422
+ transition.skipTransition();
423
+ }
424
+ setVtContext({
425
+ isTransitioning: true,
426
+ flushSync: true,
427
+ currentLocation: viewTransitionOpts.currentLocation,
428
+ nextLocation: viewTransitionOpts.nextLocation
429
+ });
430
+ });
431
+ // Update the DOM
432
+ let t = router.window.document.startViewTransition(() => {
433
+ flushSyncSafe(() => setStateImpl(newState));
434
+ });
435
+ // Clean up after the animation completes
436
+ t.finished.finally(() => {
437
+ flushSyncSafe(() => {
438
+ setRenderDfd(undefined);
439
+ setTransition(undefined);
440
+ setPendingState(undefined);
441
+ setVtContext({
442
+ isTransitioning: false
443
+ });
444
+ });
445
+ });
446
+ flushSyncSafe(() => setTransition(t));
447
+ return;
448
+ }
449
+ // startTransition + startViewTransition
450
+ if (transition) {
397
451
  // Interrupting an in-progress transition, cancel and let everything flush
398
452
  // out, and then kick off a new transition from the interruption state
399
- renderDfd.resolve();
453
+ renderDfd && renderDfd.resolve();
400
454
  transition.skipTransition();
401
455
  setInterruption({
402
456
  state: newState,
@@ -408,6 +462,7 @@ function RouterProvider(_ref) {
408
462
  setPendingState(newState);
409
463
  setVtContext({
410
464
  isTransitioning: true,
465
+ flushSync: false,
411
466
  currentLocation: viewTransitionOpts.currentLocation,
412
467
  nextLocation: viewTransitionOpts.nextLocation
413
468
  });
@@ -419,10 +474,10 @@ function RouterProvider(_ref) {
419
474
  // When we start a view transition, create a Deferred we can use for the
420
475
  // eventual "completed" render
421
476
  React.useEffect(() => {
422
- if (vtContext.isTransitioning) {
477
+ if (vtContext.isTransitioning && !vtContext.flushSync) {
423
478
  setRenderDfd(new Deferred());
424
479
  }
425
- }, [vtContext.isTransitioning]);
480
+ }, [vtContext]);
426
481
  // Once the deferred is created, kick off startViewTransition() to update the
427
482
  // DOM and then wait on the Deferred to resolve (indicating the DOM update has
428
483
  // happened)
@@ -459,6 +514,7 @@ function RouterProvider(_ref) {
459
514
  setPendingState(interruption.state);
460
515
  setVtContext({
461
516
  isTransitioning: true,
517
+ flushSync: false,
462
518
  currentLocation: interruption.currentLocation,
463
519
  nextLocation: interruption.nextLocation
464
520
  });
@@ -741,7 +797,13 @@ const NavLink = /*#__PURE__*/React.forwardRef(function NavLinkWithRef(_ref8, ref
741
797
  nextLocationPathname = nextLocationPathname ? nextLocationPathname.toLowerCase() : null;
742
798
  toPathname = toPathname.toLowerCase();
743
799
  }
744
- let isActive = locationPathname === toPathname || !end && locationPathname.startsWith(toPathname) && locationPathname.charAt(toPathname.length) === "/";
800
+ // If the `to` has a trailing slash, look at that exact spot. Otherwise,
801
+ // we're looking for a slash _after_ what's in `to`. For example:
802
+ //
803
+ // <NavLink to="/users"> and <NavLink to="/users/">
804
+ // both want to look for a / at index 6 to match URL `/users/matt`
805
+ const endSlashPosition = toPathname !== "/" && toPathname.endsWith("/") ? toPathname.length - 1 : toPathname.length;
806
+ let isActive = locationPathname === toPathname || !end && locationPathname.startsWith(toPathname) && locationPathname.charAt(endSlashPosition) === "/";
745
807
  let isPending = nextLocationPathname != null && (nextLocationPathname === toPathname || !end && nextLocationPathname.startsWith(toPathname) && nextLocationPathname.charAt(toPathname.length) === "/");
746
808
  let renderProps = {
747
809
  isActive,
@@ -972,7 +1034,8 @@ function useSubmit() {
972
1034
  formData,
973
1035
  body,
974
1036
  formMethod: options.method || method,
975
- formEncType: options.encType || encType
1037
+ formEncType: options.encType || encType,
1038
+ unstable_flushSync: options.unstable_flushSync
976
1039
  });
977
1040
  } else {
978
1041
  router.navigate(options.action || action, {
@@ -984,6 +1047,7 @@ function useSubmit() {
984
1047
  replace: options.replace,
985
1048
  state: options.state,
986
1049
  fromRouteId: currentRouteId,
1050
+ unstable_flushSync: options.unstable_flushSync,
987
1051
  unstable_viewTransition: options.unstable_viewTransition
988
1052
  });
989
1053
  }
@@ -1006,21 +1070,19 @@ function useFormAction(action, _temp2) {
1006
1070
  let path = _extends({}, useResolvedPath(action ? action : ".", {
1007
1071
  relative
1008
1072
  }));
1009
- // Previously we set the default action to ".". The problem with this is that
1010
- // `useResolvedPath(".")` excludes search params of the resolved URL. This is
1011
- // the intended behavior of when "." is specifically provided as
1012
- // the form action, but inconsistent w/ browsers when the action is omitted.
1073
+ // If no action was specified, browsers will persist current search params
1074
+ // when determining the path, so match that behavior
1013
1075
  // https://github.com/remix-run/remix/issues/927
1014
1076
  let location = useLocation();
1015
1077
  if (action == null) {
1016
1078
  // Safe to write to this directly here since if action was undefined, we
1017
1079
  // would have called useResolvedPath(".") which will never include a search
1018
1080
  path.search = location.search;
1019
- // When grabbing search params from the URL, remove the automatically
1020
- // inserted ?index param so we match the useResolvedPath search behavior
1021
- // which would not include ?index
1022
- if (match.route.index) {
1023
- let params = new URLSearchParams(path.search);
1081
+ // When grabbing search params from the URL, remove any included ?index param
1082
+ // since it might not apply to our contextual route. We add it back based
1083
+ // on match.route.index below
1084
+ let params = new URLSearchParams(path.search);
1085
+ if (params.has("index") && params.get("index") === "") {
1024
1086
  params.delete("index");
1025
1087
  path.search = params.toString() ? "?" + params.toString() : "";
1026
1088
  }
@@ -1059,7 +1121,9 @@ function useFetcher(_temp3) {
1059
1121
  !(routeId != null) ? process.env.NODE_ENV !== "production" ? UNSAFE_invariant(false, "useFetcher can only be used on routes that contain a unique \"id\"") : UNSAFE_invariant(false) : void 0;
1060
1122
  // Fetcher key handling
1061
1123
  let [fetcherKey, setFetcherKey] = React.useState(key || "");
1062
- if (!fetcherKey) {
1124
+ if (key && key !== fetcherKey) {
1125
+ setFetcherKey(key);
1126
+ } else if (!fetcherKey) {
1063
1127
  setFetcherKey(getUniqueFetcherId());
1064
1128
  }
1065
1129
  // Registration/cleanup
@@ -1073,9 +1137,9 @@ function useFetcher(_temp3) {
1073
1137
  };
1074
1138
  }, [router, fetcherKey]);
1075
1139
  // Fetcher additions
1076
- let load = React.useCallback(href => {
1140
+ let load = React.useCallback((href, opts) => {
1077
1141
  !routeId ? process.env.NODE_ENV !== "production" ? UNSAFE_invariant(false, "No routeId available for fetcher.load()") : UNSAFE_invariant(false) : void 0;
1078
- router.fetch(fetcherKey, routeId, href);
1142
+ router.fetch(fetcherKey, routeId, href, opts);
1079
1143
  }, [fetcherKey, routeId, router]);
1080
1144
  let submitImpl = useSubmit();
1081
1145
  let submit = React.useCallback((target, opts) => {
@@ -1274,7 +1338,7 @@ function usePrompt(_ref12) {
1274
1338
  when,
1275
1339
  message
1276
1340
  } = _ref12;
1277
- let blocker = unstable_useBlocker(when);
1341
+ let blocker = useBlocker(when);
1278
1342
  React.useEffect(() => {
1279
1343
  if (blocker.state === "blocked") {
1280
1344
  let proceed = window.confirm(message);