diff --git a/.changeset/strong-parrots-jog.md b/.changeset/strong-parrots-jog.md new file mode 100644 index 0000000000..0bfe51abea --- /dev/null +++ b/.changeset/strong-parrots-jog.md @@ -0,0 +1,24 @@ +--- +"react-router": patch +--- + +Fix a regression in `createRoutesStub` introduced with the middleware feature. + +As part of that work we altered the signature to align with the new middleware APIs without making it backwards compatible with the prior `AppLoadContext` API. This permitted `createRoutesStub` to work if you were opting into middleware and the updated `context` typings, but broke `createRoutesStub` for users not yet opting into middleware. + +We've reverted this change and re-implemented it in such a way that both sets of users can leverage it. + +```tsx +// If you have not opted into middleware, the old API should work again +let context: AppLoadContext = { + /*...*/ +}; +let Stub = createRoutesStub(routes, context); + +// If you have opted into middleware, you should now pass an instantiated `unstable_routerContextProvider` instead of a `getContext` factory function. +let context = new unstable_RouterContextProvider(); +context.set(SomeContext, someValue); +let Stub = createRoutesStub(routes, context); +``` + +⚠️ This may be a breaking bug for if you have adopted the unstable Middleware feature and are using `createRoutesStub` with the updated API. diff --git a/packages/react-router/__tests__/dom/stub-test.tsx b/packages/react-router/__tests__/dom/stub-test.tsx index edb2098691..821f71548d 100644 --- a/packages/react-router/__tests__/dom/stub-test.tsx +++ b/packages/react-router/__tests__/dom/stub-test.tsx @@ -12,7 +12,10 @@ import { type LoaderFunctionArgs, useRouteError, } from "../../index"; -import { unstable_createContext } from "../../lib/router/utils"; +import { + unstable_RouterContextProvider, + unstable_createContext, +} from "../../lib/router/utils"; test("renders a route", () => { let RoutesStub = createRoutesStub([ @@ -236,7 +239,50 @@ test("can pass a predefined loader", () => { ]); }); -test("can pass context values", async () => { +test("can pass context values (w/o middleware)", async () => { + let RoutesStub = createRoutesStub( + [ + { + path: "/", + HydrateFallback: () => null, + Component() { + let data = useLoaderData() as string; + return ( +
+
Context: {data}
+ +
+ ); + }, + loader({ context }) { + return context.message; + }, + children: [ + { + path: "hello", + Component() { + let data = useLoaderData() as string; + return
Context: {data}
; + }, + loader({ context }) { + return context.message; + }, + }, + ], + }, + ], + { message: "hello" } + ); + + render(); + + expect(await screen.findByTestId("root")).toHaveTextContent(/Context: hello/); + expect(await screen.findByTestId("hello")).toHaveTextContent( + /Context: hello/ + ); +}); + +test("can pass context values (w/middleware)", async () => { let helloContext = unstable_createContext(); let RoutesStub = createRoutesStub( [ @@ -269,10 +315,15 @@ test("can pass context values", async () => { ], }, ], - () => new Map([[helloContext, "hello"]]) + new unstable_RouterContextProvider(new Map([[helloContext, "hello"]])) ); - render(); + render( + + ); expect(await screen.findByTestId("root")).toHaveTextContent(/Context: hello/); expect(await screen.findByTestId("hello")).toHaveTextContent( diff --git a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx index f339b2879b..ce4376ea7e 100644 --- a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx +++ b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx @@ -1,8 +1,9 @@ import * as React from "react"; import type { ActionFunction, + ActionFunctionArgs, LoaderFunction, - unstable_InitialContext, + LoaderFunctionArgs, } from "../../router/utils"; import type { DataRouteObject, @@ -12,7 +13,12 @@ import type { import type { LinksFunction, MetaFunction, RouteModules } from "./routeModules"; import type { InitialEntry } from "../../router/history"; import type { HydrationState } from "../../router/router"; -import { convertRoutesToDataRoutes } from "../../router/utils"; +import { + convertRoutesToDataRoutes, + unstable_RouterContextProvider, +} from "../../router/utils"; +import type { MiddlewareEnabled } from "../../types/future"; +import type { AppLoadContext } from "../../server-runtime/data"; import type { AssetsManifest, FutureConfig, @@ -111,7 +117,7 @@ export interface RoutesTestStubProps { */ export function createRoutesStub( routes: StubRouteObject[], - unstable_getContext?: () => unstable_InitialContext + _context?: AppLoadContext | unstable_RouterContextProvider ) { return function RoutesTestStub({ initialEntries, @@ -147,11 +153,15 @@ export function createRoutesStub( // @ts-expect-error `StubRouteObject` is stricter about `loader`/`action` // types compared to `AgnosticRouteObject` convertRoutesToDataRoutes(routes, (r) => r), + _context !== undefined + ? _context + : future?.unstable_middleware + ? new unstable_RouterContextProvider() + : {}, frameworkContextRef.current.manifest, frameworkContextRef.current.routeModules ); routerRef.current = createMemoryRouter(patched, { - unstable_getContext, initialEntries, initialIndex, hydrationData, @@ -168,6 +178,7 @@ export function createRoutesStub( function processRoutes( routes: StubRouteObject[], + context: unknown, manifest: AssetsManifest, routeModules: RouteModules, parentId?: string @@ -192,8 +203,12 @@ function processRoutes( ErrorBoundary: route.ErrorBoundary ? withErrorBoundaryProps(route.ErrorBoundary) : undefined, - action: route.action, - loader: route.loader, + action: route.action + ? (args: ActionFunctionArgs) => route.action!({ ...args, context }) + : undefined, + loader: route.loader + ? (args: LoaderFunctionArgs) => route.loader!({ ...args, context }) + : undefined, handle: route.handle, shouldRevalidate: route.shouldRevalidate, }; @@ -235,6 +250,7 @@ function processRoutes( if (route.children) { newRoute.children = processRoutes( route.children, + context, manifest, routeModules, newRoute.id