Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update/fix Single Fetch revalidation behavior #9938

Merged
merged 8 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/flat-wasps-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@remix-run/react": patch
"@remix-run/server-runtime": patch
---

Single Fetch - fix revalidation behavior bugs

- With Single Fetch, existing routes revalidate by default
- This means requests do not need special query params for granular route revalidations out of the box - i.e., `GET /a/b/c.data`
- There are two conditions that will trigger granular revalidation:
- If a route opts out of revalidation via `shouldRevalidate`, it will be excluded from the single fetch call
- If a route defines a `clientLoader` then it will be excluded from the single fetch call and if you call `serverLoader()` from your `clientLoader`, that will make a separarte HTTP call for just that route loader - i.e., `GET /a/b/c.data?_routes=routes/a` for a `clientLoader` in `routes/a.tsx`
- When one or more routes are excluded from the single fetch call, the remaining routes that have loaders are included as query params:
- For example, if A was excluded, and the `root` route and `routes/b` had a `loader` but `routes/c` did not, the single fetch request would be `GET /a/b/c.data?_routes=root,routes/a`
34 changes: 33 additions & 1 deletion docs/guides/single-fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ There are a handful of breaking changes introduced with Single Fetch - some of w
- Add `@remix-run/react/future/single-fetch.d.ts` to the end of your `tsconfig.json`'s `compilerOptions.types` array
- Begin using `unstable_defineLoader`/`unstable_defineAction` in your routes
- This can be done incrementally - you should have _mostly_ accurate type inference in your current state
- [**Default revalidation behavior changes to opt-out on GET navigations**][revalidation]: Default revalidation behavior on normal navigations changes from opt-in to opt-out and your server loaders will re-run by default
- [**Opt-in `action` revalidation**][action-revalidation]: Revalidation after an `action` `4xx`/`5xx` `Response` is now opt-in, versus opt-out

## Adding a New Route with Single Fetch
Expand Down Expand Up @@ -427,6 +428,32 @@ function handleBrowserRequest(

### Revalidations

#### Normal Navigation Behavior

In addition to the simpler mental model and the alignment of document and data requests, another benefit of Single Fetch is simpler (and hopefully better) caching behavior. Generally, Single Fetch will make fewer HTTP requests and hopefully cache those results more frequently compared to the previous multiple-fetch behavior.

To reduce cache fragmentation, Single Fetch changes the default revalidation behavior on GET navigations. Previously, Remix would not re-run loaders for reused ancestor routes unless you opted-in via `shouldRevalidate`. Now, Remix _will_ re-run those by default in the simple case for a Single Fetch request like `GET /a/b/c.data`. If you do not have any `shouldRevalidate` or `clientLoader` functions, this will be the behavior for your app.

Adding either a `shouldRevalidate` or a `clientLoader` to any of the active routes will trigger granular Single Fetch calls that include a `_routes` parameter specifying the subset of routes to run.

If a `clientLoader` calls `serverLoader()` internally, that will trigger a separate HTTP call for that specific route, akin to the old behavior.

For example, if you are on `/a/b` and you navigate to `/a/b/c`:

- When no `shouldRevalidate` or `clientLoader` functions exist: `GET /a/b/c.data`
- If all routes have loaders but `routes/a` opts out via `shouldRevalidate`:
- `GET /a/b/c.data?_routes=root,routes/b,routes/c`
- If all routes have loaders but `routes/b` has a `clientLoader`:
- `GET /a/b/c.data?_routes=root,routes/a,routes/c`
- And then if B's `clientLoader` calls `serverLoader()`:
- `GET /a/b/c.data?_routes=routes/b`

If this new behavior is sub-optimal for your application, you should be able to opt-back into the old behavior of not-revalidating by adding a `shouldRevalidate` that returns `false` in the desired scenarios to your parent routes.

Another option is to leverage a server-side cache for expensive parent loader calculations.

#### Submission Revalidation Behavior

Previously, Remix would always revalidate all active loaders after _any_ action submission, regardless of the result of the action. You could opt-out of revalidation on a per-route basis via [`shouldRevalidate`][should-revalidate].

With Single Fetch, if an `action` returns or throws a `Response` with a `4xx/5xx` status code, Remix will _not revalidate_ loaders by default. If an `action` returns or throws anything that is not a 4xx/5xx Response, then the revalidation behavior is unchanged. The reasoning here is that in most cases, if you return a `4xx`/`5xx` Response, you didn't actually mutate any data so there is no need to reload data.
Expand Down Expand Up @@ -458,9 +485,14 @@ Revalidation is handled via a `?_routes` query string parameter on the single fe
[merging-remix-and-rr]: https://remix.run/blog/merging-remix-and-react-router
[migration-guide]: #migrating-a-route-with-single-fetch
[breaking-changes]: #breaking-changes
[action-revalidation]: #streaming-data-format
[revalidation]: #normal-navigation-behavior
[action-revalidation]: #submission-revalidation-behavior
[start]: #enabling-single-fetch
[type-inference-section]: #type-inference
[compatibility-flag]: https://developers.cloudflare.com/workers/configuration/compatibility-dates
[data-utility]: ../utils/data
[augment]: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation

```

```
2 changes: 1 addition & 1 deletion integration/error-boundary-v2-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ test.describe("single fetch", () => {
await waitForAndAssert(
page,
app,
"#child-error",
"#parent-error",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When the entire single fetch call fails we'll report this error for all the routes so it'll naturally "bubble" to the parent

"Unable to decode turbo-stream response from URL"
);
});
Expand Down
2 changes: 1 addition & 1 deletion integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@remix-run/dev": "workspace:*",
"@remix-run/express": "workspace:*",
"@remix-run/node": "workspace:*",
"@remix-run/router": "0.0.0-experimental-7d87ffb8c",
"@remix-run/router": "1.19.2-pre.0",
"@remix-run/server-runtime": "workspace:*",
"@types/express": "^4.17.9",
"@vanilla-extract/css": "^1.10.0",
Expand Down
Loading