From 2ccdde4faf8d7bf1583a581f6960f1cdad2eb7ac Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 3 Jul 2024 11:09:22 -0400 Subject: [PATCH] Stabilize future.unstable_skipActionErrorRevalidation --- .changeset/gold-snakes-build.md | 9 +++ docs/route/should-revalidate.md | 6 +- docs/routers/create-browser-router.md | 4 +- docs/upgrading/future.md | 67 +++++++++++++++++++ packages/react-router-dom/server.tsx | 2 +- packages/router/__tests__/fetchers-test.ts | 2 +- .../__tests__/should-revalidate-test.ts | 10 +-- packages/router/router.ts | 12 ++-- packages/router/utils.ts | 2 +- 9 files changed, 95 insertions(+), 19 deletions(-) create mode 100644 .changeset/gold-snakes-build.md diff --git a/.changeset/gold-snakes-build.md b/.changeset/gold-snakes-build.md new file mode 100644 index 0000000000..884a0e96c3 --- /dev/null +++ b/.changeset/gold-snakes-build.md @@ -0,0 +1,9 @@ +--- +"@remix-run/router": minor +--- + +Stabilize `future.unstable_skipActionErrorRevalidation` as `future.v7_skipActionErrorRevalidation` + +- When this flag is enabled, actions will not automatically trigger a revalidation if they return/throw a `Response` with a `4xx`/`5xx` status code +- You may still opt-into revalidation via `shouldRevalidate` +- This also changes `shouldRevalidate`'s `unstable_actionStatus` parameter to `actionStatus` diff --git a/docs/route/should-revalidate.md b/docs/route/should-revalidate.md index 7148cdec41..536a38bc16 100644 --- a/docs/route/should-revalidate.md +++ b/docs/route/should-revalidate.md @@ -25,7 +25,7 @@ interface ShouldRevalidateFunctionArgs { formData?: Submission["formData"]; json?: Submission["json"]; actionResult?: any; - unstable_actionStatus?: number; + actionStatus?: number; defaultShouldRevalidate: boolean; } ``` @@ -40,8 +40,8 @@ There are several instances where data is revalidated, keeping your UI in sync w - After an [`action`][action] is called via: - [`
`][form], [``][fetcher], [`useSubmit`][usesubmit], or [`fetcher.submit`][fetcher] - - When the `future.unstable_skipActionErrorRevalidation` flag is enabled, `loaders` will not revalidate by default if the `action` returns or throws a 4xx/5xx `Response` - - You can opt-into revalidation for these scenarios via `shouldRevalidate` and the `unstable_actionStatus` parameter + - When the `future.v7_skipActionErrorRevalidation` flag is enabled, `loaders` will not revalidate by default if the `action` returns or throws a 4xx/5xx `Response` + - You can opt-into revalidation for these scenarios via `shouldRevalidate` and the `actionStatus` parameter - When an explicit revalidation is triggered via [`useRevalidator`][userevalidator] - When the [URL params][params] change for an already rendered route - When the URL Search params change diff --git a/docs/routers/create-browser-router.md b/docs/routers/create-browser-router.md index 5ff248dbc7..0183d1d12f 100644 --- a/docs/routers/create-browser-router.md +++ b/docs/routers/create-browser-router.md @@ -125,7 +125,7 @@ The following future flags are currently available: | `v7_partialHydration` | Support partial hydration for Server-rendered apps | | `v7_prependBasename` | Prepend the router basename to navigate/fetch paths | | [`v7_relativeSplatPath`][relativesplatpath] | Fix buggy relative path resolution in splat routes | -| `unstable_skipActionErrorRevalidation` | Do not revalidate by default if the action returns a 4xx/5xx `Response` | +| `v7_skipActionErrorRevalidation` | Do not revalidate by default if the action returns a 4xx/5xx `Response` | ## `opts.hydrationData` @@ -246,7 +246,7 @@ interface HandlerResult { - 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` - After the `action`, `dataStrategy` will be called again for the `loader` revalidation, and all matches will have `shouldLoad=true` (assuming no custom `shouldRevalidate` implementations) -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.unstable_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). +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). ### Example Use Cases diff --git a/docs/upgrading/future.md b/docs/upgrading/future.md index ffbf636994..dcfbfea06c 100644 --- a/docs/upgrading/future.md +++ b/docs/upgrading/future.md @@ -199,3 +199,70 @@ createBrowserRouter(routes, { }, }); ``` + +## v7_skipActionStatusRevalidation + +If you are not using a `createBrowserRouter` you can skip this + +When this flag is enabled, loaders will no longer revalidate by default after an action throws/returns a `Response` with a `4xx`/`5xx` status code. You may opt-into revalidation in these scenarios via `shouldRevalidate` and the `actionStatus` parameter. + +👉 **Enable the Flag** + +```tsx +createBrowserRouter(routes, { + future: { + v7_skipActionStatusRevalidation: true, + }, +}); +``` + +**Update your Code** + +In most cases, you probably won't have to make changes to your app code. Usually, if an action errors, it's unlikely data was mutated and needs revalidation. If any of your code _does_ mutate data in action error scenarios you have 2 options: + +👉 **Option 1: Change the `action` to avoid mutations in error scenarios** + +```js +// Before +async function action() { + await mutateSomeData(); + if (detectError()) { + throw new Response(error, { status: 400 }); + } + await mutateOtherData(); + // ... +} + +// After +async function action() { + if (detectError()) { + throw new Response(error, { status: 400 }); + } + // All data is now mutated after validations + await mutateSomeData(); + await mutateOtherData(); + // ... +} +``` + +👉 **Option 2: Opt-into revalidation via `shouldRevalidate` and `actionStatus`** + +```js +async function action() { + await mutateSomeData(); + if (detectError()) { + throw new Response(error, { status: 400 }); + } + await mutateOtherData(); +} + +async function loader() { ... } + +function shouldRevalidate({ actionStatus, defaultShouldRevalidate }) { + if (actionStatus != null && actionStatus >= 400) { + // Revalidate this loader when actions return a 4xx/5xx status + return true; + } + return defaultShouldRevalidate; +} +``` diff --git a/packages/react-router-dom/server.tsx b/packages/react-router-dom/server.tsx index 7938be3f3b..8ffb855aec 100644 --- a/packages/react-router-dom/server.tsx +++ b/packages/react-router-dom/server.tsx @@ -315,7 +315,7 @@ export function createStaticRouter( v7_partialHydration: opts.future?.v7_partialHydration === true, v7_prependBasename: false, v7_relativeSplatPath: opts.future?.v7_relativeSplatPath === true, - unstable_skipActionErrorRevalidation: false, + v7_skipActionErrorRevalidation: false, }; }, get state() { diff --git a/packages/router/__tests__/fetchers-test.ts b/packages/router/__tests__/fetchers-test.ts index 9969c249a2..0860f2e3bf 100644 --- a/packages/router/__tests__/fetchers-test.ts +++ b/packages/router/__tests__/fetchers-test.ts @@ -2106,6 +2106,7 @@ describe("fetchers", () => { expect(shouldRevalidate.mock.calls[0][0]).toMatchInlineSnapshot(` { "actionResult": null, + "actionStatus": undefined, "currentParams": { "a": "one", }, @@ -2122,7 +2123,6 @@ describe("fetchers", () => { }, "nextUrl": "http://localhost/two/three", "text": undefined, - "unstable_actionStatus": undefined, } `); diff --git a/packages/router/__tests__/should-revalidate-test.ts b/packages/router/__tests__/should-revalidate-test.ts index be7622b5a6..0f048304f1 100644 --- a/packages/router/__tests__/should-revalidate-test.ts +++ b/packages/router/__tests__/should-revalidate-test.ts @@ -368,7 +368,7 @@ describe("shouldRevalidate", () => { formAction: "/child", formEncType: "application/x-www-form-urlencoded", actionResult: "ACTION", - unstable_actionStatus: 201, + actionStatus: 201, }; expect(arg).toMatchObject(expectedArg); // @ts-expect-error @@ -709,6 +709,7 @@ describe("shouldRevalidate", () => { expect(arg).toMatchInlineSnapshot(` { "actionResult": "FETCH", + "actionStatus": undefined, "currentParams": {}, "currentUrl": "http://localhost/", "defaultShouldRevalidate": true, @@ -720,7 +721,6 @@ describe("shouldRevalidate", () => { "nextParams": {}, "nextUrl": "http://localhost/", "text": undefined, - "unstable_actionStatus": undefined, } `); expect(Object.fromEntries(arg.formData)).toEqual({ key: "value" }); @@ -773,6 +773,7 @@ describe("shouldRevalidate", () => { expect(arg).toMatchInlineSnapshot(` { "actionResult": undefined, + "actionStatus": undefined, "currentParams": {}, "currentUrl": "http://localhost/", "defaultShouldRevalidate": true, @@ -784,7 +785,6 @@ describe("shouldRevalidate", () => { "nextParams": {}, "nextUrl": "http://localhost/", "text": undefined, - "unstable_actionStatus": undefined, } `); @@ -1214,7 +1214,7 @@ describe("shouldRevalidate", () => { root: "ROOT", }, }, - future: { unstable_skipActionErrorRevalidation: true }, + future: { v7_skipActionErrorRevalidation: true }, }); router.initialize(); @@ -1282,7 +1282,7 @@ describe("shouldRevalidate", () => { root: "ROOT", }, }, - future: { unstable_skipActionErrorRevalidation: true }, + future: { v7_skipActionErrorRevalidation: true }, }); router.initialize(); diff --git a/packages/router/router.ts b/packages/router/router.ts index e6c0ff03b6..a2a88befbd 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -372,7 +372,7 @@ export interface FutureConfig { v7_partialHydration: boolean; v7_prependBasename: boolean; v7_relativeSplatPath: boolean; - unstable_skipActionErrorRevalidation: boolean; + v7_skipActionErrorRevalidation: boolean; } /** @@ -806,7 +806,7 @@ export function createRouter(init: RouterInit): Router { v7_partialHydration: false, v7_prependBasename: false, v7_relativeSplatPath: false, - unstable_skipActionErrorRevalidation: false, + v7_skipActionErrorRevalidation: false, ...init.future, }; // Cleanup function for history @@ -1891,7 +1891,7 @@ export function createRouter(init: RouterInit): Router { activeSubmission, location, future.v7_partialHydration && initialHydration === true, - future.unstable_skipActionErrorRevalidation, + future.v7_skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, @@ -2350,7 +2350,7 @@ export function createRouter(init: RouterInit): Router { submission, nextLocation, false, - future.unstable_skipActionErrorRevalidation, + future.v7_skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, @@ -4384,7 +4384,7 @@ function getMatchesToLoad( nextParams: nextRouteMatch.params, ...submission, actionResult, - unstable_actionStatus: actionStatus, + actionStatus, defaultShouldRevalidate: shouldSkipRevalidation ? false : // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate @@ -4463,7 +4463,7 @@ function getMatchesToLoad( nextParams: matches[matches.length - 1].params, ...submission, actionResult, - unstable_actionStatus: actionStatus, + actionStatus, defaultShouldRevalidate: shouldSkipRevalidation ? false : isRevalidationRequired, diff --git a/packages/router/utils.ts b/packages/router/utils.ts index 4e22db1566..905bcfe56d 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -210,7 +210,7 @@ export interface ShouldRevalidateFunctionArgs { text?: Submission["text"]; formData?: Submission["formData"]; json?: Submission["json"]; - unstable_actionStatus?: number; + actionStatus?: number; actionResult?: any; defaultShouldRevalidate: boolean; }