Skip to content

unstable_navigationType reports "REPLACE" for loaders after an action error, while the navigation commits as "PUSH" #15618

Open

Description

@agcty

What happens

unstable_navigationType (added in #15586, dee9ad2) is documented as "the history action ("PUSH", "REPLACE", or "POP") for a navigation-initiated execution, including fetchers reloaded by that navigation".

A submission to the current location without an explicit replace starts as REPLACE (navigate() in router.ts). If the action throws, handleAction switches the committed action to PUSH so the user can go back to the form:

if ((opts && opts.replace) !== true) {
  pendingAction = NavigationType.Push;
}

startNavigation keeps passing its original historyAction (still REPLACE) into handleLoaders, which hands it to callLoadersAndMaybeResolveData(..., historyAction). So the post-action loader run, its client middleware, and any fetchers reloaded with it see unstable_navigationType: "REPLACE", and completeNavigation then calls history.push and sets state.historyAction to "PUSH".

The same stale value also lands on state.navigation.historyAction of the loading navigation built by getLoadingNavigation(location, matches, historyAction, opts.submission) after the action.

Minimal repro (in packages/react-router/__tests__/router/data-strategy-test.ts)

it("reports PUSH to loaders after an action error on a submission to the current location", async () => {
  let seen: Array<string | null> = [];
  let t = setup({
    routes: [
      {
        id: "root",
        path: "/",
        loader: true,
        children: [{ id: "page", path: "page", loader: true, action: true }],
      },
    ],
    initialEntries: ["/page"],
    hydrationData: { loaderData: { root: "ROOT", page: "PAGE" } },
    dataStrategy: async ({ matches, unstable_navigationType, request }) => {
      if (request.method === "GET") seen.push(unstable_navigationType);
      return Object.fromEntries(
        matches
          .filter((m) => m.shouldCallHandler())
          .map((m) =>
            request.method === "POST"
              ? [m.route.id, { type: "error", result: new Error("nope") }]
              : [m.route.id, { type: "data", result: m.route.id }],
          ),
      );
    },
  });

  await t.router.navigate("/page", {
    formMethod: "post",
    formData: createFormData({}),
  });

  expect(t.router.state.historyAction).toBe("PUSH"); // passes
  expect(seen).toEqual(["PUSH"]);                     // fails: ["REPLACE"]
});

Expected

Every data strategy call that belongs to the navigation reports the action the navigation commits with: "PUSH" here, matching router.state.historyAction and the history entry that is written.

Actual

The loaders, their middleware and reloaded fetchers receive "REPLACE"; the navigation commits as "PUSH".

Suggested fix

In startNavigation, after handleAction returns (and before getLoadingNavigation and handleLoaders), take the action handleAction settled on:

matches = actionResult.matches || matches;
pendingActionResult = actionResult.pendingActionResult;
historyAction = pendingAction; // handleAction may have switched REPLACE to PUSH on an action error
loadingNavigation = getLoadingNavigation(location, matches, historyAction, opts.submission);

Alternatively, handleAction could return the settled action alongside pendingActionResult, so the local value does not depend on the shared pendingAction variable. Either way the loading navigation's historyAction and unstable_navigationType then agree with the committed navigation. A test like the one above covers it.

Note: I reproduced this on a port of the router whose logic is identical to main at dee9ad2; I have not run this exact test inside this repository's own test setup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions