react-router: [Bug]: useLocation inside doesn't return given location

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.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 22
  • Comments: 24 (4 by maintainers)

Most upvoted comments

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 👍

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!

@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.

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>

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.