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