I am having trouble protecting routes in my simple react web application. I keep receiving the error Uncaught Error: [ProtectedRoute] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
App.js
import React, { useState, useEffect} from 'react';
import { Routes, Route, useNavigate, BrowserRouter} from 'react-router-dom';
import './App.css';
import Login from './Login';
import Register from './Register';
import SecretsComponent from './SecretsComponent';
import PasswordRetriever from './GetSecretsComponent';
import Homepage from './Home';
import ProtectedRoute from './PrivateRoute';
function App() {
return (
<div className="App">
<Routes>
<Route index element={<Homepage />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<ProtectedRoute path="/savePassword" element={<SecretsComponent />} />
<ProtectedRoute path="/getPassword" element={<PasswordRetriever />} />
</Routes>
</div>
);
}
export default App;
PrivateRoutes.jsx
import React from "react";
import { Route, Navigate } from "react-router-dom";
import Cookies from "universal-cookie";
const cookies = new Cookies();
// receives component and any other props represented by ...rest
export default function ProtectedRoute({ component: Component, ...rest }) {
return (
// this route takes other routes assigned to it from the App.js and return the same route if condition is met
<Route
{...rest}
render={(props) => {
// get cookie from browser if logged in
const token = cookies.get("TOKEN");
// returns route if there is a valid token set in the cookie
if (token) {
return <Component {...props} />;
} else {
// returns the user to the landing page if there is no valid token set
return (
<Navigate
to={{
pathname: "/",
state: {
// sets the location a user was about to access before being redirected to login
from: props.location,
},
}}
/>
);
}
}}
/>
);
};
Login.jsx:
import React, { useState } from "react";
import axios from "axios";
import { Form, Button } from "react-bootstrap";
import Cookies from "universal-cookie";
const cookies = new Cookies();
export default function Login () {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [login, setLogin] = useState(false)
const handleSubmit = (e) => {
e.preventDefault();
console.log(email);
const configuration = {
method: "post",
url: "/login",
data: {
email,
password,
},
};
// make the API call
axios(configuration)
.then((result) => {
setLogin(true);
// set the cookie
cookies.set("TOKEN", result.data.token, {
path: "/",
});
// redirect user to the auth page
window.location.href = "/getPassword";
})
.catch((error) => {
console.log(error);
})
}
return (
<Form onSubmit={(e)=>handleSubmit(e)}>
{/* email */}
<Form.Group controlId="formBasicEmail">
<Form.Label>Email address</Form.Label>
<Form.Control
type="email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter email"
/>
</Form.Group>
{/* password */}
<Form.Group controlId="formBasicPassword">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
name="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
</Form.Group>
{/* submit button */}
<Button
variant="primary"
type="submit"
onClick={(e) => handleSubmit(e)}
>
Login
</Button>
{/* display success message */}
{login ? (
<p className="text-success">You Are Logged in Successfully</p>
) : (
<p className="text-danger">You Are Not Logged in</p>
)}
</Form>
)
}
I then tried to follow react router v6 protected routing documentation, and changed my app.js to:
import React, { useState, useEffect } from 'react';
import { Routes, Route, useNavigate, BrowserRouter } from 'react-router-dom';
import './App.css';
import Login from './Login';
import Register from './Register';
import SecretsComponent from './SecretsComponent';
import PasswordRetriever from './GetSecretsComponent';
import Homepage from './Home';
import ProtectedRoute from './PrivateRoute';
function App() {
return (
<div className="App">
<Routes>
<Route index element={<Homepage />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route element={<ProtectedRoute path="/savePassword" />} />
<Route element={<ProtectedRoute path="/getPassword" />} />
</Routes>
</div>
);
}
export default App;
Which results in the browser telling me No routes matched location for "/savePassword"
and "/getPassword"
, and renders in a blank page missing all forms. The routes also do not look to be protected with the updated code.
Any guidance on correctly protected routes with react-router v6 would be greatly appreciated.
You must place the Route component inside the Routes, and render the private outlet inside your Route element. Outlet renders the children of the parent route if there is any, children renders the route children.
//Protected route
const PrivateOutlet = (props) => {
const { children } = props;
const { user } = useContext(ContextProvider);
if (user && user.id) {
return (
<>
{children}
<Outlet />
</>
);
}
return <Navigate to="/login" replace />;
};
//Use for routes that a logged in user cannot access (login, register)
const PublicOutlet = ({ children }) => {
const { user } = useContext(ContextProvider);
return !user ? (
<>
{children}
<Outlet />
</>
) : (
<Navigate to="/" replace />
);
};
Use it like this:
<Route
path={`myRoute/:id/new`}
element={
<PrivateOutlet>
<MySecretPage />
</PrivateOutlet>
}
/>
Just make sure you resolve the token before you render the router component. For example, initial token is undefined. If the token is undefined, do not render the router. If it is null or something else, render it, and the protected route will do it's job