Using a dialog as a route is not as simple as it seems. Content behind overlay needs to remain rendered. With the elegance of React I'll document how I achieved a solution for this use case.
For my purposes I am using Reach Router and Reach UI's Dialog but any router should work, and any modal or alert component for a dialog.
Live example: CodeSandbox
Example code: GitHub
Pattern use cases
Recently I've been working on a React project with long lists of entries. Each of these entries has additional details which I hid behind an expanding element. I then realized users might want to link directly to an expanded entry. I choose to use dialogs instead. They make navigating in a list fluid, as you can always see a glimpse of the list behind the dialog.
Twitter, Instagram, and Reddit all use this as one of their main navigation patterns. While they all behave differently, I decided the following is what I needed to implement the pattern successfully:
- A link that opens a dialog
- Update the browser history and location bar
- Display the previous route behind the dialog
- Remembering the route behind the dialog for consistency when navigating backwards (this is more of an edge case)
A link that opens a dialog
After some experimenting, I though the easiest way to determine if a route should be displayed as a dialog was to pass state through the Link
component. Since we need to know which route to render behind the overlay, that is the state we will pass. This code will look much better once we get a Reach Router version with Hooks.
location
has a few properties that are functions. Any state stored in History.state
needs to be serializable, so we sanitize it using JSON
methods. Alternatively we could also hand pick the properties @reach/router
needs.
<Location>
{({ location }) => (
<Link
to="/login"
state={{
oldLocation: JSON.parse(JSON.stringify(location)),
}}
>
Login
</Link>
)}
</Location>
You can retrieve location
from props.location
if Link
is being rendered in a direct child of <Router>
. Make sure to only sanitize with JSON
just once per page if multiple links open a dialog.
Displaying the previous route behind the dialog
Changing the location of the router causes it to render a new route. To keep our old route behind the dialog we will need to render two routers.
To simplify things we define our routes in a new component, making it easy to repeat.
function Routes(props) {
return (
<Router {...props}>
<Home path="/" />
<Login path="/login" />
<Page path="/page/:number" />
</Router>
);
}
By default Reach Router uses the browser history to render it's location, but we can override that with a prop to render any route we want. This allows us to use our oldLocation
which gets stored in location.state
when a user clicks the link.
Overall the pattern is quite small to implement, we render a main router and conditionally render a dialog with the same router.
<Location>
{({ location, navigate }) => {
const { oldLocation } = location.state || {};
return (
<>
<Routes location={oldLocation != null ? oldLocation : location} />
<Dialog
isOpen={oldLocation != null}
onDismiss={() => {
navigate(oldLocation.pathname);
}}
>
<Routes location={location} />
</Dialog>
</>
);
}}
</Location>
- If there is not
oldLocation
in the state- render the router, don't render the dialog (normal view)
- If there is
oldLocation
in the state- render the router with
oldLocation
, and render the dialog withlocation
- render the router with
Conclusion
Don't forget to checkout the complete example or the code on GitHub to see how it all works together and some other rendering tips.