Skip to content

Instantly share code, notes, and snippets.

@mjackson
Last active November 12, 2023 07:32
Show Gist options
  • Save mjackson/b5748add2795ce7448a366ae8f8ae3bb to your computer and use it in GitHub Desktop.
Save mjackson/b5748add2795ce7448a366ae8f8ae3bb to your computer and use it in GitHub Desktop.
Notes on handling redirects in React Router v6, including a detailed explanation of how this improves on what we used to do in v4/5

Redirects in React Router v6

An important part of "routing" is handling redirects. Redirects usually happen when you want to preserve an old link and send all the traffic bound for that destination to some new URL so you don't end up with broken links.

The way we recommend handling redirects has changed in React Router v6. This document explains why.

Background

In React Router v4/5 (they have the same API, you can read about why we had to bump the major version here) we had a <Redirect> component that you could use to tell the router when to automatically redirect to another URL. You might have used it like this:

import { Switch, Route, Redirect } from "react-router-dom";

function App() {
  return (
    <Switch>
      <Route path="/home">
        <HomePage />
      </Route>
      <Redirect from="/" to="/home" />
    </Switch>
  );
}

In React Router v4/5, when the user lands at the / URL in the app above, they are automatically redirected to /home.

There are two common environments in which React Router usually runs:

  • In the browser
  • On the server using React's node.js API

In the browser a <Redirect> is simply a history.replaceState() on the initial render. The idea is that when the page loads, if you're at the wrong URL, just change it and rerender so you can see the right page. This gets you to the right page, but also has some issues as we'll see later.

On the server you handle redirects by passing an object to <StaticRouter context> when you render. Then, you check context.url and context.status after the render to see if a <Redirect> was rendered somewhere in the tree. This generally works fairly well, except you have to invoke ReactDOMServer.renderToString(...) just to know if you need to redirect, which is less than ideal.

Problems

As mentioned above, there are a few problems with our redirect strategy in React Router v4/5, namely:

  • "Redirecting" in the browser isn't really redirecting. Your server still served up a valid HTML page with a 200 status code at the URL originally requested by the client. If that client was a search engine crawler, it got a valid HTML page and assumes it should index the page. It doesn't know anything about the redirect because the page was served with a 200 OK status code. This hurts your SEO for that page.
  • Invoking ReactDOMServer.renderToString() on the server just to know if you need to redirect or not wastes precious resources and time. Redirects can always be known ahead of time. You shouldn't need to render to know if you need to redirect or not.

So we are rethinking our redirect strategy in React Router v6 to avoid these problems.

Handling Redirects in React Router v6

Our recommendation for redirecting in React Router v6 really doesn't have much to do with React or React Router at all. It is simply this: if you need to redirect, do it on the server before you render any React and send the right status code. That's it.

If you do this, you'll get:

  • better SEO for redirected URLs and
  • faster responses from your web server

To handle the situation above, your server code might look something like this (using the Express API):

import * as React from "react";
import * as ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router-dom/server";

import App from "./App";

function handleExpressRequest(req, res) {
  // Handle redirects *before* you render and save yourself some time.
  // Bonus: Send a HTTP 302 Found status code so crawlers don't index
  // this page!
  if (req.url === "/") {
    return res.redirect("/home");
  }

  // If there aren't any redirects to process, go ahead and render...
  let html = ReactDOMServer.renderToString(
    <StaticRouter location={req.url}>
      <App />
    </StaticRouter>
  );

  // ...and send a HTTP 200 OK status code so crawlers index the page.
  res.end(html);
}

This will ensure that both:

  • search engine crawlers can see the redirect and avoid indexing the redirected page and
  • you don't waste any more resources server rendering than you have to

Configuring Static Hosting Providers

Many static hosting providers give you an easy way to configure redirects so you can still get good SEO even on a static site with no server. See the docs on various providers below:

On static hosting providers that don't provide a way to do redirects on the server (e.g. GitHub Pages), you can still improve SEO by serving a page with the the redirect encoded in the page's metadata, like this:

