Skip to content

Commit b60066d

Browse files
committed
fix: use a push navigation on submission errors
1 parent e8dda1b commit b60066d

File tree

3 files changed

+40
-1
lines changed

3 files changed

+40
-1
lines changed

docs/components/form.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ Instructs the form to replace the current entry in the history stack, instead of
180180
The default behavior is conditional on the form `method`:
181181

182182
- `get` defaults to `false`
183-
- every other method defaults to `true`
183+
- every other method defaults to `true` if your `action` is successful
184+
- if your `action` redirects or throws, then it will still push by default
184185

185186
We've found with `get` you often want the user to be able to click "back" to see the previous search results/filters, etc. But with the other methods the default is `true` to avoid the "are you sure you want to resubmit the form?" prompt. Note that even if `replace={false}` React Router _will not_ resubmit the form when the back button is clicked and the method is post, put, patch, or delete.
186187

packages/router/__tests__/router-test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2331,6 +2331,35 @@ describe("a router", () => {
23312331
expect(t.router.state.location.pathname).toEqual("/foo");
23322332
});
23332333

2334+
it("navigates correctly using POP navigations across action errors", async () => {
2335+
let t = initializeTmTest();
2336+
2337+
// Navigate to /foo
2338+
let A = await t.navigate("/foo");
2339+
await A.loaders.foo.resolve("FOO");
2340+
expect(t.router.state.location.pathname).toEqual("/foo");
2341+
2342+
// Navigate to /bar
2343+
let B = await t.navigate("/bar");
2344+
await B.loaders.bar.resolve("BAR");
2345+
expect(t.router.state.location.pathname).toEqual("/bar");
2346+
2347+
// Post to /bar (should push due to our error)
2348+
let C = await t.navigate("/bar", {
2349+
formMethod: "post",
2350+
formData: createFormData({ key: "value" }),
2351+
});
2352+
await C.actions.bar.reject("BAR ERROR");
2353+
await C.loaders.root.resolve("ROOT");
2354+
await C.loaders.bar.resolve("BAR");
2355+
expect(t.router.state.location.pathname).toEqual("/bar");
2356+
2357+
// POP to /bar
2358+
let D = await t.navigate(-1);
2359+
await D.loaders.bar.resolve("BAR");
2360+
expect(t.router.state.location.pathname).toEqual("/bar");
2361+
});
2362+
23342363
it("navigates correctly using POP navigations across loader redirects", async () => {
23352364
// Start at / (history stack: [/])
23362365
let t = initializeTmTest();

packages/router/router.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,15 @@ export function createRouter(init: RouterInit): Router {
901901
// Store off the pending error - we use it to determine which loaders
902902
// to call and will commit it when we complete the navigation
903903
let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
904+
905+
// By default, all submissions are REPLACE navigations, but if the
906+
// action threw an error that'll be rendered in an errorElement, we fall
907+
// back to PUSH so that the user can use the back button to get back to
908+
// the pre-submission form location to try again
909+
if (opts?.replace !== true) {
910+
pendingAction = HistoryAction.Push;
911+
}
912+
904913
return {
905914
pendingActionError: { [boundaryMatch.route.id]: result.error },
906915
};

0 commit comments

Comments
 (0)