Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silent-snakes-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router": patch
---

UNSTABLE: Add a new `unstable_runClientMiddleware` argument to `dataStrategy` to enable middleware execution in custom `dataStrategy` implementations
74 changes: 31 additions & 43 deletions packages/react-router/lib/dom/ssr/single-fetch.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react";
import { decode } from "turbo-stream";
import type { Router as DataRouter } from "../../router/router";
import { isResponse, runMiddlewarePipeline } from "../../router/router";
import { isResponse } from "../../router/router";
import type {
DataStrategyFunction,
DataStrategyFunctionArgs,
Expand Down Expand Up @@ -148,11 +148,22 @@ export function StreamTransfer({
}
}

function handleMiddlewareError(error: unknown, routeId: string) {
return { [routeId]: { type: "error", result: error } };
export function getSingleFetchDataStrategy(
manifest: AssetsManifest,
ssr: boolean,
basename: string | undefined,
getRouter: () => DataRouter
): DataStrategyFunction {
let dataStrategy = getSingleFetchDataStrategyImpl(
manifest,
ssr,
basename,
getRouter
);
return async (args) => args.unstable_runClientMiddleware(dataStrategy);
}

export function getSingleFetchDataStrategy(
export function getSingleFetchDataStrategyImpl(
manifest: AssetsManifest,
ssr: boolean,
basename: string | undefined,
Expand All @@ -163,15 +174,16 @@ export function getSingleFetchDataStrategy(

// Actions are simple and behave the same for navigations and fetchers
if (request.method !== "GET") {
return runMiddlewarePipeline(
args,
false,
() => singleFetchActionStrategy(args, basename),
handleMiddlewareError
) as Promise<Record<string, DataStrategyResult>>;
return singleFetchActionStrategy(args, basename);
Comment on lines -166 to +177
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This goes back to effectively the original non-middleware implementation because we wrap middleware around the whole thing above

}

if (!ssr) {
let foundRevalidatingServerLoader = matches.some(
(m) =>
m.unstable_shouldCallHandler() &&
manifest.routes[m.route.id]?.hasLoader &&
!manifest.routes[m.route.id]?.hasClientLoader
);
if (!ssr && !foundRevalidatingServerLoader) {
// If this is SPA mode, there won't be any loaders below root and we'll
// disable single fetch. We have to keep the `dataStrategy` defined for
// SPA mode because we may load a SPA fallback page but then navigate into
Expand Down Expand Up @@ -204,46 +216,22 @@ export function getSingleFetchDataStrategy(
// errored otherwise
// - So it's safe to make the call knowing there will be a `.data` file on
// the other end
let foundRevalidatingServerLoader = matches.some(
(m) =>
m.unstable_shouldCallHandler() &&
manifest.routes[m.route.id]?.hasLoader &&
!manifest.routes[m.route.id]?.hasClientLoader
);
if (!foundRevalidatingServerLoader) {
return runMiddlewarePipeline(
args,
false,
() => nonSsrStrategy(args, manifest, basename),
handleMiddlewareError
) as Promise<Record<string, DataStrategyResult>>;
}
return nonSsrStrategy(args, manifest, basename);
}

// Fetcher loads are singular calls to one loader
if (fetcherKey) {
return runMiddlewarePipeline(
args,
false,
() => singleFetchLoaderFetcherStrategy(request, matches, basename),
handleMiddlewareError
) as Promise<Record<string, DataStrategyResult>>;
return singleFetchLoaderFetcherStrategy(request, matches, basename);
}

// Navigational loads are more complex...
return runMiddlewarePipeline(
return singleFetchLoaderNavigationStrategy(
args,
false,
() =>
singleFetchLoaderNavigationStrategy(
args,
manifest,
ssr,
getRouter(),
basename
),
handleMiddlewareError
) as Promise<Record<string, DataStrategyResult>>;
manifest,
ssr,
getRouter(),
basename
);
};
}

Expand Down
55 changes: 49 additions & 6 deletions packages/react-router/lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2827,7 +2827,8 @@ export function createRouter(init: RouterInit): Router {
request,
matches,
fetcherKey,
scopedContext
scopedContext,
false
);
} catch (e) {
// If the outer dataStrategy method throws, just return the error for all
Expand Down Expand Up @@ -4218,7 +4219,8 @@ export function createStaticHandler(
request,
matches,
null,
requestContext
requestContext,
true
);

let dataResults: Record<string, DataResult> = {};
Expand Down Expand Up @@ -5546,7 +5548,8 @@ async function callDataStrategyImpl(
request: Request,
matches: DataStrategyMatch[],
fetcherKey: string | null,
scopedContext: unknown
scopedContext: unknown,
isStaticHandler: boolean
): Promise<Record<string, DataStrategyResult>> {
// Ensure all middleware is loaded before we start executing routes
if (matches.some((m) => m._lazyPromises?.middleware)) {
Expand All @@ -5556,12 +5559,52 @@ async function callDataStrategyImpl(
// Send all matches here to allow for a middleware-type implementation.
// handler will be a no-op for unneeded routes and we filter those results
// back out below.
let results = await dataStrategyImpl({
matches,
let dataStrategyArgs = {
request,
params: matches[0].params,
fetcherKey,
context: scopedContext,
matches,
};
let unstable_runClientMiddleware = isStaticHandler
? () => {
throw new Error(
"You cannot call `unstable_runClientMiddleware()` from a static handler " +
"`dataStrategy`. Middleware is run outside of `dataStrategy` during " +
"SSR in order to bubble up the Response. You can enable middleware " +
"via the `respond` API in `query`/`queryRoute`"
);
}
: (cb: DataStrategyFunction<unstable_RouterContextProvider>) => {
let typedDataStrategyArgs = dataStrategyArgs as (
| LoaderFunctionArgs<unstable_RouterContextProvider>
| ActionFunctionArgs<unstable_RouterContextProvider>
) & {
matches: DataStrategyMatch[];
};
return runMiddlewarePipeline(
typedDataStrategyArgs,
false,
() =>
cb({
...typedDataStrategyArgs,
fetcherKey,
unstable_runClientMiddleware: () => {
throw new Error(
"Cannot call `unstable_runClientMiddleware()` from within an " +
"`unstable_runClientMiddleware` handler"
);
},
}),
(error: unknown, routeId: string) => ({
[routeId]: { type: "error", result: error },
})
) as Promise<Record<string, DataStrategyResult>>;
};

let results = await dataStrategyImpl({
...dataStrategyArgs,
fetcherKey,
unstable_runClientMiddleware,
});

// Wait for all routes to load here but swallow the error since we want
Expand Down
6 changes: 3 additions & 3 deletions packages/react-router/lib/router/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,6 @@ export interface DataStrategyMatch
shouldLoad: boolean;
// This can be null for actions calls and for initial hydration calls
unstable_shouldRevalidateArgs: ShouldRevalidateFunctionArgs | null;
// TODO: Figure out a good name for this or use `shouldLoad` and add a future flag
// This function will use a scoped version of `shouldRevalidateArgs` because
// they are read-only but let the user provide an optional override value for
// `defaultShouldRevalidate` if they choose
Expand All @@ -362,8 +361,9 @@ export interface DataStrategyMatch
export interface DataStrategyFunctionArgs<Context = DefaultContext>
extends DataFunctionArgs<Context> {
matches: DataStrategyMatch[];
// TODO: Implement
// runMiddleware: () => unknown,
unstable_runClientMiddleware: (
cb: DataStrategyFunction<Context>
) => Promise<Record<string, DataStrategyResult>>;
fetcherKey: string | null;
}

Expand Down