<!doctype html>
<title>Redirecting to https://example.com/home</title>
<meta http-equiv="refresh" content="0; URL=https://example.com/home">
<link rel="canonical" href="https://example.com/home">

The <meta> tag tells the browser where to go, and the <link> tag tells crawlers to use that page as the "canonical" representation for that page, which allows you to consolidate duplicate URLs for Googlebot.

Not Server Rendering

If you aren't server rendering your app you can still redirect on the initial render in the client like this:

import { Routes, Route, Navigate } from "react-router-dom";

function App() {
  return (
    <Routes>
      <Route path="/home" element={<Home />} />
      <Route path="/" element={<Navigate replace to="/home" />} />
    </Routes>
  );
}

In the above example, when someone visits /, they will automatically be redirected to /home, same as before.

Please note however that this won't work when server rendering because the navigation happens in a React.useEffect().

Notes on the <Navigate> API

The new <Navigate> element in v6 works like a declarative version of the useNavigate() hook. It's particularly handy in situations where you need a React element to declare your navigation intent, like <Route element>. It also replaces any uses that you had for a <Redirect> element in v5 outside of a <Switch>.

The <Navigate replace> prop tells the router to use history.replaceState() when updating the URL so the / entry won't end up in the history stack. This means that when someone clicks the back button, they'll end up at the page they were at before they navigated to /.

Get Started Upgrading Today

You can prepare your React Router v5 app for v6 by replacing any <Redirect> elements you may be rendering inside a <Switch> with custom redirect logic in your server's request handler.

Then, you can stop using the <StaticRouter context> API and forget about checking the context object after rendering because you know that all of your redirects have already been taken care of.

If you want to redirect client-side, move your <Redirect> into a <Route render> prop.

// Change this:
<Switch>
  <Redirect from="about" to="about-us" />
</Switch>

// to this:
<Switch>
  <Route path="about" render={() => <Redirect to="about-us" />} />
</Switch>

Normal <Redirect> elements that are not inside a <Switch> are ok to remain. They will become <Navigate> elements in v6.

@hevele-moda
Copy link

We have an app that defines a lot of redirects in React components.
There are a few reasons we do this:

  • Some of our redirects depend on data from an external source, so we have to first fetch that data and then redirect if required. For data fetching, we use Apollo client which makes it really easy to handle loading states, errors, caching, etc.
  • We want these redirects to happen on both server-side and client-side, and we also like to keep them defined in one place.

In order to migrate to react-router v6, we'd have to refactor all these redirects to work without React to get them working on SSR. We would also have to make these work on client-side the same way they work today (do the fetch, show loader while data is being fetched, etc.).
This is a quite big lift but also more importantly, it's harder to keep the redirects defined in one place with this change.

I know we can wait for the backwards-compatibility package, but thinking about long term, are there any suggestions on keeping the redirects defined in one place but still having them work on both SSR and client-side?

@joshu-aa
Copy link

joshu-aa commented Dec 3, 2021

what is the equivalent of <Redirect to={{ pathname: "/otp", state: { type: "2", }, }} /> to Navigate? I cant make this one works on navigate <Navigate to={{ pathname: "/otp", state: { type: "2", }, }} />. I got an error of Uncaught TypeError: Cannot read properties of undefined (reading 'state') when I'm accessing my location.state.

@alibek-gao
Copy link

What should we do with conditional redirects? For example new user need to be redirected to onboarding wizard

@piotrekwitkowski
Copy link

piotrekwitkowski commented Dec 13, 2021

@alibek-gao the way I was doing it in v5 was:

<Switch>
    {!load('visitedGetStartedPage') && <Route exact path='/' render={() => <Redirect to='/start' />} />}
    {!!load('visitedGetStartedPage') && <Route exact path='/' render={() => <HomePage login={login} />} />}
</Switch>

and as far as I understand, it is pretty straightforward to replace the Redirect elements with such setup. However, if there are better ways, I would be really glad to see them!

@ItzaMi
Copy link

ItzaMi commented Dec 14, 2021

