Skip to content

Commit

Permalink
fix: Decouple AbortController for revalidating fetchers (#10271)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 authored Mar 29, 2023
1 parent f7f5519 commit 6f17a30
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/revalidating-fetcher-controller.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/router": patch
---

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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
},
"filesize": {
"packages/router/dist/router.umd.min.js": {
"none": "43.3 kB"
"none": "44 kB"
},
"packages/react-router/dist/react-router.production.min.js": {
"none": "13 kB"
Expand Down
179 changes: 178 additions & 1 deletion packages/router/__tests__/router-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9836,7 +9836,7 @@ describe("a router", () => {
state: "submitting",
});

// After acton resolves, both fetchers go into a loading state, with
// After action resolves, both fetchers go into a loading state, with
// the load fetcher still reflecting it's stale data
await C.actions.tasks.resolve("TASKS ACTION");
expect(t.router.state.fetchers.get(key)).toMatchObject({
Expand Down Expand Up @@ -10032,6 +10032,183 @@ describe("a router", () => {
data: "C",
});
});

it("does not cancel pending action navigation on deletion of revalidating fetcher", async () => {
let t = setup({
routes: TASK_ROUTES,
initialEntries: ["/"],
hydrationData: { loaderData: { root: "ROOT", index: "INDEX" } },
});
expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);

let key1 = "key1";
let A = await t.fetch("/tasks/1", key1);
await A.loaders.tasksId.resolve("TASKS 1");

let C = await t.navigate("/tasks", {
formMethod: "post",
formData: createFormData({}),
});
// Add a helper for the fetcher that will be revalidating
t.shimHelper(C.loaders, "navigation", "loader", "tasksId");

// Resolve the action
await C.actions.tasks.resolve("TASKS ACTION");

// Fetcher should go back into a loading state
expect(t.router.state.fetchers.get(key1)).toMatchObject({
state: "loading",
data: "TASKS 1",
});

// Delete fetcher in the middle of the revalidation
t.router.deleteFetcher(key1);
expect(t.router.state.fetchers.get(key1)).toBeUndefined();

// Resolve navigation loaders
await C.loaders.root.resolve("ROOT*");
await C.loaders.tasks.resolve("TASKS LOADER");

expect(t.router.state).toMatchObject({
actionData: {
tasks: "TASKS ACTION",
},
errors: null,
loaderData: {
tasks: "TASKS LOADER",
root: "ROOT*",
},
});
expect(t.router.state.fetchers.size).toBe(0);
});

it("does not cancel pending loader navigation on deletion of revalidating fetcher", async () => {
let t = setup({
routes: TASK_ROUTES,
initialEntries: ["/"],
hydrationData: { loaderData: { root: "ROOT", index: "INDEX" } },
});
expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);

let key1 = "key1";
let A = await t.fetch("/tasks/1", key1);
await A.loaders.tasksId.resolve("TASKS 1");

// Loading navigation with query param to trigger revalidations
let C = await t.navigate("/tasks?key=value");

// Fetcher should go back into a loading state
expect(t.router.state.fetchers.get(key1)).toMatchObject({
state: "loading",
data: "TASKS 1",
});

// Delete fetcher in the middle of the revalidation
t.router.deleteFetcher(key1);
expect(t.router.state.fetchers.get(key1)).toBeUndefined();

// Resolve navigation loaders
await C.loaders.root.resolve("ROOT*");
await C.loaders.tasks.resolve("TASKS LOADER");

expect(t.router.state).toMatchObject({
errors: null,
loaderData: {
tasks: "TASKS LOADER",
root: "ROOT*",
},
});
expect(t.router.state.fetchers.size).toBe(0);
});

it("does not cancel pending router.revalidate() on deletion of revalidating fetcher", async () => {
let t = setup({
routes: TASK_ROUTES,
initialEntries: ["/"],
hydrationData: { loaderData: { root: "ROOT", index: "INDEX" } },
});
expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);

let key1 = "key1";
let A = await t.fetch("/tasks/1", key1);
await A.loaders.tasksId.resolve("TASKS 1");

// Trigger revalidations
let C = await t.revalidate();

// Fetcher should not go back into a loading state since it's a revalidation
expect(t.router.state.fetchers.get(key1)).toMatchObject({
state: "idle",
data: "TASKS 1",
});

// Delete fetcher in the middle of the revalidation
t.router.deleteFetcher(key1);
expect(t.router.state.fetchers.get(key1)).toBeUndefined();

// Resolve navigation loaders
await C.loaders.root.resolve("ROOT*");
await C.loaders.index.resolve("INDEX*");

expect(t.router.state).toMatchObject({
errors: null,
loaderData: {
root: "ROOT*",
index: "INDEX*",
},
});
expect(t.router.state.fetchers.size).toBe(0);
});

it("does not cancel pending fetcher submission on deletion of revalidating fetcher", async () => {
let key = "key";
let actionKey = "actionKey";
let t = setup({
routes: TASK_ROUTES,
initialEntries: ["/"],
hydrationData: { loaderData: { root: "ROOT", index: "INDEX" } },
});

// Load a fetcher
let A = await t.fetch("/tasks/1", key);
await A.loaders.tasksId.resolve("TASKS ID");

// Submit a fetcher, leaves loaded fetcher untouched
let C = await t.fetch("/tasks", actionKey, {
formMethod: "post",
formData: createFormData({}),
});

// After action resolves, both fetchers go into a loading state, with
// the load fetcher still reflecting it's stale data
await C.actions.tasks.resolve("TASKS ACTION");
expect(t.router.state.fetchers.get(key)).toMatchObject({
state: "loading",
data: "TASKS ID",
});
expect(t.router.state.fetchers.get(actionKey)).toMatchObject({
state: "loading",
data: "TASKS ACTION",
});

// Delete fetcher in the middle of the revalidation
t.router.deleteFetcher(key);
expect(t.router.state.fetchers.get(key)).toBeUndefined();

// Resolve only active route loaders since fetcher was deleted
await C.loaders.root.resolve("ROOT*");
await C.loaders.index.resolve("INDEX*");

expect(t.router.state.loaderData).toMatchObject({
root: "ROOT*",
index: "INDEX*",
});
expect(t.router.state.fetchers.get(key)).toBe(undefined);
expect(t.router.state.fetchers.get(actionKey)).toMatchObject({
state: "idle",
data: "TASKS ACTION",
});
});
});

describe("fetcher ?index params", () => {
Expand Down
Loading

0 comments on commit 6f17a30

Please sign in to comment.