How can I avoid infinite loops in my React Router private routes?

Viewed 1329

In my App.js, I have some authenticated pages I protect with <PrivateRoute>, like so:

<PrivateRoute path="/dashboard">
  <Dashboard />
</PrivateRoute>

I implement <PrivateRoute> like so:

function PrivateRoute({ children, ...rest }) {
  return (
    <Route {...rest} render={() => <CheckRedirect children={children} />} />
  );
}

The problem is, the <CheckRedirect> function calls out to an endpoint on my server which dynamically tells you where to redirect.

Here's the function:

export const CheckRedirect = ({ children }) => {
  const [isChecking, setIsChecking] = React.useState(true);
  const [target, setTarget] = React.useState(null);

  const url = "https://example.com/get-redirect"

  useEffect(() =>{
    async function getPage() {
      axios.get(url)
      .then(function (response) {
        setTarget(response.data.message)
      }).finally(() => setIsChecking(false))
    }
    
    getPage();  
  }, []);

  if (isChecking) {
    return "... Checking";
  }

  return {target} ? (
    <Redirect to={target} />
  ) : (
    <Redirect to='/404' />
  );
};

If you're not logged in, it will send back "/login" in the message field. If you're logged in, it will send "/dashboard".

If it sends back "/dashboard", then React Router produces an infinite loop! It tries the same <PrivateRoute> again, which calls out to the endpoint again, which will once again return "/dashboard", and so on...

Is there a way I can tell my <PrivateRoute> to not do the <CheckRedirect> function if this is already the result of a redirect?

3 Answers

I haven't tested it myself, but have you tried passing path as a prop to CheckRedirect and only do the setTarget in your getPage fetch if it returns a different route?

function PrivateRoute({ children, path, ...rest }) {
  return (
    <Route {...rest} render={() => <CheckRedirect children={children} path={path} />} />
  );
}
export const CheckRedirect = ({ children, path }) => {
  const [isChecking, setIsChecking] = React.useState(true);
  const [target, setTarget] = React.useState(null);

  const url = "https://example.com/get-redirect"

  useEffect(() =>{
    async function getPage() {
      axios.get(url)
      .then(function (response) {
        const newPath = response.data.message
        if (path !== newPath) {
          setTarget(newPath)
        }
      }).finally(() => setIsChecking(false))
    }
    
    getPage();  
  }, []);

  if (isChecking) {
    return "... Checking";
  }

  return {target} ? (
    <Redirect to={target} />
  ) : (
    <Redirect to='/404' />
  );
};

To avoid CheckRedirect to do any redirect if everything is ok (ie. it's a valid request for that route), ensure CheckRedirect actually returns null in that case. If you have control over the server response, I'd return a different value (not null, but -1 for example) for non-existent routes (ie. to redirect to 404), and keep null for when you really just want to return null.

In CheckRedirect component, you don't even use children prop. It renders a string and then redirects to a page. It's normal that it loops forever. Pass path as a prop to CheckRedirect component and if it's same as server response, render the children.

Add path prop and pass it:

  export const CheckRedirect = ({ children, path }) => {

Add your conditional before redirecting:

  if (target === path) {
    return children
  }

Just change your PrivateRoute Logic to something like this

            const PrivateRoute = ({ component: Component, ...rest }) => {
               
               return (
                  <Route
                     {...rest}

                     render = { props =>

                        user.isOnline ? ( <Component {...props} /> ) :
                        ( 
                           <Redirect
                              to={{
                                 pathname: "/login",
                                 state: { from: props.location }
                              }}
                           />
                        )
                     }
                  />
               )
            }

then

            <PrivateRoute exact path="/dashboard" component={Dashboard} />
Related