React router modal-only routes
I think your best option is probably to utilize either the hidden state
, or query strings (for permalinks), or both, especially if a modal (e.g. a login modal) could be displayed on any page. Just in case you're not aware, React Router exposes the state
part of the history API, allowing you to store data in the user's history that's not actually visible in the URL.
Here's a set of routes I have in mind; you can jump straight into the working example on JSBin if you want. You can also view the resulting example app in its own window so you can see the URLs changing (it uses the hash location strategy for compatibility with JSBin) and to make sure refreshing works as you'd expect.
const router = (
<Router>
<Route component={LoginRedirect}>
<Route component={LocationDisplay}>
<Route path="/" component={ModalCheck}>
<Route path="/login" component={makeComponent("login")} />
<Route path="/one" component={makeComponent("one")} />
<Route path="/two" component={makeComponent("two")} />
<Route path="/users" component={makeComponent("users")}>
<Route path=":userId" component={UserProfileComponent} />
</Route>
</Route>
</Route>
</Route>
</Router>
);
Let's investigate these routes and their components.
First of all, makeComponent
is just a method that takes a string and creates a React component that renders that string as a header and then all its children; it's just a fast way to create components.
LoginRedirect
is a component with one purpose: check to see if the path is /login
or if there is a ?login
query string on the current path. If either of these are true, and the current state does not contain the login
key, it sets the login
key on the state to true
. The route is used if any child route is matched (that is, the component is always rendered).
class LoginRedirect extends React.Component {
componentWillMount() {
this.handleLoginRedirect(this.props);
}
componentWillReceiveProps(nextProps) {
this.handleLoginRedirect(nextProps);
}
handleLoginRedirect(props) {
const { location } = props;
const state = location.state || {};
const onLoginRoute = location.query.login || location.pathname === "/login";
const needsStateChange = onLoginRoute && !state.login;
if (needsStateChange) {
// we hit a URL with ?login in it
// replace state with the same URL but login modal state
props.history.setState({login: true});
}
}
render() {
return React.Children.only(this.props.children);
}
}
If you don't want to use query strings for showing the login modal, you can of course modify this component to suit your needs.
Next is LocationDisplay
, but it's just a component I built for the JSBin demo that displays information about the current path, state, and query, and also displays a set of links that demonstrate the app's functionality.
The login state is important for the next component, ModalCheck
. This component is responsible for checking the current state for the login
(or profile
, or potentially any other) keys and displaying the associated modal as appropriate. (The JSBin demo implements a super simple modal, yours will certainly be nicer. :) It also shows the status of the modal checks in text form on the main page.)
class ModalCheck extends React.Component {
render() {
const location = this.props.location;
const state = location.state || {};
const showingLoginModal = state.login === true;
const showingProfileMoal = state.profile === true;
const loginModal = showingLoginModal && <Modal location={location} stateKey="login"><LoginModal /></Modal>;
const profileModal = showingProfileMoal && <Modal location={location} stateKey="profile"><ProfileModal /></Modal>;
return (
<div style={containerStyle}>
<strong>Modal state:</strong>
<ul>
<li>Login modal: {showingLoginModal ? "Yes" : "No"}</li>
<li>Profile modal: {showingProfileMoal ? "Yes" : "No"}</li>
</ul>
{loginModal}
{profileModal}
{this.props.children}
</div>
)
}
}
Everything else is fairly standard React Router stuff. The only thing to take note of are the Link
s inside LocationDisplay
that show how you can link to various places in your app, showing modals in certain circumstances.
First of all, you can of course link (and permalink) to any page asking it to show the login modal by using the login
key in the query string:
<Link to="/one" query={{login: 1}}>/one?login</Link>
<Link to="/two" query={{login: 1}}>/two?login</Link>
You can also, of course, link directly to the /login
URL.
Next, notice you can explicitly set the state so that a modal shows, and this will not change the URL. It will, however, persist in the history, so back/forward can be used as you'd expect, and a refresh will show the modal over top the same background page.
<Link to="/one" state={{login: true}}>/one with login state</Link>
<Link to="/two" state={{login: true}}>/two with login state</Link>
You can also link to the current page, adding a particular modal.
const path = props.location.pathname;
<Link to={path} state={{login: true}}>current path + login state</Link>
<Link to={path} state={{profile: true}}>current path + profile state</Link>
Of course, depending on how you want your app to work, not all of this is applicable or useful. For example, unless a modal is truly global (that is, it can be displayed no matter the route), this may work fine, but for modals like showing the profile of a given user, I'd probably make that a separate route, nesting it in the parent, e.g.:
<Route path="/users/:id" component={UserPage}>
<Route path="/users/:id/profile" component={UserProfile} />
</Route>
UserProfile
, in this case, would be a component that renders a modal.
Another example where you may want to make a change is storing certain modals in the history; if you don't want to, use replaceState
instead of setState
as appropriate.