Conditional Navigation in React

Conditional Navigation in React

The ability to programmatically redirect to a new page in a React app is a common task. With the plethora of client side routing libraries, it's also a super easy task.

In today's world of React hooks, this is usually all it takes:

const history = useHistory() // react-router
const router = useRouter() // next.js
const nav = useNavigate() // reach-router (used in Gatsby)

history.replace('/path')
router.replace('/path')
nav('path', { replace: true })

So what's the problem?

Navigating is easy, but often times our navigation is highly conditional.

For example, when a user is logged out, what if we want to send them to the login page only if they aren't already on some public page? For now, let's say private pages are all under the path /app, and every other page is public.

Level 1

useEffect(() => {
  const onPrivatePage = new RegEx(`\/app\/([0-9A-Za-z]+\/?)+/`).test(location.pathname)

  if(!isLoggedIn && onPrivatePage) {
    return goTo('/login')
  }

}, [isLoggedIn])

Okay, the logic is pretty easy to follow, but that regex is a bit of a buzz kill. I googled that shit.

But what if we also want to redirect users who are logged in to the app dashboard if they aren't already on some page under /app? Here's what we might add to the above code:

Level 2

useEffect(() => {
  const onPrivatePage = new RegEx(`\/app\/([0-9A-Za-z]+\/?)+/`).test(location.pathname)

  if(!isLoggedIn && onPrivatePage) {
    return goTo('/login')
  }

  if(isLoggedIn && !onPrivatePage) {
    return goTo('/app/dashboard')
  }

}, [isLoggedIn])

Hmmmm... okay just one more if statement probably won't kill anyone.

But, but... what if we have dynamic routes? For example, maybe /dashboard, /analytics, /profile, /messages are all private, and so are their sub directories.

Here's what that could look like:

Level 3

useEffect(() => {
const onPrivatePage = new RegEx(`\/(profile|analytics|dashboard|messages)\/([0-9A-Za-z]+\/?)+/`).test(location.pathname)

  if(!isLoggedIn && onPrivatePage) {
    return goTo('/login')
  }

  if(isLoggedIn && !onPrivatePage) {
    return goTo('/app/dashboard')
  }

}, [isLoggedIn])

So basically we need to tinker with regular expressions if we want to do any kind of one line pattern matching. I don't know about you, but I don't find regex to be easy on the eyes.

For something so simple to express in words, it seems a lot less expressive in code.

A better way!

To deal with this in a more elegant manner, I created a super tiny library. Introducing...

React Use Navigate

The only purpose of this library is to make conditional navigation more expressive. It doesn't care what react framework or client side routing library you use.

Here are the high level features:

  • Tiny. Simple. Expressive. 1.5kb gzipped + tree shaking.
  • TypeScript ready
  • React framework agnostic (Next.js, React Router, Reach Router, etc.)
  • Glob pattern matching support

Let's take the same code we wrote earlier, except using React Use Navigate:

const { replace } = useNavigate()

useEffect(() => {
  replace({
    goTo: '/login',
    when: !isLoggedIn,
    onPaths: ['/dashboard/**, 'analytics/**', '/profile/**', '/messages/**'],
    otherwiseGoTo: '/app/dashboard', 
  })

}, [isLoggedIn])

You can probably tell what's going on here, but let's break it down line by line:

goTo: '/login' The path that we are conditionally navigating to.

when: !isLoggedIn Our primary boolean condition.

onPaths: ['/dashboard/**, 'analytics/**', '/profile/**', '/messages/**'] The paths the user must be on to trigger navigation. It supports globs, so you can match sub directories no problem.

otherWiseGoTo: '/app/dashboard' The path to navigate to if neither the when nor the onPaths conditions are met.

What if I want to navigate when a user is not on a certain page?

Easy! You can replace onPaths with notOnPaths:

replace({
    goTo: '/app/dashboard',
    when: isLoggedIn,
    notOnPaths: ['/app/**'],
    otherwiseGoTo: '/login', 
  })

Negating path conditions is usually not preferred, because as you can tell this got slightly harder to follow. We could write the same thing as:

replace({
    goTo: '/login',
    when: !isLoggedIn,
    onPaths: ['/app/**'],
    otherwiseGoTo: '/app/dashboard', 
  })

But it's totally up to you!

You said it's framework agnostic?

Yes I did. I actually left some details out of the previous examples. React Use Navigate doesn't actually care how navigation works.

The library exports a NavigateProvider where we can plug in our navigation methods from any client side routing library. Here's React Router in a basic Create React App setup:

import { NavigateProvider } from 'react-use-navigate'
import { useHistory } from 'react-router-dom'

const App = () => {
  const history = useHistory()

  const config = {
    push: history.push,
    back: history.back,
    replace: history.replace
  }

  return (
    <NavigateProvider {...config}>
      <RootComponent/>
    </NavigateProvider>
  )
}

Now in any nested component we can call useNavigate() and it will use the methods that we passed in the config.

That pretty much covers it, feel free to check out the docs. Any feedback, issues, or potential use cases in the comments would be appreciated!