Search code examples
reactjstypescriptreact-routerreact-router-dom

Modal at different route in react-router v6 that uses the Navigate component


Here is a codesandbox that recreates the problem.

I am using the modal example in the react-router v6 source, and I want to display the contents of a route in the modal.

I have my routes defined like this:

<Routes location={state?.backgroundLocation || location}>
  <Route path="/" element={<Layout />}>
    <Route index element={<Home />} />
    <Route path="gallery/:counter" element={<Gallery />} />
    <Route path="*" element={<NoMatch />} />
  </Route>
</Routes>

{/* Show the modal when a `backgroundLocation` is set */}
{state?.backgroundLocation && (
  <Routes>
    <Route path="/modal/:counter" element={<Modal />} />
  </Routes>
)}

And when the user clicks the Modal link, I want to display the contents of the gallery/:counter route in the modal, but I'm not sure how to trigger the route apart from using the <Navigate /> component, which causes the whole page to redirect.


Solution

  • You can't match two different URL paths at the same time, the browser has just the one address bar and URL. What I'd suggest is to create a layout route component that wraps the "/gallery/:counter" route and conditionally renders the nested route into the Modal component.

    Example:

    const ModalLayout = () => {
      const { state } = useLocation();
    
      return state?.backgroundLocation ? (
        <Modal>
          <Outlet />
        </Modal>
      ) : (
        <Outlet />
      );
    };
    

    The Modal is updated to take a children prop and render children as the modal content. This is where the Outlet above is rendered so the nested routes can render their element content into the modal.

    function Modal({ children }: React.PropsWithChildren) {
      const navigate = useNavigate();
      const buttonRef = useRef<HTMLButtonElement>(null);
    
      function onDismiss() {
        navigate(-1);
      }
    
      return (
        <Dialog
          aria-labelledby="label"
          onDismiss={onDismiss}
          initialFocusRef={buttonRef}
        >
          <div
            style={{
              display: "grid",
              justifyContent: "center",
              padding: "8px 8px"
            }}
          >
            {children}
            <button
              style={{ display: "block" }}
              ref={buttonRef}
              onClick={onDismiss}
            >
              Close
            </button>
          </div>
        </Dialog>
      );
    }
    

    The "/gallery/:counter" route is wrapped in the ModalLayout layout route.

    <Route element={<ModalLayout />}>
      <Route path="gallery/:counter" element={<Gallery />} />
    </Route>
    

    To make the counter state easier to share between routed components, and also persist while switching between home and gallery routes, I suggest moving the counter state from the Home component into another layout route that provides counter state and setter to descendent components via its Outlet context.

    const CounterLayout = () => {
      const [counter, setCounter] = useState(1);
    
      return <Outlet context={{ counter, setCounter }} />;
    };
    

    This wraps all the routes that want/need/care for the counter state, e.g. the Home and Gallery components.

    <Routes>
      <Route path="/" element={<Layout />}>
        <Route element={<CounterLayout />}>
          <Route index element={<Home />} />
          <Route element={<ModalLayout />}>
            <Route path="gallery/:counter" element={<Gallery />} />
          </Route>
          <Route path="*" element={<NoMatch />} />
        </Route>
      </Route>
    </Routes>
    

    Edit modal-at-different-route-in-react-router-v6-that-uses-the-navigate-component (forked)

    enter image description here enter image description here enter image description here