Skip to content

Commit aa19ad4

Browse files
committed
Add skipLoaderErrorBubbling and final tests
1 parent c72d536 commit aa19ad4

File tree

7 files changed

+480
-31
lines changed

7 files changed

+480
-31
lines changed

.changeset/skip-action-revalidation.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ Add a new `future.unstable_skipActionRevalidation` future flag
77
- Currently, active loaders revalidate after any action, regardless of the result
88
- With this flag enabled, actions that return/throw a 4xx/5xx response status will no longer automatically revalidate
99
- This should reduce load on your server since it's rare that a 4xx/5xx should actually mutate any data
10-
- If you need to revalidate after a 4xx/5xx result with this flag enabled, you can still do that via returning `true` from `shouldRevalidate` which now receives a new `unstable_actionStatus` argument
10+
- If you need to revalidate after a 4xx/5xx result with this flag enabled, you can still do that via returning `true` from `shouldRevalidate`
11+
- `shouldRevalidate` now also receives a new `unstable_actionStatus` argument alongside `actionResult` so you can make decision based on the status of the `action` response without having to encode it into the action data

.changeset/static-query-flags.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@remix-run/router": minor
3+
---
4+
5+
Added 2 new options to the `staticHandler.query` method for use in Remix's Single Fetch implementation:
6+
7+
- `loadRouteIds`: An optional array of route IDs to load if you wish to load a subset of the matched routes (useful for fine-grained revalidation)
8+
- `skipLoaderErrorBubbling`: Disable error bubbling on loader executions for single-fetch scenarios where the client-side router will handle the bubbling

decisions/0003-data-strategy.md

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ function dataStrategy({ matches }) {
5454
}
5555
```
5656

57-
⚠️ `defaultStrategy` was eliminated in favor of `match.handler`.
57+
⚠️ `defaultStrategy` was eliminated in favor of `match.resolve`.
5858

59-
We also originally intended to expose a `type: 'loader' | 'action`' field as a way to presumably let them call `match.route.loader`/`match.route.action` directly - but we have since decided against that with the `match.handler` API.
59+
We also originally intended to expose a `type: 'loader' | 'action`' field as a way to presumably let them call `match.route.loader`/`match.route.action` directly - but we have since decided against that with the `match.resolve` API.
6060

61-
⚠️ `type` was eliminated in favor of `match.handler`.
61+
⚠️ `type` was eliminated in favor of `match.resolve`.
6262

6363
`dataStrategy` is control _when_ handlers are called, not _how_. RR is in charge of calling them with the right parameters.
6464

@@ -89,7 +89,7 @@ Initially, we intended for `dataStrategy` to handle (3), and considered an optio
8989
### Handling `route.lazy`
9090

9191
There's a nuanced step we missed in our sequential steps above. If a route was
92-
using `route.lazy`, we may need to load the rout before we can execute the `loader`. There's two options here:
92+
using `route.lazy`, we may need to load the route before we can execute the `loader`. There's two options here:
9393

