Skip to content

Commit fbbfb71

Browse files
committed
fix middleware when used with createRoutesStub
1 parent b6b8a79 commit fbbfb71

File tree

4 files changed

+93
-8
lines changed

4 files changed

+93
-8
lines changed

.changeset/blue-masks-boil.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
- Update client-side router to run client `middleware` on initial load even if no loaders exist
6+
- Update `createRoutesStub` to run route middleware
7+
- You will need to set the `<RoutesStub future={{ v8_middleware: true }} />` flag to enable the proper `context` type

packages/react-router/__tests__/dom/stub-test.tsx

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from "react";
2-
import { render, screen, waitFor } from "@testing-library/react";
2+
import { act, render, screen, waitFor } from "@testing-library/react";
33
import user from "@testing-library/user-event";
44
import {
55
Form,
@@ -12,7 +12,11 @@ import {
1212
type LoaderFunctionArgs,
1313
useRouteError,
1414
} from "../../index";
15-
import { RouterContextProvider, createContext } from "../../lib/router/utils";
15+
import {
16+
RouterContextProvider,
17+
createContext,
18+
redirect,
19+
} from "../../lib/router/utils";
1620

1721
test("renders a route", () => {
1822
let RoutesStub = createRoutesStub([
@@ -53,6 +57,57 @@ test("renders a nested route", () => {
5357
expect(screen.getByText("INDEX")).toBeInTheDocument();
5458
});
5559

60+
test("middleware works without loader", async () => {
61+
let RoutesStub = createRoutesStub([
62+
{
63+
path: "/",
64+
middleware: [
65+
() => {
66+
throw redirect("/target");
67+
},
68+
],
69+
HydrateFallback: () => null,
70+
Component() {
71+
return <pre>Home</pre>;
72+
},
73+
},
74+
{
75+
path: "/target",
76+
Component() {
77+
return <pre>Target</pre>;
78+
},
79+
},
80+
]);
81+
82+
act(() => {
83+
render(<RoutesStub future={{ v8_middleware: true }} />);
84+
});
85+
86+
await waitFor(() => screen.findByText("Target"));
87+
});
88+
89+
test("middleware works with loader", async () => {
90+
let stringContext = createContext<string>();
91+
let RoutesStub = createRoutesStub([
92+
{
93+
path: "/",
94+
HydrateFallback: () => null,
95+
Component() {
96+
let data = useLoaderData();
97+
return <pre data-testid="data">Message: {data.message}</pre>;
98+
},
99+
middleware: [({ context }) => context.set(stringContext, "hello")],
100+
loader({ context }) {
101+
return { message: context.get(stringContext) };
102+
},
103+
},
104+
]);
105+
106+
render(<RoutesStub future={{ v8_middleware: true }} />);
107+
108+
await waitFor(() => screen.findByText("Message: hello"));
109+
});
110+
56111
// eslint-disable-next-line jest/expect-expect
57112
test("loaders work", async () => {
58113
let RoutesStub = createRoutesStub([

packages/react-router/lib/dom/ssr/routes-test-stub.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
ActionFunctionArgs,
55
LoaderFunction,
66
LoaderFunctionArgs,
7+
MiddlewareFunction,
78
} from "../../router/utils";
89
import type {
910
DataRouteObject,
@@ -209,6 +210,16 @@ function processRoutes(
209210
loader: route.loader
210211
? (args: LoaderFunctionArgs) => route.loader!({ ...args, context })
211212
: undefined,
213+
middleware: route.middleware
214+
? route.middleware.map(
215+
(mw) =>
216+
(...args: Parameters<MiddlewareFunction>) =>
217+
mw(
218+
{ ...args[0], context: context as RouterContextProvider },
219+
args[1],
220+
),
221+
)
222+
: undefined,
212223
handle: route.handle,
213224
shouldRevalidate: route.shouldRevalidate,
214225
};

packages/react-router/lib/router/router.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -959,8 +959,14 @@ export function createRouter(init: RouterInit): Router {
959959
// All initialMatches need to be loaded before we're ready. If we have lazy
960960
// functions around still then we'll need to run them in initialize()
961961
initialized = false;
962-
} else if (!initialMatches.some((m) => m.route.loader)) {
963-
// If we've got no loaders to run, then we're good to go
962+
} else if (
963+
!initialMatches.some(
964+
(m) =>
965+
m.route.loader ||
966+
(m.route.middleware && m.route.middleware.length > 0),
967+
)
968+
) {
969+
// If we've got no loaders or middleware to run, then we're good to go
964970
initialized = true;
965971
} else {
966972
// With "partial hydration", we're initialized so long as we were
@@ -4960,8 +4966,8 @@ function shouldLoadRouteOnHydration(
49604966
return true;
49614967
}
49624968

4963-
// No loader, nothing to initialize
4964-
if (!route.loader) {
4969+
// No loader or middleware, nothing to run
4970+
if (!route.loader && (!route.middleware || route.middleware.length === 0)) {
49654971
return false;
49664972
}
49674973

@@ -5519,9 +5525,15 @@ function runClientMiddlewarePipeline(
55195525
let { matches } = args;
55205526
let maxBoundaryIdx = Math.min(
55215527
// Throwing route
5522-
matches.findIndex((m) => m.route.id === routeId) || 0,
5528+
Math.max(
5529+
matches.findIndex((m) => m.route.id === routeId),
5530+
0,
5531+
),
55235532
// or the shallowest route that needs to load data
5524-
matches.findIndex((m) => m.unstable_shouldCallHandler()) || 0,
5533+
Math.max(
5534+
matches.findIndex((m) => m.unstable_shouldCallHandler()),
5535+
0,
5536+
),
55255537
);
55265538
let boundaryRouteId = findNearestBoundary(
55275539
matches,

0 commit comments

Comments
 (0)