Skip to content

Commit 75bdd9b

Browse files
authored
Stabilize shouldCallHandler APIs (#14592)
1 parent 9766207 commit 75bdd9b

File tree

10 files changed

+588
-40
lines changed

10 files changed

+588
-40
lines changed

.changeset/bright-brooms-hammer.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Fix a Framework Mode bug where the `defaultShouldRevalidate` parameter to `shouldRevalidate` would not be correct after `action` returned a 4xx/5xx response (`true` when it should have been `false`)
6+
7+
- If your `shouldRevalidate` function relied on that parameter, you may have seen unintended revalidations

.changeset/witty-ears-itch.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"react-router": minor
3+
---
4+
5+
Stabilize the `dataStrategy` `match.shouldRevalidateArgs`/`match.shouldCallHandler()` APIs.
6+
7+
- The `match.shouldLoad` API is now marked deprecated in favor of these more powerful alternatives
8+
- If you're using this API in a custom `dataStrategy` today, you can swap to the new API at your convenience:
9+
10+
```tsx
11+
// Before
12+
const matchesToLoad = matches.filter((m) => m.shouldLoad);
13+
14+
// After
15+
const matchesToLoad = matches.filter((m) => m.shouldCallHandler());
16+
```
17+
18+
- `match.shouldRevalidateArgs` is the argument that will be passed to the route `shouldRevaliate` function
19+
- Combined with the parameter accepted by `match.shouldCallHandler`, you can define a custom revalidation behavior for your `dataStrategy`:
20+
21+
```tsx
22+
const matchesToLoad = matches.filter((m) => {
23+
const defaultShouldRevalidate = customRevalidationBehavior(
24+
match.shouldRevalidateArgs,
25+
);
26+
return m.shouldCallHandler(defaultShouldRevalidate);
27+
// The argument here will override the internal `defaultShouldRevalidate` value
28+
});
29+
```

integration/single-fetch-test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,107 @@ test.describe("single-fetch", () => {
736736
expect(urls).toEqual([]);
737737
});
738738

739+
test("provides proper defaultShouldRevalidate value on 4xx/5xx action responses", async ({
740+
page,
741+
}) => {
742+
let fixture = await createFixture({
743+
files: {
744+
...files,
745+
"app/routes/action.tsx": js`
746+
import { Form, Link, useActionData, useLoaderData, useNavigation, data } from 'react-router';
747+
748+
export async function action({ request }) {
749+
let fd = await request.formData();
750+
if (fd.get('throw') === "5xx") {
751+
throw data("Thrown 500", { status: 500 });
752+
}
753+
if (fd.get('throw') === "4xx") {
754+
throw data("Thrown 400", { status: 400 });
755+
}
756+
if (fd.get('return') === "5xx") {
757+
return data("Returned 500", { status: 500 });
758+
}
759+
if (fd.get('return') === "4xx") {
760+
return data("Returned 400", { status: 400 });
761+
}
762+
return null;
763+
}
764+
765+
let count = 0;
766+
export function loader() {
767+
return { count: ++count };
768+
}
769+
770+
export function shouldRevalidate({ defaultShouldRevalidate }) {
771+
return defaultShouldRevalidate;
772+
}
773+
774+
export default function Comp() {
775+
let navigation = useNavigation();
776+
let data = useLoaderData();
777+
return (
778+
<Form method="post">
779+
<button type="submit" name="throw" value="5xx">Throw 5xx</button>
780+
<button type="submit" name="throw" value="4xx">Throw 4xx</button>
781+
<button type="submit" name="return" value="5xx">Return 5xx</button>
782+
<button type="submit" name="return" value="4xx">Return 4xx</button>
783+
<p id="data">{data.count}</p>
784+
{navigation.state === "idle" ? <p id="idle">idle</p> : null}
785+
</Form>
786+
);
787+
}
788+
789+
export function ErrorBoundary() {
790+
return (
791+
<div>
792+
<h1 id="error">Error</h1>
793+
<Link to="/action">Back</Link>
794+
</div>
795+
);
796+
}
797+
`,
798+
},
799+
});
800+
801+
let urls: string[] = [];
802+
page.on("request", (req) => {
803+
if (req.method() === "GET" && req.url().includes(".data")) {
804+
urls.push(req.url());
805+
}
806+
});
807+
808+
console.error = () => {};
809+
810+
let appFixture = await createAppFixture(fixture);
811+
let app = new PlaywrightFixture(appFixture, page);
812+
await app.goto("/action");
813+
expect(await app.getHtml("#data")).toContain("1");
814+
expect(urls).toEqual([]);
815+
816+
await page.click('button[name="return"][value="5xx"]');
817+
await page.waitForSelector("#idle");
818+
expect(await app.getHtml("#data")).toContain("1");
819+
expect(urls).toEqual([]);
820+
821+
await page.click('button[name="return"][value="4xx"]');
822+
await page.waitForSelector("#idle");
823+
expect(await app.getHtml("#data")).toContain("1");
824+
expect(urls).toEqual([]);
825+
826+
await page.click('button[name="throw"][value="5xx"]');
827+
await page.waitForSelector("#error");
828+
expect(urls).toEqual([]);
829+
830+
await app.clickLink("/action");
831+
await page.waitForSelector("#data");
832+
expect(await app.getHtml("#data")).toContain("2");
833+
urls = [];
834+
835+
await page.click('button[name="throw"][value="4xx"]');
836+
await page.waitForSelector("#error");
837+
expect(urls).toEqual([]);
838+
});
839+
739840
test("returns headers correctly for singular loader and action calls", async () => {
740841
let fixture = await createFixture({
741842
files: {

0 commit comments

Comments
 (0)