+- Export `NavLinkRenderProps` type for easier typing of custom `NavLink` callback ([#11553](https://github.com/remix-run/react-router/pull/11553))
+- Updated dependencies:
+ - `@remix-run/router@1.17.1`
+ - `react-router@6.24.1`
+
## 6.24.0
### Minor Changes
diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx
index 0718b318c0..77e6727e9e 100644
--- a/packages/react-router-dom/index.tsx
+++ b/packages/react-router-dom/index.tsx
@@ -1024,7 +1024,7 @@ if (__DEV__) {
Link.displayName = "Link";
}
-type NavLinkRenderProps = {
+export type NavLinkRenderProps = {
isActive: boolean;
isPending: boolean;
isTransitioning: boolean;
@@ -1464,11 +1464,7 @@ export function useSearchParams(
`You cannot use the \`useSearchParams\` hook in a browser that does not ` +
`support the URLSearchParams API. If you need to support Internet ` +
`Explorer 11, we recommend you load a polyfill such as ` +
- `https://github.com/ungap/url-search-params\n\n` +
- `If you're unsure how to load polyfills, we recommend you check out ` +
- `https://polyfill.io/v3/ which provides some recommendations about how ` +
- `to load polyfills only for users that need them, instead of for every ` +
- `user.`
+ `https://github.com/ungap/url-search-params.`
);
let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit));
diff --git a/packages/react-router-dom/package.json b/packages/react-router-dom/package.json
index 510f8e5b18..007b2a03ed 100644
--- a/packages/react-router-dom/package.json
+++ b/packages/react-router-dom/package.json
@@ -1,6 +1,6 @@
{
"name": "react-router-dom",
- "version": "6.24.0",
+ "version": "6.24.1",
"description": "Declarative routing for React web applications",
"keywords": [
"react",
diff --git a/packages/react-router-native/CHANGELOG.md b/packages/react-router-native/CHANGELOG.md
index 6ef1db310b..b0d792e82d 100644
--- a/packages/react-router-native/CHANGELOG.md
+++ b/packages/react-router-native/CHANGELOG.md
@@ -1,5 +1,12 @@
# `react-router-native`
+## 6.24.1
+
+### Patch Changes
+
+- Updated dependencies:
+ - `react-router@6.24.1`
+
## 6.24.0
### Patch Changes
diff --git a/packages/react-router-native/package.json b/packages/react-router-native/package.json
index 9582990289..02536385b4 100644
--- a/packages/react-router-native/package.json
+++ b/packages/react-router-native/package.json
@@ -1,6 +1,6 @@
{
"name": "react-router-native",
- "version": "6.24.0",
+ "version": "6.24.1",
"description": "Declarative routing for React Native applications",
"keywords": [
"react",
diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md
index 84b6b33fa8..aae5d0795f 100644
--- a/packages/react-router/CHANGELOG.md
+++ b/packages/react-router/CHANGELOG.md
@@ -1,5 +1,13 @@
# `react-router`
+## 6.24.1
+
+### Patch Changes
+
+- When using `future.v7_relativeSplatPath`, properly resolve relative paths in splat routes that are children of pathless routes ([#11633](https://github.com/remix-run/react-router/pull/11633))
+- Updated dependencies:
+ - `@remix-run/router@1.17.1`
+
## 6.24.0
### Minor Changes
diff --git a/packages/react-router/__tests__/useResolvedPath-test.tsx b/packages/react-router/__tests__/useResolvedPath-test.tsx
index e9ca3ac63e..729503a8fa 100644
--- a/packages/react-router/__tests__/useResolvedPath-test.tsx
+++ b/packages/react-router/__tests__/useResolvedPath-test.tsx
@@ -420,6 +420,43 @@ describe("useResolvedPath", () => {
"
`);
});
+
+ // gh-issue #11629
+ it("when enabled, '.' resolves to the current path including any splat paths nested in pathless routes", () => {
+ let { container } = render(
+
+
+
+
+
+ }
+ />
+
+
+
+
+ );
+ let html = getHtml(container);
+ html = html ? html.replace(/</g, "<").replace(/>/g, ">") : html;
+ expect(html).toMatchInlineSnapshot(`
+ "
+ --- Routes: ---
+ useLocation(): /foo/bar
+ useResolvedPath('.'): /foo/bar
+ useResolvedPath('..'): /foo
+ useResolvedPath('..', { relative: 'path' }): /foo
+ useResolvedPath('baz/qux'): /foo/bar/baz/qux
+ useResolvedPath('./baz/qux'): /foo/bar/baz/qux
+
+
"
+ `);
+ });
});
});
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index 021a6a1209..812f3e0c59 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -1,6 +1,6 @@
{
"name": "react-router",
- "version": "6.24.0",
+ "version": "6.24.1",
"description": "Declarative routing for React",
"keywords": [
"react",
diff --git a/packages/router/CHANGELOG.md b/packages/router/CHANGELOG.md
index 850224768f..0dece7e675 100644
--- a/packages/router/CHANGELOG.md
+++ b/packages/router/CHANGELOG.md
@@ -1,5 +1,12 @@
# `@remix-run/router`
+## 1.17.1
+
+### Patch Changes
+
+- Fog of War (unstable): Trigger a new `router.routes` identity/reflow during route patching ([#11740](https://github.com/remix-run/react-router/pull/11740))
+- Fog of War (unstable): Fix initial matching when a splat route matches ([#11759](https://github.com/remix-run/react-router/pull/11759))
+
## 1.17.0
### Minor Changes
diff --git a/packages/router/__tests__/lazy-discovery-test.ts b/packages/router/__tests__/lazy-discovery-test.ts
index 690a586014..0ae50b9fcc 100644
--- a/packages/router/__tests__/lazy-discovery-test.ts
+++ b/packages/router/__tests__/lazy-discovery-test.ts
@@ -541,6 +541,34 @@ describe("Lazy Route Discovery (Fog of War)", () => {
expect(router.state.matches.map((m) => m.route.id)).toEqual(["a", "b"]);
});
+ it("de-prioritizes splat routes in favor of looking for better async matches (splat/*)", async () => {
+ router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ {
+ path: "/",
+ },
+ {
+ id: "splat",
+ path: "/splat/*",
+ },
+ ],
+ async unstable_patchRoutesOnMiss({ matches, patch }) {
+ await tick();
+ patch(null, [
+ {
+ id: "static",
+ path: "/splat/static",
+ },
+ ]);
+ },
+ });
+
+ await router.navigate("/splat/static");
+ expect(router.state.location.pathname).toBe("/splat/static");
+ expect(router.state.matches.map((m) => m.route.id)).toEqual(["static"]);
+ });
+
it("matches splats when other paths don't pan out", async () => {
router = createRouter({
history: createMemoryHistory(),
@@ -628,6 +656,39 @@ describe("Lazy Route Discovery (Fog of War)", () => {
]);
});
+ it("discovers routes during initial hydration when a splat route matches", async () => {
+ let childrenDfd = createDeferred();
+
+ router = createRouter({
+ history: createMemoryHistory({ initialEntries: ["/test"] }),
+ routes: [
+ {
+ path: "/",
+ },
+ {
+ path: "*",
+ },
+ ],
+ async unstable_patchRoutesOnMiss({ path, patch, matches }) {
+ let children = await childrenDfd.promise;
+ patch(null, children);
+ },
+ });
+ router.initialize();
+ expect(router.state.initialized).toBe(false);
+
+ childrenDfd.resolve([
+ {
+ id: "test",
+ path: "/test",
+ },
+ ]);
+ await tick();
+ expect(router.state.initialized).toBe(true);
+ expect(router.state.location.pathname).toBe("/test");
+ expect(router.state.matches.map((m) => m.route.id)).toEqual(["test"]);
+ });
+
it("discovers new root routes", async () => {
let childrenDfd = createDeferred();
let childLoaderDfd = createDeferred();
@@ -737,7 +798,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
let childLoaderDfd = createDeferred();
router = createRouter({
- history: createMemoryHistory(),
+ history: createMemoryHistory({ initialEntries: ["/other"] }),
routes: [
{
id: "other",
@@ -835,6 +896,80 @@ describe("Lazy Route Discovery (Fog of War)", () => {
]);
});
+ it("creates a new router.routes identity when patching routes", async () => {
+ let childrenDfd = createDeferred();
+
+ router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ {
+ path: "/",
+ },
+ {
+ id: "parent",
+ path: "parent",
+ },
+ ],
+ async unstable_patchRoutesOnMiss({ patch }) {
+ let children = await childrenDfd.promise;
+ patch("parent", children);
+ },
+ });
+ let originalRoutes = router.routes;
+
+ router.navigate("/parent/child");
+ childrenDfd.resolve([
+ {
+ id: "child",
+ path: "child",
+ },
+ ]);
+ await tick();
+
+ expect(router.state.location.pathname).toBe("/parent/child");
+ expect(router.state.matches.map((m) => m.route.id)).toEqual([
+ "parent",
+ "child",
+ ]);
+
+ expect(router.routes).not.toBe(originalRoutes);
+ });
+
+ it("allows patching externally/eagerly and triggers a reflow", async () => {
+ router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ {
+ path: "/",
+ },
+ {
+ id: "parent",
+ path: "parent",
+ },
+ ],
+ });
+ let spy = jest.fn();
+ let unsubscribe = router.subscribe(spy);
+ let originalRoutes = router.routes;
+ router.patchRoutes("parent", [
+ {
+ id: "child",
+ path: "child",
+ },
+ ]);
+ expect(spy).toHaveBeenCalled();
+ expect(router.routes).not.toBe(originalRoutes);
+
+ await router.navigate("/parent/child");
+ expect(router.state.location.pathname).toBe("/parent/child");
+ expect(router.state.matches.map((m) => m.route.id)).toEqual([
+ "parent",
+ "child",
+ ]);
+
+ unsubscribe();
+ });
+
describe("errors", () => {
it("lazy 404s (GET navigation)", async () => {
let childrenDfd = createDeferred();
diff --git a/packages/router/package.json b/packages/router/package.json
index b58c4435de..a133c23194 100644
--- a/packages/router/package.json
+++ b/packages/router/package.json
@@ -1,6 +1,6 @@
{
"name": "@remix-run/router",
- "version": "1.17.0",
+ "version": "1.17.1",
"description": "Nested/Data-driven/Framework-agnostic Routing",
"keywords": [
"remix",
diff --git a/packages/router/router.ts b/packages/router/router.ts
index 9fbca292c0..e6c0ff03b6 100644
--- a/packages/router/router.ts
+++ b/packages/router/router.ts
@@ -249,7 +249,8 @@ export interface Router {
* PRIVATE DO NOT USE
*
* Patch additional children routes into an existing parent route
- * @param routeId The parent route id
+ * @param routeId The parent route id or a callback function accepting `patch`
+ * to perform batch patching
* @param children The additional children routes
*/
patchRoutes(routeId: string | null, children: AgnosticRouteObject[]): void;
@@ -840,6 +841,20 @@ export function createRouter(init: RouterInit): Router {
initialErrors = { [route.id]: error };
}
+ // If the user provided a patchRoutesOnMiss implementation and our initial
+ // match is a splat route, clear them out so we run through lazy discovery
+ // on hydration in case there's a more accurate lazy route match
+ if (initialMatches && patchRoutesOnMissImpl) {
+ let fogOfWar = checkFogOfWar(
+ initialMatches,
+ dataRoutes,
+ init.history.location.pathname
+ );
+ if (fogOfWar.active) {
+ initialMatches = null;
+ }
+ }
+
let initialized: boolean;
if (!initialMatches) {
// We need to run patchRoutesOnMiss in initialize()
@@ -1222,6 +1237,7 @@ export function createRouter(init: RouterInit): Router {
isMutationMethod(state.navigation.formMethod) &&
location.state?._isRedirect !== true);
+ // Commit any in-flight routes at the end of the HMR revalidation "navigation"
if (inFlightDataRoutes) {
dataRoutes = inFlightDataRoutes;
inFlightDataRoutes = undefined;
@@ -3160,7 +3176,10 @@ export function createRouter(init: RouterInit): Router {
return { active: true, matches: fogMatches || [] };
} else {
let leafRoute = matches[matches.length - 1].route;
- if (leafRoute.path === "*") {
+ if (
+ leafRoute.path &&
+ (leafRoute.path === "*" || leafRoute.path.endsWith("/*"))
+ ) {
// If we matched a splat, it might only be because we haven't yet fetched
// the children that would match with a higher score, so let's fetch
// around and find out
@@ -3204,12 +3223,14 @@ export function createRouter(init: RouterInit): Router {
? partialMatches[partialMatches.length - 1].route
: null;
while (true) {
+ let isNonHMR = inFlightDataRoutes == null;
+ let routesToUse = inFlightDataRoutes || dataRoutes;
try {
await loadLazyRouteChildren(
patchRoutesOnMissImpl!,
pathname,
partialMatches,
- dataRoutes || inFlightDataRoutes,
+ routesToUse,
manifest,
mapRouteProperties,
pendingPatchRoutes,
@@ -3217,13 +3238,22 @@ export function createRouter(init: RouterInit): Router {
);
} catch (e) {
return { type: "error", error: e, partialMatches };
+ } finally {
+ // If we are not in the middle of an HMR revalidation and we changed the
+ // routes, provide a new identity so when we `updateState` at the end of
+ // this navigation/fetch `router.routes` will be a new identity and
+ // trigger a re-run of memoized `router.routes` dependencies.
+ // HMR will already update the identity and reflow when it lands
+ // `inFlightDataRoutes` in `completeNavigation`
+ if (isNonHMR) {
+ dataRoutes = [...dataRoutes];
+ }
}
if (signal.aborted) {
return { type: "aborted" };
}
- let routesToUse = inFlightDataRoutes || dataRoutes;
let newMatches = matchRoutes(routesToUse, pathname, basename);
let matchedSplat = false;
if (newMatches) {
@@ -3284,6 +3314,31 @@ export function createRouter(init: RouterInit): Router {
);
}
+ function patchRoutes(
+ routeId: string | null,
+ children: AgnosticRouteObject[]
+ ): void {
+ let isNonHMR = inFlightDataRoutes == null;
+ let routesToUse = inFlightDataRoutes || dataRoutes;
+ patchRoutesImpl(
+ routeId,
+ children,
+ routesToUse,
+ manifest,
+ mapRouteProperties
+ );
+
+ // If we are not in the middle of an HMR revalidation and we changed the
+ // routes, provide a new identity and trigger a reflow via `updateState`
+ // to re-run memoized `router.routes` dependencies.
+ // HMR will already update the identity and reflow when it lands
+ // `inFlightDataRoutes` in `completeNavigation`
+ if (isNonHMR) {
+ dataRoutes = [...dataRoutes];
+ updateState({});
+ }
+ }
+
router = {
get basename() {
return basename;
@@ -3315,15 +3370,7 @@ export function createRouter(init: RouterInit): Router {
dispose,
getBlocker,
deleteBlocker,
- patchRoutes(routeId, children) {
- return patchRoutes(
- routeId,
- children,
- dataRoutes || inFlightDataRoutes,
- manifest,
- mapRouteProperties
- );
- },
+ patchRoutes,
_internalFetchControllers: fetchControllers,
_internalActiveDeferreds: activeDeferreds,
// TODO: Remove setRoutes, it's temporary to avoid dealing with
@@ -4488,7 +4535,7 @@ function shouldRevalidateLoader(
}
/**
- * Idempotent utility to execute route.children() method to lazily load route
+ * Idempotent utility to execute patchRoutesOnMiss() to lazily load route
* definitions and update the routes/routeManifest
*/
async function loadLazyRouteChildren(
@@ -4510,7 +4557,7 @@ async function loadLazyRouteChildren(
matches,
patch: (routeId, children) => {
if (!signal.aborted) {
- patchRoutes(
+ patchRoutesImpl(
routeId,
children,
routes,
@@ -4531,10 +4578,10 @@ async function loadLazyRouteChildren(
}
}
-function patchRoutes(
+function patchRoutesImpl(
routeId: string | null,
children: AgnosticRouteObject[],
- routes: AgnosticDataRouteObject[],
+ routesToUse: AgnosticDataRouteObject[],
manifest: RouteManifest,
mapRouteProperties: MapRoutePropertiesFunction
) {
@@ -4559,10 +4606,10 @@ function patchRoutes(
let dataChildren = convertRoutesToDataRoutes(
children,
mapRouteProperties,
- ["patch", String(routes.length || "0")],
+ ["patch", String(routesToUse.length || "0")],
manifest
);
- routes.push(...dataChildren);
+ routesToUse.push(...dataChildren);
}
}
diff --git a/packages/router/utils.ts b/packages/router/utils.ts
index 2c5e30eabc..4e22db1566 100644
--- a/packages/router/utils.ts
+++ b/packages/router/utils.ts
@@ -1221,7 +1221,7 @@ export function getResolveToMatches<
// https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329
if (v7_relativeSplatPath) {
return pathMatches.map((match, idx) =>
- idx === matches.length - 1 ? match.pathname : match.pathnameBase
+ idx === pathMatches.length - 1 ? match.pathname : match.pathnameBase
);
}