Skip to content

Commit d3abdc3

Browse files
authored
Send submission to shouldRevalidate on action redirects (#9777)
1 parent 3136786 commit d3abdc3

File tree

4 files changed

+226
-20
lines changed

4 files changed

+226
-20
lines changed

.changeset/fifty-kiwis-sniff.md

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+
Include submission info in `shouldRevalidate` on action redirects

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
},
108108
"filesize": {
109109
"packages/router/dist/router.umd.min.js": {
110-
"none": "37.5 kB"
110+
"none": "38 kB"
111111
},
112112
"packages/react-router/dist/react-router.production.min.js": {
113113
"none": "12.5 kB"

packages/router/__tests__/router-test.ts

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1671,6 +1671,112 @@ describe("a router", () => {
16711671
router.dispose();
16721672
});
16731673

1674+
it("includes submission on actions that return data", async () => {
1675+
let shouldRevalidate = jest.fn(() => true);
1676+
1677+
let history = createMemoryHistory({ initialEntries: ["/child"] });
1678+
let router = createRouter({
1679+
history,
1680+
routes: [
1681+
{
1682+
path: "/",
1683+
id: "root",
1684+
loader: () => "ROOT",
1685+
shouldRevalidate,
1686+
children: [
1687+
{
1688+
path: "child",
1689+
id: "child",
1690+
loader: () => "CHILD",
1691+
action: () => "ACTION",
1692+
},
1693+
],
1694+
},
1695+
],
1696+
});
1697+
router.initialize();
1698+
1699+
// Initial load - no existing data, should always call loader and should
1700+
// not give use ability to opt-out
1701+
await tick();
1702+
router.navigate("/child", {
1703+
formMethod: "post",
1704+
formData: createFormData({ key: "value" }),
1705+
});
1706+
await tick();
1707+
expect(shouldRevalidate.mock.calls.length).toBe(1);
1708+
// @ts-expect-error
1709+
let arg = shouldRevalidate.mock.calls[0][0];
1710+
expect(arg).toMatchObject({
1711+
currentParams: {},
1712+
currentUrl: new URL("http://localhost/child"),
1713+
nextParams: {},
1714+
nextUrl: new URL("http://localhost/child"),
1715+
defaultShouldRevalidate: true,
1716+
formMethod: "post",
1717+
formAction: "/child",
1718+
formEncType: "application/x-www-form-urlencoded",
1719+
actionResult: "ACTION",
1720+
});
1721+
// @ts-expect-error
1722+
expect(Object.fromEntries(arg.formData)).toEqual({ key: "value" });
1723+
1724+
router.dispose();
1725+
});
1726+
1727+
it("includes submission on actions that return redirects", async () => {
1728+
let shouldRevalidate = jest.fn(() => true);
1729+
1730+
let history = createMemoryHistory({ initialEntries: ["/child"] });
1731+
let router = createRouter({
1732+
history,
1733+
routes: [
1734+
{
1735+
path: "/",
1736+
id: "root",
1737+
loader: () => "ROOT",
1738+
shouldRevalidate,
1739+
children: [
1740+
{
1741+
path: "child",
1742+
id: "child",
1743+
loader: () => "CHILD",
1744+
action: () => redirect("/"),
1745+
},
1746+
],
1747+
},
1748+
],
1749+
});
1750+
router.initialize();
1751+
1752+
// Initial load - no existing data, should always call loader and should
1753+
// not give use ability to opt-out
1754+
await tick();
1755+
router.navigate("/child", {
1756+
formMethod: "post",
1757+
formData: createFormData({ key: "value" }),
1758+
});
1759+
await tick();
1760+
expect(shouldRevalidate.mock.calls.length).toBe(1);
1761+
// @ts-expect-error
1762+
let arg = shouldRevalidate.mock.calls[0][0];
1763+
expect(arg).toMatchObject({
1764+
currentParams: {},
1765+
currentUrl: new URL("http://localhost/child"),
1766+
nextParams: {},
1767+
nextUrl: new URL("http://localhost/"),
1768+
defaultShouldRevalidate: true,
1769+
formMethod: "post",
1770+
formAction: "/child",
1771+
formEncType: "application/x-www-form-urlencoded",
1772+
actionResult: undefined,
1773+
});
1774+
// @ts-expect-error
1775+
expect(Object.fromEntries(arg.formData)).toEqual({ key: "value" });
1776+
1777+
router.dispose();
1778+
});
1779+
16741780
it("provides the default implementation to the route function", async () => {
16751781
let rootLoader = jest.fn((args) => "ROOT");
16761782

@@ -1894,7 +2000,8 @@ describe("a router", () => {
18942000
data: "FETCH",
18952001
});
18962002

1897-
expect(shouldRevalidate.mock.calls[0][0]).toMatchInlineSnapshot(`
2003+
let arg = shouldRevalidate.mock.calls[0][0];
2004+
expect(arg).toMatchInlineSnapshot(`
18982005
Object {
18992006
"actionResult": "FETCH",
19002007
"currentParams": Object {},
@@ -1908,6 +2015,68 @@ describe("a router", () => {
19082015
"nextUrl": "http://localhost/",
19092016
}
19102017
`);
2018+
expect(Object.fromEntries(arg.formData)).toEqual({ key: "value" });
2019+
2020+
router.dispose();
2021+
});
2022+
2023+
it("applies to fetcher submissions when action redirects", async () => {
2024+
let shouldRevalidate = jest.fn((args) => true);
2025+
2026+
let history = createMemoryHistory();
2027+
let router = createRouter({
2028+
history,
2029+
routes: [
2030+
{
2031+
path: "",
2032+
id: "root",
2033+
2034+
children: [
2035+
{
2036+
path: "/",
2037+
id: "index",
2038+
loader: () => "INDEX",
2039+
shouldRevalidate,
2040+
},
2041+
{
2042+
path: "/fetch",
2043+
id: "fetch",
2044+
action: () => redirect("/"),
2045+
},
2046+
],
2047+
},
2048+
],
2049+
});
2050+
router.initialize();
2051+
await tick();
2052+
2053+
let key = "key";
2054+
router.fetch(key, "root", "/fetch", {
2055+
formMethod: "post",
2056+
formData: createFormData({ key: "value" }),
2057+
});
2058+
await tick();
2059+
expect(router.state.fetchers.get(key)).toMatchObject({
2060+
state: "idle",
2061+
data: undefined,
2062+
});
2063+
2064+
let arg = shouldRevalidate.mock.calls[0][0];
2065+
expect(arg).toMatchInlineSnapshot(`
2066+
Object {
2067+
"actionResult": undefined,
2068+
"currentParams": Object {},
2069+
"currentUrl": "http://localhost/",
2070+
"defaultShouldRevalidate": true,
2071+
"formAction": "/fetch",
2072+
"formData": FormData {},
2073+
"formEncType": "application/x-www-form-urlencoded",
2074+
"formMethod": "post",
2075+
"nextParams": Object {},
2076+
"nextUrl": "http://localhost/",
2077+
}
2078+
`);
2079+
expect(Object.fromEntries(arg.formData)).toEqual({ key: "value" });
19112080

19122081
router.dispose();
19132082
});

packages/router/router.ts

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,7 +1092,7 @@ export function createRouter(init: RouterInit): Router {
10921092
replace =
10931093
result.location === state.location.pathname + state.location.search;
10941094
}
1095-
await startRedirectNavigation(state, result, replace);
1095+
await startRedirectNavigation(state, result, { submission, replace });
10961096
return { shortCircuited: true };
10971097
}
10981098

@@ -1152,10 +1152,26 @@ export function createRouter(init: RouterInit): Router {
11521152
loadingNavigation = navigation;
11531153
}
11541154

1155+
// If this was a redirect from an action we don't have a "submission" but
1156+
// we have it on the loading navigation so use that if available
1157+
let activeSubmission = submission
1158+
? submission
1159+
: loadingNavigation.formMethod &&
1160+
loadingNavigation.formAction &&
1161+
loadingNavigation.formData &&
1162+
loadingNavigation.formEncType
1163+
? {
1164+
formMethod: loadingNavigation.formMethod,
1165+
formAction: loadingNavigation.formAction,
1166+
formData: loadingNavigation.formData,
1167+
formEncType: loadingNavigation.formEncType,
1168+
}
1169+
: undefined;
1170+
11551171
let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
11561172
state,
11571173
matches,
1158-
submission,
1174+
activeSubmission,
11591175
location,
11601176
isRevalidationRequired,
11611177
cancelledDeferredRoutes,
@@ -1244,7 +1260,7 @@ export function createRouter(init: RouterInit): Router {
12441260
// If any loaders returned a redirect Response, start a new REPLACE navigation
12451261
let redirect = findRedirect(results);
12461262
if (redirect) {
1247-
await startRedirectNavigation(state, redirect, replace);
1263+
await startRedirectNavigation(state, redirect, { replace });
12481264
return { shortCircuited: true };
12491265
}
12501266

@@ -1401,7 +1417,10 @@ export function createRouter(init: RouterInit): Router {
14011417
state.fetchers.set(key, loadingFetcher);
14021418
updateState({ fetchers: new Map(state.fetchers) });
14031419

1404-
return startRedirectNavigation(state, actionResult, false, true);
1420+
return startRedirectNavigation(state, actionResult, {
1421+
submission,
1422+
isFetchActionRedirect: true,
1423+
});
14051424
}
14061425

14071426
// Process any non-redirect errors thrown
@@ -1495,7 +1514,7 @@ export function createRouter(init: RouterInit): Router {
14951514

14961515
let redirect = findRedirect(results);
14971516
if (redirect) {
1498-
return startRedirectNavigation(state, redirect);
1517+
return startRedirectNavigation(state, redirect, { submission });
14991518
}
15001519

15011520
// Process and commit output from loaders
@@ -1673,8 +1692,15 @@ export function createRouter(init: RouterInit): Router {
16731692
async function startRedirectNavigation(
16741693
state: RouterState,
16751694
redirect: RedirectResult,
1676-
replace?: boolean,
1677-
isFetchActionRedirect?: boolean
1695+
{
1696+
submission,
1697+
replace,
1698+
isFetchActionRedirect,
1699+
}: {
1700+
submission?: Submission;
1701+
replace?: boolean;
1702+
isFetchActionRedirect?: boolean;
1703+
} = {}
16781704
) {
16791705
if (redirect.revalidate) {
16801706
isRevalidationRequired = true;
@@ -1714,24 +1740,30 @@ export function createRouter(init: RouterInit): Router {
17141740
let redirectHistoryAction =
17151741
replace === true ? HistoryAction.Replace : HistoryAction.Push;
17161742

1743+
// Use the incoming submission if provided, fallback on the active one in
1744+
// state.navigation
17171745
let { formMethod, formAction, formEncType, formData } = state.navigation;
1746+
if (!submission && formMethod && formAction && formData && formEncType) {
1747+
submission = {
1748+
formMethod,
1749+
formAction,
1750+
formEncType,
1751+
formData,
1752+
};
1753+
}
17181754

17191755
// If this was a 307/308 submission we want to preserve the HTTP method and
17201756
// re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
17211757
// redirected location
17221758
if (
17231759
redirectPreserveMethodStatusCodes.has(redirect.status) &&
1724-
formMethod &&
1725-
isMutationMethod(formMethod) &&
1726-
formEncType &&
1727-
formData
1760+
submission &&
1761+
isMutationMethod(submission.formMethod)
17281762
) {
17291763
await startNavigation(redirectHistoryAction, redirectLocation, {
17301764
submission: {
1731-
formMethod,
1765+
...submission,
17321766
formAction: redirect.location,
1733-
formEncType,
1734-
formData,
17351767
},
17361768
});
17371769
} else {
@@ -1741,10 +1773,10 @@ export function createRouter(init: RouterInit): Router {
17411773
overrideNavigation: {
17421774
state: "loading",
17431775
location: redirectLocation,
1744-
formMethod: formMethod || undefined,
1745-
formAction: formAction || undefined,
1746-
formEncType: formEncType || undefined,
1747-
formData: formData || undefined,
1776+
formMethod: submission ? submission.formMethod : undefined,
1777+
formAction: submission ? submission.formAction : undefined,
1778+
formEncType: submission ? submission.formEncType : undefined,
1779+
formData: submission ? submission.formData : undefined,
17481780
},
17491781
});
17501782
}

0 commit comments

Comments
 (0)