Any suggestion on what a person should do if they have / behind a PrivateRoute and don't really have anything on / and want to redirect the user to another route from there? (something like /threats/live).

I can't figure out how I would be able to do that with this setup:

const App = () => {
  return (
    <div className={css.host}>
      <BrowserRouter>
        <Routes>
          <Route path="/login" element={<Login />} />

          <Route
            path=""
            element={
              <RequireAuth redirectTo="/login">
                <Home />
              </RequireAuth>
            }
          >
            {routes.map(({ link, component }, index) => {
              return <Route path={link} key={index} element={component} />
            })}
          </Route>
        </Routes>
      </BrowserRouter>
    </div>
  )
}

EDIT1: Actually, it gets weirder. I left my issue on https://stackoverflow.com/questions/70350267/creating-routes-inside-a-private-route-with-react-router-v6 and ended up finding a solution for my problem but this doesn't seem to work as intended if I want to redirect to this threats/live on auth because I'm rendering all the other routes inside /... So... I'm not sure how I should actually approach this.
I need the Sidebar rendered inside Home but I don't want to land on / since I won't have anything there

EDIT2: I can do something like

useEffect(() => {
  if (window.location.pathname === '/') {
    navigate('/threats/live')
  }
}, [])

but this seems wrong... Or is it not?

@bhavinkatira123
Copy link

bhavinkatira123 commented Dec 16, 2021

@kannan007 ,your soltuion is working fine as i have implemented it to redirect any unwanted url with default link .

@Gzbox
Copy link

Gzbox commented Dec 28, 2021

thank you so much
this solved my problem

@mvittiglio
Copy link

Hey folks... so as far as I can tell my implementation will have to change (from leveraging the BrowserRouter and redirects) by removing any protected routes in the "tree" (client side) so that the protected endpoint call is sent to the server at which point I will also add to the top of the route handling method (server side) endpoint-access checking before rendering the page out.

app.get('*', (req, res) => {
  
  // BEGIN New redirect handling
  const redirectInfo = requiresRedirect(req)
  if(redirectInfo !== false) {
    return res.redirect(redirectInfo.destination)
  }
  // END

  const stylesSheets = new ServerStyleSheets()
  const theme = createTheme({ ... })

  const context = { url: undefined }
  const markup = ReactDOMServer.renderToString(
    stylesSheets.collect(
      <StaticRouter location={req.url} context={context}>
        <MainRouter />
      </StaticRouter>
    )
  )

  // DEPRECATED REDIRECT
  // if (context.url) {
  //   return res.redirect(303, context.url)
  // }

  res.status(200).send(
    Template({
      markup: markup,
      css: stylesSheets.toString()
    })
  )
})

@fearnliu
Copy link

Thank you so much!!! I'm using React Router v6 and trying to find a way to implement the "Redirect" functionality.

@shivam-214
Copy link

@joshu-aa, you can just use : <Navigate to="/otp", state: { type: "2", } /> instead. It works fine!!

@ZicoRazzi
Copy link

Hey guys, could you hel me with refactoring of below code, please? Need to replace Redirect.

import React from 'react';
import { Route, Redirect } from 'react-router-dom';

export function IsUserRedirect({ user, loggedInPath, children, ...restProps }) {
return (
<Route
{...restProps}
render={() => {
if (!user) {
return children;
}

    if (user) {
      return (
        <Redirect
          to={{
            pathname: loggedInPath,
          }}
        />
      );
    }

    return null;
  }}
/>

);
}

export function ProtectedRoute({ user, children, ...rest }) {
return (
<Route
{...restProps}
render={({ location }) => {
if (user) {
return children;
}

    if (!user) {
      return (
        <Redirect
          to={{
            pathname: 'signin',
            state: { from: location },
          }}
        />
      );
    }

    return null;
  }}
/>

);
}

@ZicoRazzi
Copy link

And here are the routes...
import React from 'react';
import Home from './pages/home';
import Browse from './pages/browse';
import Signup from './pages/signup';
import Signin from './pages/signin';
import { IsUserRedirect } from './helpers/routes'
import { Route, Routes } from 'react-router-dom';

