diff --git a/.changeset/stupid-games-build.md b/.changeset/stupid-games-build.md
new file mode 100644
index 00000000000..99770801029
--- /dev/null
+++ b/.changeset/stupid-games-build.md
@@ -0,0 +1,6 @@
+---
+"@remix-run/react": patch
+"@remix-run/server-runtime": patch
+---
+
+Ignore pathless layout routes in action matches
diff --git a/integration/form-test.ts b/integration/form-test.ts
index 4db956db6b3..5b0c08cb7be 100644
--- a/integration/form-test.ts
+++ b/integration/form-test.ts
@@ -400,6 +400,47 @@ test.describe("Forms", () => {
)
}
`,
+
+ "app/routes/pathless-layout-parent.jsx": js`
+ import { json } from '@remix-run/server-runtime'
+ import { Form, Outlet, useActionData } from '@remix-run/react'
+
+ export async function action({ request }) {
+ return json({ submitted: true });
+ }
+ export default function () {
+ let data = useActionData();
+ return (
+ <>
+
+
+ {data?.submitted === true ? 'Submitted - Yes' : 'Submitted - No'}
+ >
+ );
+ }
+ `,
+
+ "app/routes/pathless-layout-parent/__pathless.jsx": js`
+ import { Outlet } from '@remix-run/react';
+
+ export default function () {
+ return (
+ <>
+ Pathless Layout
+
+ >
+ );
+ }
+ `,
+
+ "app/routes/pathless-layout-parent/__pathless/index.jsx": js`
+ export default function () {
+ return Pathless Layout Index
+ }
+ `,
},
});
@@ -942,4 +983,24 @@ test.describe("Forms", () => {
);
}
});
+
+ test("pathless layout routes are ignored in form actions", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/pathless-layout-parent");
+ let html = await app.getHtml();
+ expect(html).toMatch("Pathless Layout Parent");
+ expect(html).toMatch("Pathless Layout ");
+ expect(html).toMatch("Pathless Layout Index");
+
+ let el = getElement(html, `form`);
+ expect(el.attr("action")).toMatch("/pathless-layout-parent");
+
+ expect(await app.getHtml()).toMatch("Submitted - No");
+ // This submission should ignore the index route and the pathless layout
+ // route above it and hit the action in routes/pathless-layout-parent.jsx
+ await app.clickSubmitButton("/pathless-layout-parent");
+ expect(await app.getHtml()).toMatch("Submitted - Yes");
+ });
});
diff --git a/packages/remix-react/transition.ts b/packages/remix-react/transition.ts
index 5ef8949b916..9bafb79584b 100644
--- a/packages/remix-react/transition.ts
+++ b/packages/remix-react/transition.ts
@@ -590,7 +590,7 @@ export function createTransitionManager(init: TransitionManagerInit) {
invariant(matches, "No matches found");
if (fetchControllers.has(key)) abortFetcher(key);
- let match = getFetcherRequestMatch(
+ let match = getRequestMatch(
new URL(href, window.location.href),
matches
);
@@ -644,17 +644,28 @@ export function createTransitionManager(init: TransitionManagerInit) {
return false;
}
- function getFetcherRequestMatch(
- url: URL,
- matches: RouteMatch[]
- ) {
+ // Return the correct single match for a route (used for submission
+ // navigations and and fetchers)
+ // - ?index should try to match the leaf index route
+ // - otherwise it should match the deepest "path contributing" match, which
+ // ignores index and pathless routes
+ function getRequestMatch(url: URL, matches: ClientMatch[]) {
let match = matches.slice(-1)[0];
- if (!isIndexRequestUrl(url) && match.route.index) {
- return matches.slice(-2)[0];
+ if (isIndexRequestUrl(url) && match.route.index) {
+ return match;
}
- return match;
+ return getPathContributingMatches(matches).slice(-1)[0];
+ }
+
+ // Filter index and pathless routes when looking for submission targets
+ function getPathContributingMatches(matches: ClientMatch[]) {
+ return matches.filter(
+ (match, index) =>
+ index === 0 ||
+ (!match.route.index && match.route.path && match.route.path.length > 0)
+ );
}
async function handleActionFetchSubmission(
@@ -1071,14 +1082,10 @@ export function createTransitionManager(init: TransitionManagerInit) {
// to run on layout/index routes. We do not want to mutate the eventual
// matches used for revalidation
let actionMatches = matches;
- if (
- !isIndexRequestUrl(createUrl(submission.action)) &&
- actionMatches[matches.length - 1].route.index
- ) {
- actionMatches = actionMatches.slice(0, -1);
- }
-
- let leafMatch = actionMatches.slice(-1)[0];
+ let leafMatch = getRequestMatch(
+ createUrl(submission.action),
+ actionMatches
+ );
let result = await callAction(submission, leafMatch, controller.signal);
if (controller.signal.aborted) {
diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts
index f5ee5b200cd..edc3311f696 100644
--- a/packages/remix-server-runtime/server.ts
+++ b/packages/remix-server-runtime/server.ts
@@ -708,11 +708,19 @@ function isIndexRequestUrl(url: URL) {
function getRequestMatch(url: URL, matches: RouteMatch[]) {
let match = matches.slice(-1)[0];
- if (!isIndexRequestUrl(url) && match.route.id.endsWith("/index")) {
- return matches.slice(-2)[0];
+ if (isIndexRequestUrl(url) && match.route.id.endsWith("/index")) {
+ return match;
}
- return match;
+ return getPathContributingMatches(matches).slice(-1)[0];
+}
+
+function getPathContributingMatches(matches: RouteMatch[]) {
+ return matches.filter(
+ (match, index) =>
+ index === 0 ||
+ (!match.route.index && match.route.path && match.route.path.length > 0)
+ );
}
function getDeepestRouteIdWithBoundary(