9494
1. We pre-execute all `route.lazy` methods before calling `dataStrategy`
9595
2. We let `dataStrategy` execute them accordingly
@@ -129,7 +129,7 @@ function dataStrategy({ matches, defaultStrategy }) {
129129
}
130130
```
131131

132-
⚠️ We are actively seeing if we can eliminate this via `match.handler`
132+
⚠️ This match.route as a function API was removed in favor of `match.resolve`
133133

134134
### Handling `shouldRevalidate` behavior
135135

@@ -326,33 +326,94 @@ Extending on the idea above - it all started to feel super leaky and full of imp
326326
327327
That's wayyyy to many rough edges for us to document and users to get wrong (rightfully so!).
328328
329-
Why can't we just do it all? LE'ts wrap _all_ of that up into a single `match.handler()` function that:
329+
Why can't we just do it all? Let's wrap _all_ of that up into a single `match.resolve()` function that:
330330
331331
- Waits for `route.lazy` to resolve (if needed)
332332
- No-ops if the route isn't supposed to revalidate
333333
- Open question here if we return the _current_ data from these no-ops or return `undefined`?
334+
- We decided _not_ to expose this data for now since we don't have a good use case
334335
- Knows whether to call the `loader` or the `action`
335336
- Allows users to pass _additional_ params to loaders/actions for middleware/context use cases.
336337
337338
```js
339+
// Simplest case - call all loaders in parallel just like current behavior
338340
function dataStrategy({ matches }) {
339341
// No more type, defaultStrategy, or match.route promise APIs!
342+
return Promise.all(matches.map(match => {
343+
// resolve `route.lazy` if needed and call loader/action
344+
return m.resolve();
345+
});
346+
}
340347

348+
// More advanced case - call loader sequentially passing a context through
349+
async function dataStrategy({ matches }) {
350+
let ctx = {};
351+
let results = [];
352+
for (let match of matches) {
353+
// You can pass a "handlerOverride" function to resolve giving you control
354+
// over how/if to call the handler. The argument passed to `handler` will
355+
// be passed as the second argument to your `loader`/`action`:
356+
// function loader({ request }, ctx) {...}
357+
let result = await m.resolve((handler) => {
358+
return handler(ctx);
359+
});
360+
results.push(result);
361+
});
362+
return results;
363+
}
364+
365+
// More performant case leveraging a middleware type abstraction which lets loaders
366+
// still run in parallel after sequential middlewares:
367+
function dataStrategy({ matches }) {
341368
// Can implement middleware as above since you now get all matches
342369
let context = runMiddlewares(matches);
343370

344371
// Call all loaders in parallel (no params to pass) but you _can_ pass you
345-
// own argument to `handler` and it will come in as `loader({ request }, handlerArg)`
372+
// own argument to `resolve` and it will come in as `loader({ request }, handlerArg)`
346373
// So you can send middleware context through to loaders/actions
347-
return Promise.all(matches.map(m => m.handler(context));
374+
return Promise.all(matches.map(match => {
375+
return m.resolve(context);
376+
});
348377

349378
// Note we don't do any filtering above - if a match doesn't need to load,
350-
// `match.handler` is no-op. Just like `serverLoader` is a no-op in `clientLoader`
379+
// `match.resolve` is no-op. Just like `serverLoader` is a no-op in `clientLoader`
351380
// when it doesn't need to run
352381
}
382+
383+
// Advanced case - single-fetch type approach
384+
// More advanced case - call loader sequentially passing a context through
385+
async function dataStrategy({ matches }) {
386+
let singleFetchData = await makeSingleFetchCall()
387+
// Assume we get back:
388+
// { data: { [routeId]: unknown }, errors: { [routeId]: unknown } }
389+
let results = [];
390+
for (let match of matches) {
391+
// Don't even call the handler since we have the data we need from single fetch
392+
let result = await m.resolve(() => {
393+
if (singleFetchData.errors?.[m.route.id]) {
394+
return {
395+
type: 'error',
396+
result: singleFetchData.errors?.[m.route.id]
397+
}
398+
}
399+
return {
400+
type: 'data',
401+
result: singleFetchData.data?.[m.route.id]
402+
}
403+
});
404+
results.push(result);
405+
});
406+
return results;
407+
}
353408
```
354409
355-
## Consequences
410+
## Status codes
411+
412+
Initially, we thought we could just let the `handlerOverride`return or throw and then internally we could convert the returned/thrown valuer into a `HandlerResult`. However, this didn't work for the `unstable_skipActionRevalidation` behavior we wanted to implement with Single Fetch.
413+
414+
If users returned normal Response's it would be fine, since we could decode the response internally and also know the status. However, if user's wanted to do custom response decoding (i.e., use `turbo-stream` like we did in single fetch) then there was no way to return/throw data _and the status code from the response_ without introducing something like the `ErrorResponse` API which holds a status and data. We decided to make `HandlerResult` public API and put an optional `status` field on it.
415+
416+
This means that if you just call resolve with no `handlerOverride` you never need to know about `HandlerResult`. If you do pass a `handlerOverride`, then you need to return a proper HandlerResult with `type:"data"|"error"`.
356417
357418
[single-fetch-issue]: https://github.com/remix-run/remix/issues/7641
358419
[single-fetch-rfc]: https://github.com/remix-run/remix/discussions/7640

0 commit comments

Comments
 (0)