function App() {
const user = {}
return (



<Route path='/signin' element={} />
<IsUserRedirect
user={user}
loggedInPath={'/browse'}
path={'/signin'}
>


<Route path='/signup' element={} />
<Route path='/' element={} />
<Route path='/browse' element={} />

  </Routes>
</div>

);
}

export default App;

@MohamedAmal
Copy link

MohamedAmal commented Mar 28, 2022

Hello , I have two components
the first has a path of "/home"
second has a path of "/add" and contains the following

render () {
if (this.state.redirect)
{
return <Navigate to="/" state={'well done'}/>;
}
else
{
return (


// I want to receive here the value of state sent from Navigate in add component it should print here the value 'well done'

)
}
}

The question:
how to receive the value of the state inside <Navigate to="/" state={'well done'}/> given from add component and display it in home component, without using hooks.

Thanks in Advance

@allpro
Copy link

allpro commented Apr 10, 2022

if you need to redirect, do it on the server before you render any React and send the right status code. That's it.
If you do this, you'll get:

  • better SEO for redirected URLs and
  • faster responses from your web server

These comments are relevant only to a small minority of public websites that use server-side-rendering. Even in this scenario, it only describes the FIRST render of an SPA app. While this may be good advice for this very limited use-case, but it seems odd to that such an edge-case is noted as an influence in the design of a widely used SPA tool like RR6.

For the vast majority of developers using RR, the only job of the webserver is to deliver source-code to the browser. The SPA in the client handles all logic, (including navigation logic), and renders all content. For enterprise apps like I build, SEO and "faster response" are irrelevant and nonsensical because it is not public and an identical code-bundle is fetched on app-load regardless of the initial URL.

This comment is only to clarify that client-side redirection is necessary, useful and valid. It is not rare or something to avoid.

I use the <Navigate> component to perform the dozens of redirects we use to supports backwards compatibility with legacy hard-coded links and outdated user bookmarks. Plus we use many dynamic redirects from layout-route paths to a first-child, which differs because the sitemap is dynamic, based on user-rights and user preferences. I had to create a wrapper around <Navigate> with more precise logic to fix some infinite loops when redirecting to nested children, but RR6 provides the tools needed to make it work. The upgrade from RR5 to RR6 has required a lot of time and effort, but is worth it. Thank you for all your great work.

@jaylin-slate
Copy link

Is there anyway we can redirect to other domain?

@robinelvin
Copy link

Is there anyway we can redirect to other domain?

I would like to know if this is possible too. So far I have had to resort to writing my own <Redirect /> component which detects if the URL is internal or external. If internal it uses navigate(...) and if external it calls window.open(...)
Now I am about to start the SSR side of things I have to add more logic to detect this situation and perform the redirect in a different way as window.open won't work.
It would be nice if this was built into RR6 as my way seems a bit clunky.

@CoderAWei
Copy link

Is there any solution that we can redirect the nested routes?
for example:

const routes = [
     {
    path: 'nested-routes',
    element: <NestedRoutes/>,
    children: [
      {
        path: 'nested1',
        element: <Nested1/>
      },
      {
        path: 'nested2',
        element: <Nested2/>
      },
    ]
  },
]

when I visit localhost:3000/nested-routes, actually, I want to visit localhost:3000/nested-routes/nested1.How can I achieve this problem.
Thanks

@shrekuu
Copy link

shrekuu commented May 30, 2022

What should we do with conditional redirects? For example new user need to be redirected to onboarding wizard

I have something similar.

import { Navigate, RouteObject, useRoutes } from 'react-router-dom'
import Index from '@/pages/Index'
import Products from '@/pages/Products'
import { IAppStore, useAppStore } from './store'

export default function Routes() {
  const stores = useAppStore((state: IAppStore) => state.stores)
  const activeStoreId = stores.find(e => e.active)?.id

  const routes: RouteObject[] = [
    // Here, if activeStoreId exists, redirect to Products Page.
    { path: '/', element: activeStoreId ? <Navigate to={`${activeStoreId}/products`} /> : <Index /> },
    { path: '/:storeId/products', element: <Products /> },
  ]

  return useRoutes(routes)
}

