Skip to content

Commit 6f17a30

Browse files
authored
fix: Decouple AbortController for revalidating fetchers (#10271)
1 parent f7f5519 commit 6f17a30

File tree

4 files changed

+254
-16
lines changed

4 files changed

+254
-16
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@remix-run/router": patch
3+
---
4+
5+
Decouple `AbortController` usage between revalidating fetchers and the thing that triggered them such that the unmount/deletion of a revalidating fetcher doesn't impact the ongoing triggering navigation/revalidation

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": "43.3 kB"
108+
"none": "44 kB"
109109
},
110110
"packages/react-router/dist/react-router.production.min.js": {
111111
"none": "13 kB"

packages/router/__tests__/router-test.ts

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9836,7 +9836,7 @@ describe("a router", () => {
98369836
state: "submitting",
98379837
});
98389838

9839-
// After acton resolves, both fetchers go into a loading state, with
9839+
// After action resolves, both fetchers go into a loading state, with
98409840
// the load fetcher still reflecting it's stale data
98419841
await C.actions.tasks.resolve("TASKS ACTION");
98429842
expect(t.router.state.fetchers.get(key)).toMatchObject({
@@ -10032,6 +10032,183 @@ describe("a router", () => {
1003210032
data: "C",
1003310033
});
1003410034
});
10035+
10036+
it("does not cancel pending action navigation on deletion of revalidating fetcher", async () => {
10037+
let t = setup({
10038+
routes: TASK_ROUTES,
10039+
initialEntries: ["/"],
10040+
hydrationData: { loaderData: { root: "ROOT", index: "INDEX" } },
10041+
});
10042+
expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);
10043+
10044+
let key1 = "key1";
10045+
let A = await t.fetch("/tasks/1", key1);
10046+
await A.loaders.tasksId.resolve("TASKS 1");
10047+
10048+
let C = await t.navigate("/tasks", {
10049+
formMethod: "post",
10050+
formData: createFormData({}),
10051+
});
10052+
// Add a helper for the fetcher that will be revalidating
10053+
t.shimHelper(C.loaders, "navigation", "loader", "tasksId");
10054+
10055+
// Resolve the action
10056+
await C.actions.tasks.resolve("TASKS ACTION");
10057+
10058+
// Fetcher should go back into a loading state
10059+
expect(t.router.state.fetchers.get(key1)).toMatchObject({
10060+
state: "loading",
10061+
data: "TASKS 1",
10062+
});
10063+
10064+
// Delete fetcher in the middle of the revalidation
10065+
t.router.deleteFetcher(key1);
10066+
expect(t.router.state.fetchers.get(key1)).toBeUndefined();
10067+
10068+
// Resolve navigation loaders
10069+
await C.loaders.root.resolve("ROOT*");
10070+
await C.loaders.tasks.resolve("TASKS LOADER");
10071+
10072+
expect(t.router.state).toMatchObject({
10073+
actionData: {
10074+
tasks: "TASKS ACTION",
10075+
},
10076+
errors: null,
10077+
loaderData: {
10078+
tasks: "TASKS LOADER",
10079+
root: "ROOT*",
10080+
},
10081+
});
10082+
expect(t.router.state.fetchers.size).toBe(0);
10083+
});
10084+
10085+
it("does not cancel pending loader navigation on deletion of revalidating fetcher", async () => {
10086+
let t = setup({
10087+
routes: TASK_ROUTES,
10088+
initialEntries: ["/"],
10089+
hydrationData: { loaderData: { root: "ROOT", index: "INDEX" } },
10090+
});
10091+
expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);
10092+
10093+
let key1 = "key1";
10094+
let A = await t.fetch("/tasks/1", key1);
10095+
await A.loaders.tasksId.resolve("TASKS 1");
10096+
10097+
// Loading navigation with query param to trigger revalidations
10098+
let C = await t.navigate("/tasks?key=value");
10099+
10100+
// Fetcher should go back into a loading state
10101+
expect(t.router.state.fetchers.get(key1)).toMatchObject({
10102+
state: "loading",
10103+
data: "TASKS 1",
10104+
});
10105+
10106+
// Delete fetcher in the middle of the revalidation
10107+
t.router.deleteFetcher(key1);
10108+
expect(t.router.state.fetchers.get(key1)).toBeUndefined();
10109+
10110+
// Resolve navigation loaders
10111+
await C.loaders.root.resolve("ROOT*");
10112+
await C.loaders.tasks.resolve("TASKS LOADER");
10113+
10114+
expect(t.router.state).toMatchObject({
10115+
errors: null,
10116+
loaderData: {
10117+
tasks: "TASKS LOADER",
10118+
root: "ROOT*",
10119+
},
10120+
});
10121+
expect(t.router.state.fetchers.size).toBe(0);
10122+
});
10123+
10124+
it("does not cancel pending router.revalidate() on deletion of revalidating fetcher", async () => {
10125+
let t = setup({
10126+
routes: TASK_ROUTES,
10127+
initialEntries: ["/"],
10128+
hydrationData: { loaderData: { root: "ROOT", index: "INDEX" } },
10129+
});
10130+
expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);
10131+
10132+
let key1 = "key1";
10133+
let A = await t.fetch("/tasks/1", key1);
10134+
await A.loaders.tasksId.resolve("TASKS 1");
10135+
10136+
// Trigger revalidations
10137+
let C = await t.revalidate();
10138+
10139+
// Fetcher should not go back into a loading state since it's a revalidation
10140+
expect(t.router.state.fetchers.get(key1)).toMatchObject({
10141+
state: "idle",
10142+
data: "TASKS 1",
10143+
});
10144+
10145+
// Delete fetcher in the middle of the revalidation
10146+
t.router.deleteFetcher(key1);
10147+
expect(t.router.state.fetchers.get(key1)).toBeUndefined();
10148+
10149+
// Resolve navigation loaders
10150+
await C.loaders.root.resolve("ROOT*");
10151+
await C.loaders.index.resolve("INDEX*");
10152+
10153+
expect(t.router.state).toMatchObject({
10154+
errors: null,
10155+
loaderData: {
10156+
root: "ROOT*",
10157+
index: "INDEX*",
10158+
},
10159+
});
10160+
expect(t.router.state.fetchers.size).toBe(0);
10161+
});
10162+
10163+
it("does not cancel pending fetcher submission on deletion of revalidating fetcher", async () => {
10164+
let key = "key";
10165+
let actionKey = "actionKey";
10166+
let t = setup({
10167+
routes: TASK_ROUTES,
10168+
initialEntries: ["/"],
10169+
hydrationData: { loaderData: { root: "ROOT", index: "INDEX" } },
10170+
});
10171+
10172+
// Load a fetcher
10173+
let A = await t.fetch("/tasks/1", key);
10174+
await A.loaders.tasksId.resolve("TASKS ID");
10175+
10176+
// Submit a fetcher, leaves loaded fetcher untouched
10177+
let C = await t.fetch("/tasks", actionKey, {
10178+
formMethod: "post",
10179+
formData: createFormData({}),
10180+
});
10181+
10182+
// After action resolves, both fetchers go into a loading state, with
10183+
// the load fetcher still reflecting it's stale data
10184+
await C.actions.tasks.resolve("TASKS ACTION");
10185+
expect(t.router.state.fetchers.get(key)).toMatchObject({
10186+
state: "loading",
10187+
data: "TASKS ID",
10188+
});
10189+
expect(t.router.state.fetchers.get(actionKey)).toMatchObject({
10190+
state: "loading",
10191+
data: "TASKS ACTION",
10192+
});
10193+
10194+
// Delete fetcher in the middle of the revalidation
10195+
t.router.deleteFetcher(key);
10196+
expect(t.router.state.fetchers.get(key)).toBeUndefined();
10197+
10198+
// Resolve only active route loaders since fetcher was deleted
10199+
await C.loaders.root.resolve("ROOT*");
10200+
await C.loaders.index.resolve("INDEX*");
10201+
10202+
expect(t.router.state.loaderData).toMatchObject({
10203+
root: "ROOT*",
10204+
index: "INDEX*",
10205+
});
10206+
expect(t.router.state.fetchers.get(key)).toBe(undefined);
10207+
expect(t.router.state.fetchers.get(actionKey)).toMatchObject({
10208+
state: "idle",
10209+
data: "TASKS ACTION",
10210+
});
10211+
});
1003510212
});
1003610213

1003710214
describe("fetcher ?index params", () => {

0 commit comments

Comments
 (0)