From c967d247479b8c5911f4dd7c2e4f87a95713bcfd Mon Sep 17 00:00:00 2001 From: rururux Date: Sun, 12 Oct 2025 01:39:09 +0900 Subject: [PATCH] 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({