Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: useResolvedPath and useNavigate use different logic #8985

Merged
merged 6 commits into from Jun 21, 2022

Conversation

david-crespo
Copy link
Contributor

@david-crespo david-crespo commented Jun 14, 2022

Discussed in Discord (thread). #8697 and #8954 updated the logic in useNavigate without making the corresponding change to useResolvedPath, which is used to set the href on Links. This means there are cases where the href is correct but the navigate behavior is wrong and vice versa.

Going to try to add a test that reproduces the bug I was seeing.

@changeset-bot
Copy link

changeset-bot bot commented Jun 14, 2022

🦋 Changeset detected

Latest commit: e70cf75

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
react-router Patch
react-router-dom Patch
react-router-dom-v5-compat Patch
react-router-native Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@remix-cla-bot
Copy link
Contributor

remix-cla-bot bot commented Jun 14, 2022

Hi @david-crespo,

Welcome, and thank you for contributing to React Router!

Before we consider your pull request, we ask that you sign our Contributor License Agreement (CLA). We require this only once.

You may review the CLA and sign it by adding your name to contributors.yml.

Once the CLA is signed, the CLA Signed label will be added to the pull request.

If you have already signed the CLA and received this response in error, or if you have any questions, please contact us at hello@remix.run.

Thanks!

- The Remix team

@remix-cla-bot
Copy link
Contributor

remix-cla-bot bot commented Jun 14, 2022

Thank you for signing the Contributor License Agreement. Let's get this merged! 🥳

@david-crespo david-crespo changed the title Share logic between useResolvedPath and useNavigate Fix: useResolvedPath and useNavigate use different logic Jun 14, 2022
@david-crespo
Copy link
Contributor Author

david-crespo commented Jun 14, 2022

I see the failing tests, taking a look. Seems like maybe the pathContributingRoutes logic is not quite right.

@brophdawg11 brophdawg11 self-assigned this Jun 14, 2022
@david-crespo
Copy link
Contributor Author

I noticed that in this test (which this PR causes to fail), the description does not match what is asserted. This PR makes it the root URL and not /about.

test('<Link to=".."> with more .. segments than parent routes resolves to the root URL', () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/inbox/messages"]}>
<Routes>
<Route path="inbox">
<Route path="messages" element={<Link to="../../about" />} />
</Route>
</Routes>
</MemoryRouter>
);
});
expect(renderer.root.findByType("a").props.href).toEqual("/about");
});
});

