Skip to content

Commit 90695fb

Browse files
authored
Add unstable_runClientMiddleware API for dataStrategy (#13395)
1 parent 416d3e2 commit 90695fb

File tree

4 files changed

+88
-52
lines changed

4 files changed

+88
-52
lines changed

.changeset/silent-snakes-mix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
UNSTABLE: Add a new `unstable_runClientMiddleware` argument to `dataStrategy` to enable middleware execution in custom `dataStrategy` implementations

packages/react-router/lib/dom/ssr/single-fetch.tsx

Lines changed: 31 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from "react";
22
import { decode } from "turbo-stream";
33
import type { Router as DataRouter } from "../../router/router";
4-
import { isResponse, runMiddlewarePipeline } from "../../router/router";
4+
import { isResponse } from "../../router/router";
55
import type {
66
DataStrategyFunction,
77
DataStrategyFunctionArgs,
@@ -148,11 +148,22 @@ export function StreamTransfer({
148148
}
149149
}
150150

151-
function handleMiddlewareError(error: unknown, routeId: string) {
152-
return { [routeId]: { type: "error", result: error } };
151+
export function getSingleFetchDataStrategy(
152+
manifest: AssetsManifest,
153+
ssr: boolean,
154+
basename: string | undefined,
155+
getRouter: () => DataRouter
156+
): DataStrategyFunction {
157+
let dataStrategy = getSingleFetchDataStrategyImpl(
158+
manifest,
159+
ssr,
160+
basename,
161+
getRouter
162+
);
163+
return async (args) => args.unstable_runClientMiddleware(dataStrategy);
153164
}
154165

155-
export function getSingleFetchDataStrategy(
166+
export function getSingleFetchDataStrategyImpl(
156167
manifest: AssetsManifest,
157168
ssr: boolean,
158169
basename: string | undefined,
@@ -163,15 +174,16 @@ export function getSingleFetchDataStrategy(
163174

164175
// Actions are simple and behave the same for navigations and fetchers
165176
if (request.method !== "GET") {
166-
return runMiddlewarePipeline(
167-
args,
168-
false,
169-
() => singleFetchActionStrategy(args, basename),
170-
handleMiddlewareError
171-
) as Promise<Record<string, DataStrategyResult>>;
177+
return singleFetchActionStrategy(args, basename);
172178
}
173179

174-
if (!ssr) {
180+
let foundRevalidatingServerLoader = matches.some(
181+
(m) =>
182+
m.unstable_shouldCallHandler() &&
183+
manifest.routes[m.route.id]?.hasLoader &&
184+
!manifest.routes[m.route.id]?.hasClientLoader
185+
);
186+
if (!ssr && !foundRevalidatingServerLoader) {
175187
// If this is SPA mode, there won't be any loaders below root and we'll
176188
// disable single fetch. We have to keep the `dataStrategy` defined for
177189
// SPA mode because we may load a SPA fallback page but then navigate into
@@ -204,46 +216,22 @@ export function getSingleFetchDataStrategy(
204216
// errored otherwise
205217
// - So it's safe to make the call knowing there will be a `.data` file on
206218
// the other end
207-
let foundRevalidatingServerLoader = matches.some(
208-
(m) =>
209-
m.unstable_shouldCallHandler() &&
210-
manifest.routes[m.route.id]?.hasLoader &&
211-
!manifest.routes[m.route.id]?.hasClientLoader
212-
);
213-
if (!foundRevalidatingServerLoader) {
214-
return runMiddlewarePipeline(
215-
args,
216-
false,
217-
() => nonSsrStrategy(args, manifest, basename),
218-
handleMiddlewareError
219-
) as Promise<Record<string, DataStrategyResult>>;
220-
}
219+
return nonSsrStrategy(args, manifest, basename);
221220
}
222221

223222
// Fetcher loads are singular calls to one loader
224223
if (fetcherKey) {
225-
return runMiddlewarePipeline(
226-
args,
227-
false,
228-
() => singleFetchLoaderFetcherStrategy(request, matches, basename),
229-
handleMiddlewareError
230-
) as Promise<Record<string, DataStrategyResult>>;
224+
return singleFetchLoaderFetcherStrategy(request, matches, basename);
231225
}
232226

233227
// Navigational loads are more complex...
234-
return runMiddlewarePipeline(
228+
return singleFetchLoaderNavigationStrategy(
235229
args,
236-
false,
237-
() =>
238-
singleFetchLoaderNavigationStrategy(
239-
args,
240-
manifest,
241-
ssr,
242-
getRouter(),
243-
basename
244-
),
245-
handleMiddlewareError
246-
) as Promise<Record<string, DataStrategyResult>>;
230+
manifest,
231+
ssr,
232+
getRouter(),
233+
basename
234+
);
247235
};
248236
}
249237

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

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2827,7 +2827,8 @@ export function createRouter(init: RouterInit): Router {
28272827
request,
28282828
matches,
28292829
fetcherKey,
2830-
scopedContext
2830+
scopedContext,
2831+
false
28312832
);
28322833
} catch (e) {
28332834
// If the outer dataStrategy method throws, just return the error for all
@@ -4218,7 +4219,8 @@ export function createStaticHandler(
42184219
request,
42194220
matches,
42204221
null,
4221-
requestContext
4222+
requestContext,
4223+
true
42224224
);
42234225

42244226
let dataResults: Record<string, DataResult> = {};
@@ -5546,7 +5548,8 @@ async function callDataStrategyImpl(
55465548
request: Request,
55475549
matches: DataStrategyMatch[],
55485550
fetcherKey: string | null,
5549-
scopedContext: unknown
5551+
scopedContext: unknown,
5552+
isStaticHandler: boolean
55505553
): Promise<Record<string, DataStrategyResult>> {
55515554
// Ensure all middleware is loaded before we start executing routes
55525555
if (matches.some((m) => m._lazyPromises?.middleware)) {
@@ -5556,12 +5559,52 @@ async function callDataStrategyImpl(
55565559
// Send all matches here to allow for a middleware-type implementation.
55575560
// handler will be a no-op for unneeded routes and we filter those results
55585561
// back out below.
5559-
let results = await dataStrategyImpl({
5560-
matches,
5562+
let dataStrategyArgs = {
55615563
request,
55625564
params: matches[0].params,
5563-
fetcherKey,
55645565
context: scopedContext,
5566+
matches,
5567+
};
5568+
let unstable_runClientMiddleware = isStaticHandler
5569+
? () => {
5570+
throw new Error(
5571+
"You cannot call `unstable_runClientMiddleware()` from a static handler " +
5572+
"`dataStrategy`. Middleware is run outside of `dataStrategy` during " +
5573+
"SSR in order to bubble up the Response. You can enable middleware " +
5574+
"via the `respond` API in `query`/`queryRoute`"
5575+
);
5576+
}
5577+
: (cb: DataStrategyFunction<unstable_RouterContextProvider>) => {
5578+
let typedDataStrategyArgs = dataStrategyArgs as (
5579+
| LoaderFunctionArgs<unstable_RouterContextProvider>
5580+
| ActionFunctionArgs<unstable_RouterContextProvider>
5581+
) & {
5582+
matches: DataStrategyMatch[];
5583+
};
5584+
return runMiddlewarePipeline(
5585+
typedDataStrategyArgs,
5586+
false,
5587+
() =>
5588+
cb({
5589+
...typedDataStrategyArgs,
5590+
fetcherKey,
5591+
unstable_runClientMiddleware: () => {
5592+
throw new Error(
5593+
"Cannot call `unstable_runClientMiddleware()` from within an " +
5594+
"`unstable_runClientMiddleware` handler"
5595+
);
5596+
},
5597+
}),
5598+
(error: unknown, routeId: string) => ({
5599+
[routeId]: { type: "error", result: error },
5600+
})
5601+
) as Promise<Record<string, DataStrategyResult>>;
5602+
};
5603+
5604+
let results = await dataStrategyImpl({
5605+
...dataStrategyArgs,
5606+
fetcherKey,
5607+
unstable_runClientMiddleware,
55655608
});
55665609

55675610
// Wait for all routes to load here but swallow the error since we want

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,6 @@ export interface DataStrategyMatch
347347
shouldLoad: boolean;
348348
// This can be null for actions calls and for initial hydration calls
349349
unstable_shouldRevalidateArgs: ShouldRevalidateFunctionArgs | null;
350-
// TODO: Figure out a good name for this or use `shouldLoad` and add a future flag
351350
// This function will use a scoped version of `shouldRevalidateArgs` because
352351
// they are read-only but let the user provide an optional override value for
353352
// `defaultShouldRevalidate` if they choose
@@ -362,8 +361,9 @@ export interface DataStrategyMatch
362361
export interface DataStrategyFunctionArgs<Context = DefaultContext>
363362
extends DataFunctionArgs<Context> {
364363
matches: DataStrategyMatch[];
365-
// TODO: Implement
366-
// runMiddleware: () => unknown,
364+
unstable_runClientMiddleware: (
365+
cb: DataStrategyFunction<Context>
366+
) => Promise<Record<string, DataStrategyResult>>;
367367
fetcherKey: string | null;
368368
}
369369

0 commit comments

Comments
 (0)