Skip to content

Commit 05612a3

Browse files
authored
unstable_dataStrategy refactor for better single fetch support (#11943)
1 parent 766f07d commit 05612a3

File tree

12 files changed

+581
-351
lines changed

12 files changed

+581
-351
lines changed

.changeset/four-books-bow.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@remix-run/router": patch
3+
---
4+
5+
Update the `unstable_dataStrategy` API to allow for more advanced implementations
6+
7+
- Rename `unstable_HandlerResult` to `unstable_DataStrategyResult`
8+
- The return signature has changed from a parallel array of `unstable_DataStrategyResult[]` (parallel to `matches`) to a key/value object of `routeId => unstable_DataStrategyResult`
9+
- This allows you to more easily decide to opt-into or out-of revalidating data that may not have been revalidated by default (via `match.shouldLoad`)
10+
- ⚠️ This is a breaking change if you've currently adopted `unstable_dataStrategy`
11+
- Added a new `fetcherKey` parameter to `unstable_dataStrategy` to allow differentiation from navigational and fetcher calls
12+
- You should now return/throw a result from your `handlerOverride` instead of returning a `DataStrategyResult`
13+
- If you are aggregating the results of `match.resolve()` into a final results object you should not need to think about the `DataStrategyResult` type
14+
- If you are manually filling your results object from within your `handlerOverride`, then you will need to assign a `DataStrategyResult` as the value so React Router knows if it's a successful execution or an error.

docs/routers/create-browser-router.md

Lines changed: 89 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ The `unstable_dataStrategy` option gives you full control over how your loaders
199199
```ts
200200
interface DataStrategyFunction {
201201
(args: DataStrategyFunctionArgs): Promise<
202-
HandlerResult[]
202+
Record<string, DataStrategyResult>
203203
>;
204204
}
205205

@@ -208,6 +208,7 @@ interface DataStrategyFunctionArgs<Context = any> {
208208
params: Params;
209209
context?: Context;
210210
matches: DataStrategyMatch[];
211+
fetcherKey: string | null;
211212
}
212213

213214
interface DataStrategyMatch
@@ -219,34 +220,36 @@ interface DataStrategyMatch
219220
resolve: (
220221
handlerOverride?: (
221222
handler: (ctx?: unknown) => DataFunctionReturnValue
222-
) => Promise<HandlerResult>
223-
) => Promise<HandlerResult>;
223+
) => Promise<DataStrategyResult>
224+
) => Promise<DataStrategyResult>;
224225
}
225226

226-
interface HandlerResult {
227+
interface DataStrategyResult {
227228
type: "data" | "error";
228-
result: any; // data, Error, Response, DeferredData
229-
status?: number;
229+
result: unknown; // data, Error, Response, DeferredData, DataWithResponseInit
230230
}
231231
```
232232

233233
### Overview
234234

235-
`unstable_dataStrategy` receives the same arguments as a `loader`/`action` (`request`, `params`) but it also receives a `matches` array which is an array of the matched routes where each match is extended with 2 new fields for use in the data strategy function:
236-
237-
- **`match.resolve`** - An async function that will resolve any `route.lazy` implementations and execute the route's handler (if necessary), returning a `HandlerResult`
238-
- You should call `match.resolve` for _all_ matches every time to ensure that all lazy routes are properly resolved
239-
- This does not mean you're calling the loader/action (the "handler") - `resolve` will only call the `handler` internally if needed and if you don't pass your own `handlerOverride` function parameter
240-
- See the examples below for how to implement custom handler execution via `match.resolve`
241-
- **`match.shouldLoad`** - A boolean value indicating whether this route handler needs to be called in this pass
242-
- The `matches` array always includes _all_ matched routes even when only _some_ route handlers need to be called so that things like middleware can be implemented
243-
- `shouldLoad` is usually only interesting if you are skipping the route handler entirely and implementing custom handler logic - since it lets you determine if that custom logic should run for this route or not
244-
- For example:
245-
- If you are on `/parent/child/a` and you navigate to `/parent/child/b` - you'll get an array of three matches (`[parent, child, b]`), but only `b` will have `shouldLoad=true` because the data for `parent` and `child` is already loaded
246-
- If you are on `/parent/child/a` and you submit to `a`'s `action`, then only `a` will have `shouldLoad=true` for the action execution of `dataStrategy`
247-
- After the `action`, `dataStrategy` will be called again for the `loader` revalidation, and all matches will have `shouldLoad=true` (assuming no custom `shouldRevalidate` implementations)
248-
249-
The `dataStrategy` function should return a parallel array of `HandlerResult` instances, which indicates if the handler was successful or not. If the returned `handlerResult.result` is a `Response`, React Router will unwrap it for you (via `res.json` or `res.text`). If you need to do custom decoding of a `Response` but preserve the status code, you can return the decoded value in `handlerResult.result` and send the status along via `handlerResult.status` (for example, when using the `future.v7_skipActionRevalidation` flag). `match.resolve()` will return a `HandlerResult` if you are not passing it a handler override function. If you are, then you need to wrap the `handler` result in a `HandlerResult` (see examples below).
235+
`unstable_dataStrategy` receives the same arguments as a `loader`/`action` (`request`, `params`) but it also receives 2 new parameters: `matches` and `fetcherKey`:
236+
237+
- **`matches`** - An array of the matched routes where each match is extended with 2 new fields for use in the data strategy function:
238+
- **`match.shouldLoad`** - A boolean value indicating whether this route handler should be called in this pass
239+
- The `matches` array always includes _all_ matched routes even when only _some_ route handlers need to be called so that things like middleware can be implemented
240+
- `shouldLoad` is usually only interesting if you are skipping the route handler entirely and implementing custom handler logic - since it lets you determine if that custom logic should run for this route or not
241+
- For example:
242+
- If you are on `/parent/child/a` and you navigate to `/parent/child/b` - you'll get an array of three matches (`[parent, child, b]`), but only `b` will have `shouldLoad=true` because the data for `parent` and `child` is already loaded
243+
- If you are on `/parent/child/a` and you submit to `a`'s `action`, then only `a` will have `shouldLoad=true` for the action execution of `dataStrategy`
244+
- After the `action`, `dataStrategy` will be called again for the `loader` revalidation, and all matches will have `shouldLoad=true` (assuming no custom `shouldRevalidate` implementations)
245+
- **`match.resolve`** - An async function that will resolve any `route.lazy` implementations and execute the route's handler (if necessary), returning a `DataStrategyResult`
246+
- Calling `match.resolve` does not mean you're calling the `loader`/`action` (the "handler") - `resolve` will only call the `handler` internally if needed _and_ if you don't pass your own `handlerOverride` function parameter
247+
- It is safe to call `match.resolve` for all matches, even if they have `shouldLoad=false`, and it will no-op if no loading is required
248+
- You should generally always call `match.resolve()` for `shouldLoad:true` routes to ensure that any `route.lazy` implementations are processed
249+
- See the examples below for how to implement custom handler execution via `match.resolve`
250+
- **`fetcherKey`** - The key of the fetcher we are calling `unstable_dataStrategy` for, otherwise `null` for navigational executions
251+
252+
The `dataStrategy` function should return a key/value object of `routeId -> DataStrategyResult` and should include entries for any routes where a handler was executed. A `DataStrategyResult` indicates if the handler was successful or not based on the `DataStrategyResult["type"]` field. If the returned `DataStrategyResult["result"]` is a `Response`, React Router will unwrap it for you (via `res.json` or `res.text`). If you need to do custom decoding of a `Response` but want to preserve the status code, you can use the `unstable_data` utility to return your decoded data along with a `ResponseInit`.
250253

251254
### Example Use Cases
252255

@@ -256,18 +259,61 @@ In the simplest case, let's look at hooking into this API to add some logging fo
256259

257260
```ts
258261
let router = createBrowserRouter(routes, {
259-
unstable_dataStrategy({ request, matches }) {
260-
return Promise.all(
261-
matches.map(async (match) => {
262-
console.log(`Processing route ${match.route.id}`);
262+
async unstable_dataStrategy({ request, matches }) {
263+
// Grab only the matches we need to run handlers for
264+
const matchesToLoad = matches.filter(
265+
(m) => m.shouldLoad
266+
);
267+
// Run the handlers in parallel, logging before and after
268+
const results = await Promise.all(
269+
matchesToLoad.map(async (match) => {
270+
console.log(`Processing ${match.route.id}`);
263271
// Don't override anything - just resolve route.lazy + call loader
264-
let result = await match.resolve();
265-
console.log(
266-
`Done processing route ${match.route.id}`
267-
);
272+
const result = await match.resolve();
268273
return result;
269274
})
270275
);
276+
277+
// Aggregate the results into a bn object of `routeId -> DataStrategyResult`
278+
return results.reduce(
279+
(acc, result, i) =>
280+
Object.assign(acc, {
281+
[matchesToLoad[i].route.id]: result,
282+
}),
283+
{}
284+
);
285+
},
286+
});
287+
```
288+
289+
If you want to avoid the `reduce`, you can manually build up the `results` object, but you'll need to construct the `DataStrategyResult` manually - indicating if the handler was successful or not:
290+
291+
```ts
292+
let router = createBrowserRouter(routes, {
293+
async unstable_dataStrategy({ request, matches }) {
294+
const matchesToLoad = matches.filter(
295+
(m) => m.shouldLoad
296+
);
297+
const results = {};
298+
await Promise.all(
299+
matchesToLoad.map(async (match) => {
300+
console.log(`Processing ${match.route.id}`);
301+
try {
302+
const result = await match.resolve();
303+
results[match.route.id] = {
304+
type: "data",
305+
result,
306+
};
307+
} catch (e) {
308+
results[match.route.id] = {
309+
type: "error",
310+
result: e,
311+
};
312+
}
313+
})
314+
);
315+
316+
return results;
271317
},
272318
});
273319
```
@@ -324,16 +370,23 @@ let router = createBrowserRouter(routes, {
324370
}
325371

326372
// Run loaders in parallel with the `context` value
327-
return Promise.all(
328-
matches.map((match, i) =>
329-
match.resolve(async (handler) => {
373+
let matchesToLoad = matches.filter((m) => m.shouldLoad);
374+
let results = await Promise.all(
375+
matchesToLoad.map((match, i) =>
376+
match.resolve((handler) => {
330377
// Whatever you pass to `handler` will be passed as the 2nd parameter
331378
// to your loader/action
332-
let result = await handler(context);
333-
return { type: "data", result };
379+
return handler(context);
334380
})
335381
)
336382
);
383+
return results.reduce(
384+
(acc, result, i) =>
385+
Object.assign(acc, {
386+
[matchesToLoad[i].route.id]: result,
387+
}),
388+
{}
389+
);
337390
},
338391
});
339392
```
@@ -377,7 +430,8 @@ let router = createBrowserRouter(routes, {
377430
// Compose route fragments into a single GQL payload
378431
let gql = getFragmentsFromRouteHandles(matches);
379432
let data = await fetchGql(gql);
380-
// Parse results back out into individual route level HandlerResult's
433+
// Parse results back out into individual route level `DataStrategyResult`'s
434+
// keyed by `routeId`
381435
let results = parseResultsFromGql(data);
382436
return results;
383437
},

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
},
106106
"filesize": {
107107
"packages/router/dist/router.umd.min.js": {
108-
"none": "57.2 kB"
108+
"none": "58.1 kB"
109109
},
110110
"packages/react-router/dist/react-router.production.min.js": {
111111
"none": "15.0 kB"

packages/react-router-dom-v5-compat/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export type {
5454
unstable_DataStrategyFunction,
5555
unstable_DataStrategyFunctionArgs,
5656
unstable_DataStrategyMatch,
57+
unstable_DataStrategyResult,
5758
DataRouteMatch,
5859
DataRouteObject,
5960
ErrorResponse,
@@ -112,7 +113,6 @@ export type {
112113
UIMatch,
113114
Blocker,
114115
BlockerFunction,
115-
unstable_HandlerResult,
116116
} from "./react-router-dom";
117117
export {
118118
AbortedDeferredError,

packages/react-router-dom/index.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
RouterProps,
1717
RouterProviderProps,
1818
To,
19+
unstable_DataStrategyFunction,
1920
unstable_PatchRoutesOnNavigationFunction,
2021
} from "react-router";
2122
import {
@@ -38,9 +39,6 @@ import {
3839
} from "react-router";
3940
import type {
4041
BrowserHistory,
41-
unstable_DataStrategyFunction,
42-
unstable_DataStrategyFunctionArgs,
43-
unstable_DataStrategyMatch,
4442
Fetcher,
4543
FormEncType,
4644
FormMethod,
@@ -89,9 +87,6 @@ import {
8987
////////////////////////////////////////////////////////////////////////////////
9088

9189
export type {
92-
unstable_DataStrategyFunction,
93-
unstable_DataStrategyFunctionArgs,
94-
unstable_DataStrategyMatch,
9590
FormEncType,
9691
FormMethod,
9792
GetScrollRestorationKeyFunction,
@@ -111,6 +106,10 @@ export type {
111106
BlockerFunction,
112107
DataRouteMatch,
113108
DataRouteObject,
109+
unstable_DataStrategyFunction,
110+
unstable_DataStrategyFunctionArgs,
111+
unstable_DataStrategyMatch,
112+
unstable_DataStrategyResult,
114113
ErrorResponse,
115114
Fetcher,
116115
FutureConfig,
@@ -152,7 +151,6 @@ export type {
152151
ShouldRevalidateFunctionArgs,
153152
To,
154153
UIMatch,
155-
unstable_HandlerResult,
156154
unstable_PatchRoutesOnNavigationFunction,
157155
} from "react-router";
158156
export {

packages/react-router-native/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export type {
3030
unstable_DataStrategyFunction,
3131
unstable_DataStrategyFunctionArgs,
3232
unstable_DataStrategyMatch,
33+
unstable_DataStrategyResult,
3334
ErrorResponse,
3435
Fetcher,
3536
FutureConfig,
@@ -71,7 +72,6 @@ export type {
7172
ShouldRevalidateFunctionArgs,
7273
To,
7374
UIMatch,
74-
unstable_HandlerResult,
7575
} from "react-router";
7676
export {
7777
AbortedDeferredError,

packages/react-router/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
unstable_DataStrategyFunction,
88
unstable_DataStrategyFunctionArgs,
99
unstable_DataStrategyMatch,
10+
unstable_DataStrategyResult,
1011
ErrorResponse,
1112
Fetcher,
1213
HydrationState,
@@ -31,7 +32,6 @@ import type {
3132
ShouldRevalidateFunctionArgs,
3233
To,
3334
UIMatch,
34-
unstable_HandlerResult,
3535
unstable_AgnosticPatchRoutesOnNavigationFunction,
3636
} from "@remix-run/router";
3737
import {
@@ -139,6 +139,7 @@ export type {
139139
unstable_DataStrategyFunction,
140140
unstable_DataStrategyFunctionArgs,
141141
unstable_DataStrategyMatch,
142+
unstable_DataStrategyResult,
142143
ErrorResponse,
143144
Fetcher,
144145
FutureConfig,
@@ -182,7 +183,6 @@ export type {
182183
UIMatch,
183184
Blocker,
184185
BlockerFunction,
185-
unstable_HandlerResult,
186186
};
187187
export {
188188
AbortedDeferredError,

0 commit comments

Comments
 (0)