Maybe don't navigate(-1) in Remix/React Router
Remix/React Router 7 has a handy hook for navigating without the user interacting. It covers use cases like:
- Navigating based on complex logic
- Navigating to logout after inactivity
- Implementing "back" links
You can give it a route path:
const navigate = useNavigate()
const goSomewhere = () => navigate('/some/place/in/your/app')
Or you can give it an integer to navigate the user's history stack:
const goBack = () => navigate(-1) // go the previous browser location
At first glance, this seems pretty handy for implementing a "Back" link. For example, if you have a list of todos and have the ability for the user to navigate to a single todo. You might want to have a convenient "Back" link to direct the user back to the list of todos. This is especially useful if you have multiple ways to navigate to a single todo:
- View all todos -> click on a single todo
- View users todos -> click on a single todo
- View todos of a certain category -> click on a single todo
"Back" can mean different things in the single todo route so you wouldn't want to just hardcode a link:
<Link to='/todos'>Back</Link>
This would always assume you navigated from a the same place, the /todos
route. No bueno. You could, however, slap an event handler on it and make use of navigate(-1)
:
<Link
to='/todos'
onClick={(e) => {
e.preventDefault()
navigate(-1)
}}
>
Back
</Link>
This seems better but there's another problem...
Browser context
The problem with navigate(-1)
is that it isn't limited to your app's history stack. It's the browser tab's history stack. The Remix docs give you some words of caution about this behavior:
Note that this may send you out of your application since the history stack of the browser isn't scoped to just your application.
I can't think of any web application that I use where I want that behavior. I'm of the mind that a "Back" link in a web application should be contained to the application. I don't want to click a "Back" link in an app and get bounced out of the app.
Let's make it better
Remix/React Router 7 gives us a handy way pass data during a navigation via the state
property of the <Link>
component.
<Link to={`/todos/${todoId}`} state={{back: '/todos'}}>Todos</Link>
And then if the user clicks on that <Link>
, in the todos.$id
route component you can read that state:
const location = useLocation()
let back = location.state?.back // '/todos'
We just need to make use of that in a "Back" link:
<Link
to='/todos'
onClick={e => {
if (back) {
e.preventDefault()
navigate(back)
}
}}
>
Back
</Link>
Now, if the user click on a link with back
state defined, we'll use that. Otherwise, we'll let the <Link>
navigate to to
. Nice.
Let's make it easy
We can define some custom hooks to reuse some of this logic and make things a bit easier.
First, we'll define a simple hook to return the current route url with search params to use as the back
state value.
function useCurrentURL() {
let location = useLocation()
return location.pathname + location.search
}
Using it would look like something like that.
const currentURL = useCurrentURL()
// ...
<Link to={`/todos/${todoId}`} state={{back: currentURL}}>Back</Link>
Then, we can create a hook to handle this state.
import { type LinkProps, useLocation, useNavigate } from '@remix-run/react'
export function useBackNavigation() {
let navigate = useNavigate()
let location = useLocation()
let handleClick: LinkProps['onClick'] = (e) => {
let back = location.state?.back
if (back) {
e.preventDefault()
navigate(back)
}
}
return handleClick
}
Using it would look something like this.
const handleBack = useBackNavigation()
// ...
<Link to='/todos' onClick={handleBack}>Back</Link>
Much better. Now we can navigate back based on the presence of a back
navigation state value, otherwise we'll fall back to the default link's location. 🎉
Alright, that was a pretty lengthy post about "Back" links but hey, we sweat the small stuff 'round here 🍻
Categories: Remix, React Router