@RubenZx
Copy link

RubenZx commented Oct 14, 2022

What about dynamic routes on redirect? I mean, before v6 we had the following:

<Redirect from="/original/route/:something" to="/new/route/:something" /> 

But now with the Navigate solution this doesn't work:

<Route
  path="/original/route/:something"
  element={<Navigate replace to="/new/route/:something" />}
/>

If you go to /original/route/example it redirects you to /new/route/:something instead of /new/route/example. I've been searching in the documentation but I cannot find anything about this.

@nathanschwarz
Copy link

nathanschwarz commented Oct 17, 2022

I've used a nice solution since v4 to avoid SEO issues for SSR redirections :

// for both server && client : require extra config to replace process in the client bundle
const IS_SSR = process?.env === undefined

// on the server
let serverContext = { redirection: null }
const tree = <App context={serverContext} />
if (serverContext.redirect !== null) {
    return res.redirect(serverContext.redirection.url, serverContext.redirection.code)
}

// in a nested component
function MyComponent(props) {
   const redirect: boolean = shouldRedirect() // some logic here
   if (redirect) {
        if (IS_SSR) {
             // throwing an error will avoid rendering the rest of the tree
             const err = new Error('redirect', { reason:  'someUrl' })
             err.code = 301 // some redirection code
             throw err
         }
        return <Navigate to='someUrl' />
   }
}

// in your error component
function ErrorCatcher({ serverContext }) {
    //...your logic here
    if (error.message === 'redirect') {
         serverContext = { redirection: { url: error.reason, code: error.code }}
         return null
    }
}

Hope that can help some.

@jonDufty
Copy link

Plus we use many dynamic redirects from layout-route paths to a first-child, which differs because the sitemap is dynamic, based on user-rights and user preferences. I had to create a wrapper around <Navigate> with more precise logic to fix some infinite loops when redirecting to nested children, but RR6 provides the tools needed to make it work

@allpro do you mind sharing how you went about doing this. We're looking at a similar use case

@nirmaoz
Copy link

nirmaoz commented Oct 26, 2022

@RubenZx @jonDufty and anyone else who needs a solution for <Navigate> with dynamic segments,
I've just published this package that solves the issue. It exports a <Redirect> component that uses <Navigate replace> internally.
It supports dynamic segments like /something/:id.
https://www.npmjs.com/package/react-router6-redirect

@hichemfantar
Copy link

Is there a solution for the flicker issue mentioned in this issue when using the redirect element?

@etipton
Copy link

etipton commented Mar 24, 2023

@RubenZx @jonDufty and anyone else who needs a solution for <Navigate> with dynamic segments, I've just published this package that solves the issue. It exports a <Redirect> component that uses <Navigate replace> internally. It supports dynamic segments like /something/:id. https://www.npmjs.com/package/react-router6-redirect

@nirmaoz great stuff here, thank you!

@CookieBOY
Copy link

CookieBOY commented Aug 27, 2023

Is there any solution that we can redirect the nested routes? for example:

const routes = [
     {
    path: 'nested-routes',
    element: <NestedRoutes/>,
    children: [
      {
        path: 'nested1',
        element: <Nested1/>
      },
      {
        path: 'nested2',
        element: <Nested2/>
      },
    ]
  },
]

when I visit localhost:3000/nested-routes, actually, I want to visit localhost:3000/nested-routes/nested1.How can I achieve this problem. Thanks

Even i was looking for the solution for this but after some research I came up with a solution
You can have a parent component similar to this

import { useNavigate, Outlet } from "react-router-dom";
import React, { useEffect } from "react";

const NestedRoutes = () => {
    const navigate = useNavigate()
    const location = useLocation()

    useEffect(() => {
    if (location.pathname === '/nested-routes')
      navigate('/nested-routes/nested1')
  }, [navigate, location.pathname])

return ( <Outlet /> )
}

@kouroshey
Copy link

Thank u. It was very helpful for me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment