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

[Bug]: useLocation inside <Routes location={location}> doesn't return given location #8470

Closed
gitcatrat opened this issue Dec 12, 2021 · 24 comments · Fixed by #9094
Closed
Labels

Comments

@gitcatrat
Copy link

gitcatrat commented Dec 12, 2021

What version of React Router are you using?

6.1.1

Steps to Reproduce

Full reproduction can be found here, it's functionally similar to v5 modal gallery example:
https://codesandbox.io/s/heuristic-rhodes-508k5?file=/src/App.js

Steps to replicate:

  1. Click on Post link
  2. Home page should now use previous "background" location
  3. Notice how useLocation in Home still returns new location

Expected Behavior

useLocation inside <Routes location={location}> returns the prop location.
useLocation inside <Routes> returns the "correct" currently active location.

Actual Behavior

Both useLocation return "correct" currently active location (as the one corresponding to URL bar).

I.e issue seems to be that useLocation is not scoped, it only reads global location.

@gitcatrat gitcatrat added the bug label Dec 12, 2021
@liuhanqu
Copy link
Contributor

  let location = React.useMemo(() => {
    let trailingPathname = stripBasename(pathname, basename);

    if (trailingPathname == null) {
      return null;
    }

    return {
      pathname: trailingPathname,
      search,
      hash,
      state,
      key
    };
  }, [basename, pathname, search, hash, state, key]);

The location would change if it's dependencies change. You can see the code is in the Router component.

@gitcatrat
Copy link
Author

@liuhanqu Yup you're right. But the issue here seems to be that there's only one location in context.

I expect this behavior:
useLocation inside <Routes location={location}> returns the prop location.
useLocation inside <Routes> returns the "correct" currently active location.

@gitcatrat
Copy link
Author

gitcatrat commented Dec 23, 2021

I had to use a temporary workaround for now because this is blocking my huge release!

  1. wrap the Routes with location prop with custom Context and pass the same prop into it.
export const Location = createContext();

export default function Routes() {
  const location = useLocation();
  
  let background = null;
  
  if (location?.state?.modal && location?.state?.background) {
    // "layered" logic might work differently in your app but
    // when modal is opened, I've got the following state:
    // {modal: true, background: previousLocation}
    background = location.state.background;
  }

  return (
    <>
      <Location.Provider value={background || location}> {/* this right here officer */}
        <Routes location={background || location}>  
          {/* normal/background routes */}
        </Routes> 
      </Location.Provider>
    
      {background &&
        <Routes>
          {/* overlay/modal routes */}
        </Routes>
      }
    </>
  );
}
  1. Make a custom useLocation that tries to figure out where it is based on context existence.
export default function useLocation() {
  const scopedLocation = useContext(Location);
  const globalLocation = useReactRouterLocation(); // "real" useLocation 
  const modalIsOpen = scopedLocation?.pathname !== globalLocation?.pathname;
  const thisHookIsCalledFromModal = modalIsOpen && !scopedLocation && globalLocation?.state?.modal;
  return (thisHookIsCalledFromModal ? globalLocation : scopedLocation);
}

Dadaa! Now useLocation returns the correct location in every situation!

@bloomdido
Copy link

Gah! This bug just cost me a few hours...

Thanks @gitcatrat for reporting and giving a workaround!

@bloomdido
Copy link

Unfortunately, I'm using useSearchParams() to read values out of the location, which is coded to use the unscoped useLocation() hook, so I'm going to have to do a bit more work on my end.

@mrwwalmsley
Copy link

mrwwalmsley commented Mar 23, 2022

This issue also cost me a few hours!

I'm also using useSearchParams() to read values out of the location. I am using a CSS TransitionBlock

 <TransitionBlock id={location.key || ' '} fullScreen>
    <Routes location={location}>
      <Route path="/menu/a" element={<A />} />
      <Route path="/menu/b" element={<B />} />
    </Routes>
  </TransitionBlock>

While navigating from page A to page B, both pages render with the current location state.
This was not the behaviour with the Switch. This breaks some transitions away from pages that use useSearchParams.

This is the last thing I have to do to complete upgrade to ReactRouter v6.

@SleeplessByte
Copy link

In the source code we can see that indeed useLocation does not use the injected location prop. Without creating the custom hook like @gitcatrat described, you can do this:

import { Action } from 'history';
import {  UNSAFE_LocationContext as LocationContext } from 'react-router-dom'

// wherever you need the stored location, or anywhere higher in the tree
<LocationContext.Provider value={{ location: storedLocation, navigationType: Action.Pop }}>
  {...}
</LocationContext.Provider>

I expected #8008 to mention this issue as well.

@evoyy
Copy link

evoyy commented May 7, 2022

Well, this sucks. This breaks functionality for anyone using the background modal technique that dates all the way back to React Router v1. The modal example in the official documentation is also affected by this.

As mentioned above, the location object given as a prop to Routes and then passed on to useRoutes, is not used. Only the pathname property of the passed location is used.

The workaround using a custom location context feels a bit hackish and requires making changes to any components that use location for logic (or undesired things will happen behind the modal). I considered reverting to v5 but for now I have simply reset my isModal state to false before rendering my routes, which causes a navigation instead of opening the modal.

Michael or Tim @mjackson @timdorr, please could you share your opinion on this issue?

@SleeplessByte
Copy link

You don't need to make changes to all components that use location for logic. You only need it around the modals. A single context provider would work.

(That said, it feels very hacky)

@evoyy
Copy link

evoyy commented May 8, 2022

@SleeplessByte I tried your workaround but it doesn't seem to work with components that use other hooks such as useParams(). The path matching seems to be broken. It works when I create my own LocationContext and use it to pass down my stored location:

export const LocationContext = createContext();
<LocationContext.Provider value={isModal ? prevLocation : location}>
...
</LocationContext.Provider>

Then I have to do this across my whole codebase:

-  import { useLocation } from 'react-router-dom';
+  import { LocationContext } from 'myApp';

-  const location = useLocation();
+  const location = useContext(LocationContext);

@SleeplessByte
Copy link

I think the provider may not be at the "right" location then. I use this workaround in production.

Using your own context (and hook) definitely should work 😁

@evoyy
Copy link

evoyy commented May 8, 2022

@SleeplessByte You were right, the scope of my provider was too large and so I was passing the wrong location to my modal (God, this is confusing!!). Your workaround is a nice solution. Thanks for your help!

The only change, instead of importing Action from history, I'm getting the navigation type from the hook:

const navigationType = useNavigationType();

@SleeplessByte
Copy link

Yeah. That should work. Instead of hardcoding it, that should actually retrieve the value dynamically.

@loicmagne
Copy link

any plans to solve this ?

@zhouzi
Copy link

zhouzi commented Jun 2, 2022

It would be great to know if the maintainers agree that it's a bug and that <Routes /> should provide the new location through the location context. We could then open a PR to propose a fix (I would be happy to give it a try).

@brophdawg11
Copy link
Contributor

I'm going to defer to @mjackson and @ryanflorence here (as they've got the historical insight) but this doesn't feel like a bug to me. The Location concept is basically a wrapper around window.location which is a singleton -- it's impossible to have 2 separate window.location's and thus it feels odd to me if useLocation can actually have different contextual values based on where you are in the tree.

If we made the change in #9094, wouldn't that just introduce the opposite problem in that contextual components could never actually access the true global location anymore? Seems like we're trading one problem for another.

In the example provided, couldn't components that need to use a contextual location just do something like the following:

const location = useLocation();
const contextualLocation = location.state?.background || location; 

That now leaves the decision of which location to use in the hands of the developer without prohibiting contextual components from accessing the global location?

@SleeplessByte
Copy link

It feels very weird to have to:

  • either import UNSAFE_XXX...
  • or create a new wrapper and new useXXX hooks.

I would be completely okay with keeping the workaround I listed, but I do think something needs to be done in order to support #8008.

@zhouzi
Copy link

zhouzi commented Jul 26, 2022

