From c967d247479b8c5911f4dd7c2e4f87a95713bcfd Mon Sep 17 00:00:00 2001 From: rururux Date: Sun, 12 Oct 2025 01:39:09 +0900 Subject: [PATCH 1/6] fix(react-router): run action handlers for routes with middleware even if no loader is present --- .changeset/nice-donuts-obey.md | 5 ++ .../router/context-middleware-test.tsx | 65 +++++++++++++++++++ packages/react-router/lib/router/router.ts | 6 +- 3 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 .changeset/nice-donuts-obey.md diff --git a/.changeset/nice-donuts-obey.md b/.changeset/nice-donuts-obey.md new file mode 100644 index 0000000000..e5fd2b5995 --- /dev/null +++ b/.changeset/nice-donuts-obey.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +fix(react-router): run action handlers for routes with middleware even if no loader is present diff --git a/packages/react-router/__tests__/router/context-middleware-test.tsx b/packages/react-router/__tests__/router/context-middleware-test.tsx index cea82e235c..ab62e5c132 100644 --- a/packages/react-router/__tests__/router/context-middleware-test.tsx +++ b/packages/react-router/__tests__/router/context-middleware-test.tsx @@ -477,6 +477,71 @@ describe("context/middleware", () => { ]); }); + it("runs middleware even if no loader exists but an action is present", async () => { + let snapshot; + router = createRouter({ + history: createMemoryHistory(), + routes: [ + { + path: "/", + }, + { + id: "parent", + path: "/parent", + middleware: [ + async ({ context }, next) => { + await next(); + // Grab a snapshot at the end of the upwards middleware chain + snapshot = context.get(orderContext); + }, + getOrderMiddleware(orderContext, "a"), + getOrderMiddleware(orderContext, "b"), + ], + children: [ + { + id: "child", + path: "child", + middleware: [ + getOrderMiddleware(orderContext, "c"), + getOrderMiddleware(orderContext, "d"), + ], + action({ context }) { + context.get(orderContext).push("child action"); + }, + }, + ], + }, + ], + }); + + await router.navigate("/parent/child", { + formMethod: "post", + formData: createFormData({}), + }); + + expect(snapshot).toEqual([ + // Action + "a middleware - before next()", + "b middleware - before next()", + "c middleware - before next()", + "d middleware - before next()", + "child action", + "d middleware - after next()", + "c middleware - after next()", + "b middleware - after next()", + "a middleware - after next()", + // Revalidation + "a middleware - before next()", + "b middleware - before next()", + "c middleware - before next()", + "d middleware - before next()", + "d middleware - after next()", + "c middleware - after next()", + "b middleware - after next()", + "a middleware - after next()", + ]); + }); + it("returns result of middleware in client side routers", async () => { let values: unknown[] = []; let consoleSpy = jest diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 4cd9d1df51..24bad313d3 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -5744,7 +5744,7 @@ function getDataStrategyMatch( return shouldRevalidateLoader(match, unstable_shouldRevalidateArgs); }, resolve(handlerOverride) { - let { lazy, loader, middleware } = match.route; + let { lazy, loader, action, middleware } = match.route; let callHandler = isUsingNewApi || @@ -5754,10 +5754,10 @@ function getDataStrategyMatch( (lazy || loader)); // If this match was marked `shouldLoad` due to a middleware and it - // doesn't have a `loader` to run and no `lazy` to add one, then we can + // doesn't have a `loader` or `action` to run and no `lazy` to add one, then we can // just return undefined from the "loader" here let isMiddlewareOnlyRoute = - middleware && middleware.length > 0 && !loader && !lazy; + middleware && middleware.length > 0 && !loader && !action && !lazy; if (callHandler && !isMiddlewareOnlyRoute) { return callLoaderOrAction({ From 5d56cdd22a30b438a6ce1e28f33106b924caf6d9 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 23 Oct 2025 09:36:50 -0400 Subject: [PATCH 2/6] Apply suggestion from @brophdawg11 --- packages/react-router/lib/router/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 24bad313d3..8888332f30 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -5744,7 +5744,7 @@ function getDataStrategyMatch( return shouldRevalidateLoader(match, unstable_shouldRevalidateArgs); }, resolve(handlerOverride) { - let { lazy, loader, action, middleware } = match.route; + let { lazy, loader, middleware } = match.route; let callHandler = isUsingNewApi || From b5aa671c60c30696bdeb4a85f6ad66f563196ddd Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 23 Oct 2025 09:36:56 -0400 Subject: [PATCH 3/6] Apply suggestion from @brophdawg11 --- packages/react-router/lib/router/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 8888332f30..eefe921a8b 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -5754,7 +5754,7 @@ function getDataStrategyMatch( (lazy || loader)); // If this match was marked `shouldLoad` due to a middleware and it - // doesn't have a `loader` or `action` to run and no `lazy` to add one, then we can + // doesn't have a `loader` to run and no `lazy` to add one, then we can // just return undefined from the "loader" here let isMiddlewareOnlyRoute = middleware && middleware.length > 0 && !loader && !action && !lazy; From 9e122c81c650251f203ffc05e799fcdb0512767a Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 23 Oct 2025 09:37:02 -0400 Subject: [PATCH 4/6] Apply suggestion from @brophdawg11 --- packages/react-router/lib/router/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index eefe921a8b..4cd9d1df51 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -5757,7 +5757,7 @@ function getDataStrategyMatch( // doesn't have a `loader` to run and no `lazy` to add one, then we can // just return undefined from the "loader" here let isMiddlewareOnlyRoute = - middleware && middleware.length > 0 && !loader && !action && !lazy; + middleware && middleware.length > 0 && !loader && !lazy; if (callHandler && !isMiddlewareOnlyRoute) { return callLoaderOrAction({ From 7f76fa270d83f00bf4aca874e3e17b30156c01c6 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 23 Oct 2025 09:40:06 -0400 Subject: [PATCH 5/6] Alternate approach --- packages/react-router/lib/router/router.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 4cd9d1df51..3739cc4b1d 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -5753,13 +5753,16 @@ function getDataStrategyMatch( !isMutationMethod(request.method) && (lazy || loader)); - // If this match was marked `shouldLoad` due to a middleware and it - // doesn't have a `loader` to run and no `lazy` to add one, then we can - // just return undefined from the "loader" here + // For GET requests, if this match was marked `shouldLoad` due to a + // middleware and it doesn't have a `loader` to run and no `lazy` to add + // one, then we can just return undefined from the "loader" here let isMiddlewareOnlyRoute = middleware && middleware.length > 0 && !loader && !lazy; - if (callHandler && !isMiddlewareOnlyRoute) { + if ( + callHandler && + (isMutationMethod(request.method) || !isMiddlewareOnlyRoute) + ) { return callLoaderOrAction({ request, match, From 7e539837b836b83bda82d37287e9fcefec4817a3 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 23 Oct 2025 09:40:37 -0400 Subject: [PATCH 6/6] Apply suggestion from @brophdawg11 --- .changeset/nice-donuts-obey.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/nice-donuts-obey.md b/.changeset/nice-donuts-obey.md index e5fd2b5995..18fa7ad247 100644 --- a/.changeset/nice-donuts-obey.md +++ b/.changeset/nice-donuts-obey.md @@ -2,4 +2,4 @@ "react-router": patch --- -fix(react-router): run action handlers for routes with middleware even if no loader is present +Ensure action handlers run for routes with middleware even if no loader is present