@@ -58,7 +58,7 @@ describe("<Link> href", () => {
<MemoryRouter initialEntries={["/inbox/messages"]}>
<Routes>
<Route path="inbox">
<Route path="messages" element={<Link to="../../about" />} />
<Route path="messages" element={<Link to="../../../about" />} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was incorrect before - ../../about would be the correct # of ..'s. The test passed but it wasn't asserting having more segments than parent routes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this link going completely out of the routes? .. traverses the route hierarchy, so:

  • the route hierarchy at /inbox/messages is [inbox, messages]
  • .. goes to [inbox]
  • ../.. goes to []
  • ../../.. goes completely out of everything

can you help me understand?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the description, I thought that was the point of this test — showing that even if you accidentally put too many .. you’ll still get / instead of an error or something nonsensical.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that was my understanding as well from the test description, proving that links are "safe" insofar that you can't goof yourself by doing too many

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this test and the one above in the original commit they were added, it seems clear this is just about whether there are more .. than parent Route components. The first test shows that .. takes you to the parent Route, and the second shows that you can resolve ../../ to / even when there is no explicit <Route path="/">. So it does seem like ../../about is enough to satisfy the goal of the test. I changed them back.

It could be worth changing more .. segments than parent routes to more .. segments than parent <Route>s or something to avoid this confusion.

test('<Link to=".."> resolves relative to the parent route', () => {
let renderer = createTestRenderer(
<MemoryRouter initialEntries={["/inbox/messages"]}>
<Routes>
<Route path="inbox">
<Route path="messages" element={<Link to=".." />} />
</Route>
</Routes>
</MemoryRouter>
);
expect(renderer.root.findByType("a").props.href).toEqual("/inbox");
});
test('<Link to=".."> with more .. segments than parent routes resolves to the root URL', () => {
let renderer = createTestRenderer(
<MemoryRouter initialEntries={["/inbox/messages"]}>
<Routes>
<Route path="inbox">
<Route path="messages" element={<Link to="../../about" />} />
</Route>
</Routes>
</MemoryRouter>
);
expect(renderer.root.findByType("a").props.href).toEqual("/about");
});
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine if we change them back 👍 . Michael's on vacation this week so we won't be able to get any clarity on exactly what he may have envisioned as the purpose for the tests so probably best to leave them unchanged. My main concern was that we weren't properly asserting the following line in resolveTo:

    // If there are more ".." segments than parent routes, resolve relative to
    // the root / URL.
    from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";

But - funny enough, even if we change that to from = routePathnames[routePathnameIndex] everything still works since from being undefined causes it to default to "/" in resolvePath anyway so we still get the same behavior.

As to the original commit intent - I think it makes sense that ../../about still passes - I would argue that it's not asserting the "more than" aspect of the description. ../../about should go back to /about because its correctly traversing the exact number of parents that it has. Adding the third .. tests the scenario that you use "more than". I altered the test locally to show how I'm thinking about it:

    test('<Link to=".."> with more .. segments than parent routes resolves to the root URL', () => {
      let renderer: TestRenderer.ReactTestRenderer;
      TestRenderer.act(() => {
        renderer = TestRenderer.create(
          <MemoryRouter initialEntries={["/inbox/messages"]}>
            <Routes>
              <Route path="inbox">
                <Route
                  path="messages"
                  element={
                    <>
                      <Link to="./about" />        // 1
                      <Link to="../about" />       // 2
                      <Link to="../../about" />    // 3
                      <Link to="../../../about" /> // 4
                    </>
                  }
                />
              </Route>
            </Routes>
          </MemoryRouter>
        );
      });

      expect(renderer.root.findAllByType("a").map((a) => a.props.href)).toEqual([
        "/inbox/messages/about", 
        "/inbox/about", 
        "/about",
        "/about",
      ]);
    });
  • Link 2 and 3 assert the same thing - that we can traverse upwards with ..
  • Link 4 asserts something different - specifically when we have "more than"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do like the clarity of having all 4!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry, I reviewed too quickly, the test says "more than parents" so I like this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

K, I’ll revert my revert.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spoke to Ryan - going to merge this as-is - these existing tests are a bit tangentially related so I'll re-make the changes in another PR 👍

@@ -132,7 +132,7 @@ describe("<Link> href", () => {
<Route path="inbox">
<Route
path="messages/:id"
element={<Link to="../../about" />}
element={<Link to="../../../about" />}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

@@ -193,7 +193,7 @@ describe("<Link> href", () => {
);
});

expect(renderer.root.findByType("a").props.href).toEqual("/inbox");
expect(renderer.root.findByType("a").props.href).toEqual("/");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This .. should ignore the index route and thus be relative to inbox which would resolve to the root. The incorrect nature of this test can be seen by comparing to the test above where in this same layout, <Link to="."> resolves to /inbox - so .. should resolve to /

@@ -278,7 +278,7 @@ describe("<Link> href", () => {
);
});

expect(renderer.root.findByType("a").props.href).toEqual("/inbox");
expect(renderer.root.findByType("a").props.href).toEqual("/");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

@brophdawg11 brophdawg11 marked this pull request as ready for review June 15, 2022 14:07
@brophdawg11
Copy link
Contributor

Thanks for the PR @david-crespo! I agree some of those existing tests were incorrect given the bug fix this is addressing 👍 . Going to kick this over to @ryanflorence for a final approval but this should be good to go.

let routePathnamesJson = JSON.stringify(
pathContributingMatches.map((match) => match.pathnameBase)
getPathContributingMatches(matches).map((match) => match.pathnameBase)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a feeling doing the JSON.stringify inline in both spots was where it would end up.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it certainly worked fine, just felt a little distanced in the shared util since it's a dependency implementation detail for the individual hooks

brophdawg11 pushed a commit that referenced this pull request Mar 27, 2024
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants