React V6 route guarding and routing within component

Viewed 4031

Im trying to use React Routing V6 for my project.

Currently im struggeling to make the authentication and routing itself to work. the idea of my code is:

Not authenticated user:

redirect to /login with my login component. (only login component)

Authenticated user:

Load the gameComponent component, and the rest of links inside of gamecomponent, will load inside gameComponents div class named middleContentHolder

examples:

authenticated user:

visits url /crime -> loads gamecomponent, and within gamecomponent it loads the crime component. visits url /test -> loads gamecomponent , and within gamecomponent it loads the SideBarRight component.

not authenticated user:

vitits url /crime -> not authenticated -> redirects to /login -> loads loginmodule only.

please note that in gamecomponent component, i do have links that will load within gamecomponent. app.js will either load the gamecomponent, or redirect user to login if not auth.

app.js:

import React from 'react';
import logo from './logo.svg';
import './App.css';
import GameComponent from './gameComponent.jsx';
import { BrowserRouter as Router } from 'react-router-dom';
import { Routes, Route, Navigate, Outlet } from 'react-router-dom';
import Crime from './components/game/crime.jsx';

import PrivateRoute from './PrivateRoute';
import Login from './components/login/login.jsx';




function App() {
  return (
  
  <Router>
    <Routes>
         <Route path="/" element={<GameComponent />}>
             <PrivateRoute isAuth={true} path="crime" component={Crime}  redirectTo='/login'/>
         </Route>
     </Routes>
  </Router>
  );
}

export default App;

Gamecomponent:

import React, {Component} from 'react';
//import Component from 'react-dom';
import SideBarRight from './components/game/sideBarRight.jsx';
import SideBarLeft from './components/game/sideBarLeft.jsx';
import Crime from './components/game/crime.jsx';
import Login from './components/login/login.jsx';
import './gameComponent.css';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import { BrowserRouter} from "react-router-dom";


class GameComponent extends Component{
 constructor() {
    super();
    this.state = {
    userData: {
        user: {cash:0, bank:0, weapon:'', username: 'test', locationname: 'Bankok',
        defence: 0},
        rankbar: {rankpercent: 50, rank: 'Mafia'},
        }
    }
    
    
    
    
  }


render() {
    return (
    <div className="main">
    <div className="sidebar left">
                <SideBarLeft/>

    </div>
    <div className="middleContentHolder">
        <Route path="/" element={<Crime />} />

    <Route path="/test" element={<Crime />} />
    <Route path="/crime" element={<Crime />} />
    <Route path="/test" element={<SideBarRight UserData={this.state.userData} />} />

        <div className="col-8">
            <div className="content">
            <div className="header"><span>Test...</span></div>
            
            </div>
        </div>
    </div>
    <div className="sidebar right">
        <SideBarRight UserData={this.state.userData}/>
        </div>
    </div>
    );
  }
    
}



export default GameComponent;

PrivateRoute:(auth is just a dummy atm)

import React from 'react';
import PropTypes from 'prop-types';
import { Route, Navigate } from 'react-router-dom';
import { useNavigate  } from "react-router-dom";
import Login from './components/login/login.jsx';
import GameComponent from './gameComponent.jsx';


const PrivateRoute = ({ component: Component, redirectTo, isAuth, path, ...props }) => {
    isAuth = false;
    
    if(!isAuth) {
        return <Navigate to={redirectTo} />;
    }
    return <Route path={path} element={<Component />} />
};

export default PrivateRoute;

update:

orginal auth was:in privateroute:

isAuth = isAuth;

one example of non-working code that would show what i want:

<Route path="/login" element={}>

     <PrivateRoute isAuth={true} path="/" component={GameComponent}  redirectTo='/login'>
              rest of routes exist in gamecomponent..
     </PrivateRoute>
3 Answers

If you only want GameComponent to load if use is authenticated, you will need to change your App component like this:

function App() {
  return (

  <Router>
    <Routes>
       <Route path="/login" element={<LoginComponent />} />
       <PrivateRoute isAuth={true} path="/" component={GameComponent}  redirectTo='/login'/>
    </Routes>
  </Router>
  );
}

Here we are essentially putting a switch so that we can navigate to /login when there is no authentication. <Routes> is vital here, because it will only render the component that matches the exact path.

With the official release of React Router V6, the other answer is no longer valid. It will throw an error since <PrivateRoute /> isn't a <Route />.

The proper way to do it is to refactor your <PrivateRoute /> component like so...

import { Navigate, useLocation } from "react-router-dom"

const PrivateRoute = (props: { children: React.ReactNode }): JSX.Element => {
  const { children } = props

  // Up to you specific app to decide how to get this value
  const isLoggedIn: boolean = localStorage.getItem('logged_user') !== null;
  const location = useLocation()

  return isLoggedIn ? (
    <>{children}</>
  ) : (
    <Navigate
      replace={true}
      to="/login"
      state={{ from: `${location.pathname}${location.search}` }}
    />
  )
}

Then whichever file you setup your routes in, you would do...

<Routes>
   <Route path="/PRIVATE" element={<PrivateRoute> <PrivatePage /> </PrivateRoute>}/>
   <Route path="/profile" element={<PrivateRoute> <ProfilePage /> </PrivateRoute>}/>
   <Route path="/login" element={<LoginPage />}/>
   <Route path="/" element={<HomePage />}/>
</Routes>

This is the proper way of doing it in V6 since only a <Route /> can be nested in a <Routes />. Then your authenticated logic gets moved into the element prop.

As an added bonus, the state={{ from: `${location.pathname}${location.search}` }} in PrivateRoute allows you to get the URL of the page they tried to enter, but was denied. This is passed to your login page, where you can redirect them back to the URL after they authenticate.

Solution for newer version

Create custom middleware <AuthGuard> and use as wrapper

<PrivateRoute> not vaild for newer version as child of <Routes>

Error "A <Route> is only ever to be used as the child of <Routes> element"

App.js

import React from 'react'
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import AuthGuard from "./Routes/AuthGuard";

function App() {
  return (
    <div className='App'>
        <Router>
          <Routes>
              <Route path='/' element={<Home />} />
              <Route path='/contact' element={<Contact />} />
              <Route path='/guest-page' element={<AuthGuard isGuest={true}><h1>Guest Page</h1></AuthGuard>} />
              <Route path='/protected-page' element={<AuthGuard requireToken={true}><h1>ProtectedPage</h1></AuthGuard>} />
          </Routes>
        </Router>
    </div>
  );
}

export default App;

AuthGuard.js

import { Route, useNavigate } from "react-router-dom";
import { useLayoutEffect } from "react";

const ProtectedRoute = ({requireToken, guest, children, ...rest}) => {
    
    console.log(requireToken, guest, children, rest); 
    
    const navigate = useNavigate();
    const hasToken = false;

    let hasAccess = (!requireToken || (requireToken && hasToken));
    let navigateTo = '/?login-rquired=true'; 

    if(guest) {
        hasAccess = !hasToken; 
        navigateTo = '/?guest=true';
        console.log("Guest > hasAccess: " + hasAccess)
    }

    if(requireToken){
        console.log("requireToken", requireToken)
    }
    
    useLayoutEffect(()=>{
        if (!hasAccess) {
            console.log("Not allowed");
            navigate(navigateTo);
        }
    },[])

    return (
        <>{ hasAccess ? children : "Login required" }</>
    )
}

export default ProtectedRoute;
Related