diff --git a/.changeset/nice-donuts-obey.md b/.changeset/nice-donuts-obey.md new file mode 100644 index 0000000000..18fa7ad247 --- /dev/null +++ b/.changeset/nice-donuts-obey.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Ensure action handlers run 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..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,