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