it's impossible to have 2 separate window.location's and thus it feels odd to me if useLocation can actually have different contextual values based on where you are in the tree.

This is particularly useful for when you have a modal that has its own URL. For example: https://v5.reactrouter.com/web/example/modal-gallery

If we made the change in #9094, wouldn't that just introduce the opposite problem in that contextual components could never actually access the true global location anymore?

The idea is that the components that are receiving the "background" location work as usual. They are not aware of the concept of background/foreground location. To be honest I have never come across a use case where the background location would need to access the "global" location.

const location = useLocation();
const contextualLocation = location.state?.background || location; 

That now leaves the decision of which location to use in the hands of the developer without prohibiting contextual components from accessing the global location?

The problem is that the components where we need this behavior are <Route /> and anything related to the location (e.g hooks). For example:

<Routes location={location.state?.background ?? location}>
  <Route path="/first" element={<Page1 />} exact /> // Route should read from the location above
</Routes>

@johnpangalos
Copy link
Contributor

@brophdawg11 I can change #9094 to add the global location to the LocationContext and add a hook to fetch global location if someone would want that. That way the current behavior could still be accessed if needed.

@flying-sheep
Copy link

flying-sheep commented Aug 4, 2022

I totally agree with @zhouzi and @johnpangalos.

@brophdawg11 my intuition about Location is the same as react-helmet-async, i.e. there’s a “global default” derived from browser APIs (window.location here, window.document’s DOM for Helmet). Users (or library authors) can choose to override those values. The truth is fetched from the closest overriding parent component (in case of multiple nested ones) and if there’s no override, the browser’s global default wins out.

I didn’t consider the possiblility that the current behavior might possibly be intentional at all. I went in here with a very clear feeling of “ah, good thing this bug is finally being addressed”. As you can see from @zhouzi’s comment, merging #9094 would allow library authors to rely on the ability to truly override the hook’s return value. This alone is an indicator of good design.

@mycroes
Copy link

mycroes commented Aug 17, 2022

I'm going to defer to @mjackson and @ryanflorence here (as they've got the historical insight) but this doesn't feel like a bug to me. The Location concept is basically a wrapper around window.location which is a singleton -- it's impossible to have 2 separate window.location's and thus it feels odd to me if useLocation can actually have different contextual values based on where you are in the tree.

The gallery example uses a different set of routes for the modal and for the background, I guess the fact that you can use different routes within the same top-level router alone warrants useLocation to be contextual. I've been using similar approaches with 2 Routes (or Switch in v5), one for rendering page titles and the other for rendering page content. In all honesty I don't have useLocation issues in that specific situation, but I guess it makes sense to have useLocation aware of the Routes it's in.

@AsemK
Copy link

AsemK commented Aug 21, 2022

This issue just caused me several hours of debugging. It is a major breaking change that is never mentioned in the documentation. I agree with others that the most intuitive behavior is that useLocation and useSearchParams should return values based on the context not on the global location object.
For now whenever I use useLocation, I have to do this:

const globalLocation = useLocation();
const location = globalLocation.state?.backgroundLocation || globalLocation;

Any plans for fixing that? I think most use cases of useLocation would expect the contextual location.

The interface mentions:

/**
 * Returns the current location object, which represents the current URL in web
 * browsers.
 *
 * Note: If you're using this it may mean you're doing some of your own
 * "routing" in your app, and we'd like to know what your use case is. We may
 * be able to provide something higher-level to better suit your needs.
 *
 * @see https://reactrouter.com/docs/en/v6/api#uselocation
 */
 export declare function useLocation(): Location;

I would argue that, in order to do "some of your own routing", you need the contextual location.

@evoyy
Copy link

evoyy commented Aug 21, 2022

@AsemK The same thing happened to me. I believe this will be resolved once the following pull request is merged: #9094

@brophdawg11
Copy link
Contributor

Hey folks - I checked in with @ryanflorence and he confirmed this is a bug, so we should be able to get #9094 merged shortly for release with 6.4.0 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.