I am trying to create a protected route component, using firebase, I have the following setup
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router-dom';
import firebase from 'firebase';
firebase.initializeApp({
apiKey: 'xxxxxx',
authDomain: 'xxxxxx',
databaseURL: 'xxxxxx',
projectId: 'xxxxxx',
storageBucket: 'xxxxxx',
messagingSenderId: 'xxxxxx',
});
class ProtectedRoute extends Component {
componentWillMount() {}
render() {
const { component: Component, layout: Layout, redirect, auth: isAuthorized, ...rest } = this.props;
if (!this.props.hasOwnProperty('auth') && !this.props.hasOwnProperty('layout')) {
return <Route {...this.props} />;
}
const template = Layout ? (
<Layout>
<Component />
</Layout>
) : (
<Component />
);
if (!this.props.hasOwnProperty('auth') && this.props.hasOwnProperty('layout')) {
return <Route {...rest} component={() => template} />;
}
if (isAuthorized) {
firebase.auth().onAuthStateChanged(user => {
if(!user) {
console.log(user)
return redirect ? <Redirect to={{ pathname: redirect }} /> : <Route render={() => <div>Unauthorized</div>} />;
}
})
}
return <Route {...rest} render={() => template} />;
}
}
export default ProtectedRoute;
My routes are setup like this, which allows me to pass in if a route should be private or not
import Route from './containers/ProtectedRoute';
<Switch>
<Route exact path="/" component={LandingPage} />
<Route path="/private" auth={true} component={PrivatePage} />
<Redirect to="/" />
</Switch>
What I would expect to happen is, when visiting /private I should trigger the firebase.auth().onAuthStateChanged call and on returning null I should then trigger the redirect logic.
Instead I am still hitting return <Route {...rest} render={() => template} />;
Meanwhile the console.log within the firebase call is outputting null
firebase.auth().onAuthStateChanged is asynchronous, so the return statement inside your if (isAuthorized) { ... } is never run.
You could instead put this logic in componentDidMount and store the result of the lastest change in your component state and use that.
Example
class ProtectedRoute extends Component {
auth = firebase.auth();
state = { user: this.auth.currentUser };
componentDidMount() {
this.unsubscribe = this.auth.onAuthStateChanged(user => {
this.setState({ user });
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const {
component: Component,
layout: Layout,
redirect,
auth: isAuthorized,
...rest
} = this.props;
const { user } = this.state;
if (
!this.props.hasOwnProperty("auth") &&
!this.props.hasOwnProperty("layout")
) {
return <Route {...this.props} />;
}
const template = Layout ? (
<Layout>
<Component />
</Layout>
) : (
<Component />
);
if (
!this.props.hasOwnProperty("auth") &&
this.props.hasOwnProperty("layout")
) {
return <Route {...rest} component={() => template} />;
}
if (isAuthorized && !user) {
return redirect ? (
<Redirect to={{ pathname: redirect }} />
) : (
<Route render={() => <div>Unauthorized</div>} />
);
}
return <Route {...rest} render={() => template} />;
}
}
Related
How to create a protected route with react-router-dom and storing the response in localStorage, so that when a user tries to open next time they can view their details again. After login, they should redirect to the dashboard page.
All functionality is added in ContextApi.
Codesandbox link : Code
I tried but was not able to achieve it
Route Page
import React, { useContext } from "react";
import { globalC } from "./context";
import { Route, Switch, BrowserRouter } from "react-router-dom";
import About from "./About";
import Dashboard from "./Dashboard";
import Login from "./Login";
import PageNotFound from "./PageNotFound";
function Routes() {
const { authLogin } = useContext(globalC);
console.log("authLogin", authLogin);
return (
<BrowserRouter>
<Switch>
{authLogin ? (
<>
<Route path="/dashboard" component={Dashboard} exact />
<Route exact path="/About" component={About} />
</>
) : (
<Route path="/" component={Login} exact />
)}
<Route component={PageNotFound} />
</Switch>
</BrowserRouter>
);
}
export default Routes;
Context Page
import React, { Component, createContext } from "react";
import axios from "axios";
export const globalC = createContext();
export class Gprov extends Component {
state = {
authLogin: null,
authLoginerror: null
};
componentDidMount() {
var localData = JSON.parse(localStorage.getItem("loginDetail"));
if (localData) {
this.setState({
authLogin: localData
});
}
}
loginData = async () => {
let payload = {
token: "ctz43XoULrgv_0p1pvq7tA",
data: {
name: "nameFirst",
email: "internetEmail",
phone: "phoneHome",
_repeat: 300
}
};
await axios
.post(`https://app.fakejson.com/q`, payload)
.then((res) => {
if (res.status === 200) {
this.setState({
authLogin: res.data
});
localStorage.setItem("loginDetail", JSON.stringify(res.data));
}
})
.catch((err) =>
this.setState({
authLoginerror: err
})
);
};
render() {
// console.log(localStorage.getItem("loginDetail"));
return (
<globalC.Provider
value={{
...this.state,
loginData: this.loginData
}}
>
{this.props.children}
</globalC.Provider>
);
}
}
Issue
<BrowserRouter>
<Switch>
{authLogin ? (
<>
<Route path="/dashboard" component={Dashboard} exact />
<Route exact path="/About" component={About} />
</>
) : (
<Route path="/" component={Login} exact />
)}
<Route component={PageNotFound} />
</Switch>
</BrowserRouter>
The Switch doesn't handle rendering anything other than Route and Redirect components. If you want to "nest" like this then you need to wrap each in generic routes, but that is completely unnecessary.
Your login component also doesn't handle redirecting back to any "home" page or private routes that were originally being accessed.
Solution
react-router-dom v5
Create a PrivateRoute component that consumes your auth context.
const PrivateRoute = (props) => {
const location = useLocation();
const { authLogin } = useContext(globalC);
if (authLogin === undefined) {
return null; // or loading indicator/spinner/etc
}
return authLogin ? (
<Route {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
);
};
Update your Login component to handle redirecting back to the original route being accessed.
export default function Login() {
const location = useLocation();
const history = useHistory();
const { authLogin, loginData } = useContext(globalC);
useEffect(() => {
if (authLogin) {
const { from } = location.state || { from: { pathname: "/" } };
history.replace(from);
}
}, [authLogin, history, location]);
return (
<div
style={{ height: "100vh" }}
className="d-flex justify-content-center align-items-center"
>
<button type="button" onClick={loginData} className="btn btn-primary">
Login
</button>
</div>
);
}
Render all your routes in a "flat list"
function Routes() {
return (
<BrowserRouter>
<Switch>
<PrivateRoute path="/dashboard" component={Dashboard} />
<PrivateRoute path="/About" component={About} />
<Route path="/login" component={Login} />
<Route component={PageNotFound} />
</Switch>
</BrowserRouter>
);
}
react-router-dom v6
In version 6 custom route components have fallen out of favor, the preferred method is to use an auth layout component.
import { Navigate, Outlet } from 'react-router-dom';
const PrivateRoutes = () => {
const location = useLocation();
const { authLogin } = useContext(globalC);
if (authLogin === undefined) {
return null; // or loading indicator/spinner/etc
}
return authLogin
? <Outlet />
: <Navigate to="/login" replace state={{ from: location }} />;
}
...
<BrowserRouter>
<Routes>
<Route path="/" element={<PrivateRoutes />} >
<Route path="dashboard" element={<Dashboard />} />
<Route path="about" element={<About />} />
</Route>
<Route path="/login" element={<Login />} />
<Route path="*" element={<PageNotFound />} />
</Routes>
</BrowserRouter>
or
const routes = [
{
path: "/",
element: <PrivateRoutes />,
children: [
{
path: "dashboard",
element: <Dashboard />,
},
{
path: "about",
element: <About />
},
],
},
{
path: "/login",
element: <Login />,
},
{
path: "*",
element: <PageNotFound />
},
];
...
export default function Login() {
const location = useLocation();
const navigate = useNavigate();
const { authLogin, loginData } = useContext(globalC);
useEffect(() => {
if (authLogin) {
const { from } = location.state || { from: { pathname: "/" } };
navigate(from, { replace: true });
}
}, [authLogin, location, navigate]);
return (
<div
style={{ height: "100vh" }}
className="d-flex justify-content-center align-items-center"
>
<button type="button" onClick={loginData} className="btn btn-primary">
Login
</button>
</div>
);
}
For v6:
import { Routes, Route, Navigate } from "react-router-dom";
function App() {
return (
<Routes>
<Route path="/public" element={<PublicPage />} />
<Route
path="/protected"
element={
<RequireAuth redirectTo="/login">
<ProtectedPage />
</RequireAuth>
}
/>
</Routes>
);
}
function RequireAuth({ children, redirectTo }) {
let isAuthenticated = getAuth();
return isAuthenticated ? children : <Navigate to={redirectTo} />;
}
Link to docs:
https://gist.github.com/mjackson/d54b40a094277b7afdd6b81f51a0393f
import { v4 as uuidv4 } from "uuid";
const routes = [
{
id: uuidv4(),
isProtected: false,
exact: true,
path: "/home",
component: param => <Overview {...param} />,
},
{
id: uuidv4(),
isProtected: true,
exact: true,
path: "/protected",
component: param => <Overview {...param} />,
allowed: [...advanceProducts], // subscription
},
{
// if you conditional based rendering for same path
id: uuidv4(),
isProtected: true,
exact: true,
path: "/",
component: null,
conditionalComponent: true,
allowed: {
[subscription1]: param => <Overview {...param} />,
[subscription2]: param => <Customers {...param} />,
},
},
]
// Navigation Component
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { Switch, Route, useLocation } from "react-router-dom";
// ...component logic
<Switch>
{routes.map(params => {
return (
<ProtectedRoutes
exact
routeParams={params}
key={params.path}
path={params.path}
/>
);
})}
<Route
render={() => {
props.setHideNav(true);
setHideHeader(true);
return <ErrorPage type={404} />;
}}
/>
</Switch>
// ProtectedRoute component
import React from "react";
import { Route } from "react-router-dom";
import { useSelector } from "react-redux";
const ProtectedRoutes = props => {
const { routeParams } = props;
const currentSubscription = 'xyz'; // your current subscription;
if (routeParams.conditionalComponent) {
return (
<Route
key={routeParams.path}
path={routeParams.path}
render={routeParams.allowed[currentSubscription]}
/>
);
}
if (routeParams.isProtected && routeParams.allowed.includes(currentSubscription)) {
return (
<Route key={routeParams.path} path={routeParams.path} render={routeParams?.component} />
);
}
if (!routeParams.isProtected) {
return (
<Route key={routeParams.path} path={routeParams.path} render={routeParams?.component} />
);
}
return null;
};
export default ProtectedRoutes;
Would like to add highlight never forget to give path as prop to ProtectedRoute, else it will not work.
Here is an easy react-router v6 protected route. I have put all the routes I want to protect in a routes.js:-
const routes = [{ path: "/dasboard", name:"Dashboard", element: <Dashboard/> }]
To render the routes just map them as follows: -
<Routes>
{routes.map((routes, id) => {
return(
<Route
key={id}
path={route.path}
exact={route.exact}
name={route.name}
element={
localStorage.getItem("token") ? (
route.element
) : (
<Navigate to="/login" />
)
}
)
})
}
</Routes>
If you want an easy way to implement then use Login in App.js, if user is loggedin then set user variable. If user variable is set then start those route else it will stuck at login page. I implemented this in my project.
return (
<div>
<Notification notification={notification} type={notificationType} />
{
user === null &&
<LoginForm startLogin={handleLogin} />
}
{
user !== null &&
<NavBar user={user} setUser={setUser} />
}
{
user !== null &&
<Router>
<Routes>
<Route exact path="/" element={<Home />} />
<Route exact path="/adduser" element={<AddUser />} /> />
<Route exact path="/viewuser/:id" element={<ViewUser />} />
</Routes>
</Router>
}
</div>
)
How to create a protected route with react-router-dom and storing the response in localStorage, so that when a user tries to open next time they can view their details again. After login, they should redirect to the dashboard page.
All functionality is added in ContextApi.
Codesandbox link : Code
I tried but was not able to achieve it
Route Page
import React, { useContext } from "react";
import { globalC } from "./context";
import { Route, Switch, BrowserRouter } from "react-router-dom";
import About from "./About";
import Dashboard from "./Dashboard";
import Login from "./Login";
import PageNotFound from "./PageNotFound";
function Routes() {
const { authLogin } = useContext(globalC);
console.log("authLogin", authLogin);
return (
<BrowserRouter>
<Switch>
{authLogin ? (
<>
<Route path="/dashboard" component={Dashboard} exact />
<Route exact path="/About" component={About} />
</>
) : (
<Route path="/" component={Login} exact />
)}
<Route component={PageNotFound} />
</Switch>
</BrowserRouter>
);
}
export default Routes;
Context Page
import React, { Component, createContext } from "react";
import axios from "axios";
export const globalC = createContext();
export class Gprov extends Component {
state = {
authLogin: null,
authLoginerror: null
};
componentDidMount() {
var localData = JSON.parse(localStorage.getItem("loginDetail"));
if (localData) {
this.setState({
authLogin: localData
});
}
}
loginData = async () => {
let payload = {
token: "ctz43XoULrgv_0p1pvq7tA",
data: {
name: "nameFirst",
email: "internetEmail",
phone: "phoneHome",
_repeat: 300
}
};
await axios
.post(`https://app.fakejson.com/q`, payload)
.then((res) => {
if (res.status === 200) {
this.setState({
authLogin: res.data
});
localStorage.setItem("loginDetail", JSON.stringify(res.data));
}
})
.catch((err) =>
this.setState({
authLoginerror: err
})
);
};
render() {
// console.log(localStorage.getItem("loginDetail"));
return (
<globalC.Provider
value={{
...this.state,
loginData: this.loginData
}}
>
{this.props.children}
</globalC.Provider>
);
}
}
Issue
<BrowserRouter>
<Switch>
{authLogin ? (
<>
<Route path="/dashboard" component={Dashboard} exact />
<Route exact path="/About" component={About} />
</>
) : (
<Route path="/" component={Login} exact />
)}
<Route component={PageNotFound} />
</Switch>
</BrowserRouter>
The Switch doesn't handle rendering anything other than Route and Redirect components. If you want to "nest" like this then you need to wrap each in generic routes, but that is completely unnecessary.
Your login component also doesn't handle redirecting back to any "home" page or private routes that were originally being accessed.
Solution
react-router-dom v5
Create a PrivateRoute component that consumes your auth context.
const PrivateRoute = (props) => {
const location = useLocation();
const { authLogin } = useContext(globalC);
if (authLogin === undefined) {
return null; // or loading indicator/spinner/etc
}
return authLogin ? (
<Route {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
);
};
Update your Login component to handle redirecting back to the original route being accessed.
export default function Login() {
const location = useLocation();
const history = useHistory();
const { authLogin, loginData } = useContext(globalC);
useEffect(() => {
if (authLogin) {
const { from } = location.state || { from: { pathname: "/" } };
history.replace(from);
}
}, [authLogin, history, location]);
return (
<div
style={{ height: "100vh" }}
className="d-flex justify-content-center align-items-center"
>
<button type="button" onClick={loginData} className="btn btn-primary">
Login
</button>
</div>
);
}
Render all your routes in a "flat list"
function Routes() {
return (
<BrowserRouter>
<Switch>
<PrivateRoute path="/dashboard" component={Dashboard} />
<PrivateRoute path="/About" component={About} />
<Route path="/login" component={Login} />
<Route component={PageNotFound} />
</Switch>
</BrowserRouter>
);
}
react-router-dom v6
In version 6 custom route components have fallen out of favor, the preferred method is to use an auth layout component.
import { Navigate, Outlet } from 'react-router-dom';
const PrivateRoutes = () => {
const location = useLocation();
const { authLogin } = useContext(globalC);
if (authLogin === undefined) {
return null; // or loading indicator/spinner/etc
}
return authLogin
? <Outlet />
: <Navigate to="/login" replace state={{ from: location }} />;
}
...
<BrowserRouter>
<Routes>
<Route path="/" element={<PrivateRoutes />} >
<Route path="dashboard" element={<Dashboard />} />
<Route path="about" element={<About />} />
</Route>
<Route path="/login" element={<Login />} />
<Route path="*" element={<PageNotFound />} />
</Routes>
</BrowserRouter>
or
const routes = [
{
path: "/",
element: <PrivateRoutes />,
children: [
{
path: "dashboard",
element: <Dashboard />,
},
{
path: "about",
element: <About />
},
],
},
{
path: "/login",
element: <Login />,
},
{
path: "*",
element: <PageNotFound />
},
];
...
export default function Login() {
const location = useLocation();
const navigate = useNavigate();
const { authLogin, loginData } = useContext(globalC);
useEffect(() => {
if (authLogin) {
const { from } = location.state || { from: { pathname: "/" } };
navigate(from, { replace: true });
}
}, [authLogin, location, navigate]);
return (
<div
style={{ height: "100vh" }}
className="d-flex justify-content-center align-items-center"
>
<button type="button" onClick={loginData} className="btn btn-primary">
Login
</button>
</div>
);
}
For v6:
import { Routes, Route, Navigate } from "react-router-dom";
function App() {
return (
<Routes>
<Route path="/public" element={<PublicPage />} />
<Route
path="/protected"
element={
<RequireAuth redirectTo="/login">
<ProtectedPage />
</RequireAuth>
}
/>
</Routes>
);
}
function RequireAuth({ children, redirectTo }) {
let isAuthenticated = getAuth();
return isAuthenticated ? children : <Navigate to={redirectTo} />;
}
Link to docs:
https://gist.github.com/mjackson/d54b40a094277b7afdd6b81f51a0393f
import { v4 as uuidv4 } from "uuid";
const routes = [
{
id: uuidv4(),
isProtected: false,
exact: true,
path: "/home",
component: param => <Overview {...param} />,
},
{
id: uuidv4(),
isProtected: true,
exact: true,
path: "/protected",
component: param => <Overview {...param} />,
allowed: [...advanceProducts], // subscription
},
{
// if you conditional based rendering for same path
id: uuidv4(),
isProtected: true,
exact: true,
path: "/",
component: null,
conditionalComponent: true,
allowed: {
[subscription1]: param => <Overview {...param} />,
[subscription2]: param => <Customers {...param} />,
},
},
]
// Navigation Component
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { Switch, Route, useLocation } from "react-router-dom";
// ...component logic
<Switch>
{routes.map(params => {
return (
<ProtectedRoutes
exact
routeParams={params}
key={params.path}
path={params.path}
/>
);
})}
<Route
render={() => {
props.setHideNav(true);
setHideHeader(true);
return <ErrorPage type={404} />;
}}
/>
</Switch>
// ProtectedRoute component
import React from "react";
import { Route } from "react-router-dom";
import { useSelector } from "react-redux";
const ProtectedRoutes = props => {
const { routeParams } = props;
const currentSubscription = 'xyz'; // your current subscription;
if (routeParams.conditionalComponent) {
return (
<Route
key={routeParams.path}
path={routeParams.path}
render={routeParams.allowed[currentSubscription]}
/>
);
}
if (routeParams.isProtected && routeParams.allowed.includes(currentSubscription)) {
return (
<Route key={routeParams.path} path={routeParams.path} render={routeParams?.component} />
);
}
if (!routeParams.isProtected) {
return (
<Route key={routeParams.path} path={routeParams.path} render={routeParams?.component} />
);
}
return null;
};
export default ProtectedRoutes;
Would like to add highlight never forget to give path as prop to ProtectedRoute, else it will not work.
Here is an easy react-router v6 protected route. I have put all the routes I want to protect in a routes.js:-
const routes = [{ path: "/dasboard", name:"Dashboard", element: <Dashboard/> }]
To render the routes just map them as follows: -
<Routes>
{routes.map((routes, id) => {
return(
<Route
key={id}
path={route.path}
exact={route.exact}
name={route.name}
element={
localStorage.getItem("token") ? (
route.element
) : (
<Navigate to="/login" />
)
}
)
})
}
</Routes>
If you want an easy way to implement then use Login in App.js, if user is loggedin then set user variable. If user variable is set then start those route else it will stuck at login page. I implemented this in my project.
return (
<div>
<Notification notification={notification} type={notificationType} />
{
user === null &&
<LoginForm startLogin={handleLogin} />
}
{
user !== null &&
<NavBar user={user} setUser={setUser} />
}
{
user !== null &&
<Router>
<Routes>
<Route exact path="/" element={<Home />} />
<Route exact path="/adduser" element={<AddUser />} /> />
<Route exact path="/viewuser/:id" element={<ViewUser />} />
</Routes>
</Router>
}
</div>
)
I am having trouble understanding why my Authorized vs Unauthorized routes are not acting properly.. Here are the way my routes are set up:
class App extends Component {
render() {
return (
<HashRouter>
<React.Suspense fallback={loading()}>
<Switch>
<UnauthenticatedRoute exact path="/login" name="Login Page" component={Login} />
<Route exact path="/register" name="Register Page" component={Register} />
<Route exact path="/404" name="Page 404" component={Page404} />
<Route exact path="/500" name="Page 500" component={Page500} />
<AuthenticatedRoute path="/" name="Home" component={DefaultLayout} />
</Switch>
</React.Suspense>
</HashRouter>
);
}
}
export default App;
I have an auth.js that holds all these route types as well as a check to see if the JWT token is valid:
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import { Api } from './api'
const isAuthenticated = () => {
Api.isActiveToken(sessionStorage.getItem('token')).then(
(response) => {
console.log(response)
return response.ok
},
(error) => {
return false
}
)
}
const AuthenticatedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
isAuthenticated()
? <Component {...props} />
: <Redirect to='/login' />
)} />
);
const UnauthenticatedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
!isAuthenticated()
? <Component {...props} />
: <Redirect to='/' />
)} />
);
export { AuthenticatedRoute, UnauthenticatedRoute }
Where the console.log(response) looks as such:
Response {type: "cors", url: "http://xxx/api/v1/login/validate-token", redirected: false, status: 200, ok: true, …}
body: (...)
bodyUsed: false
headers: Headers {}
ok: true
redirected: false
status: 200
statusText: "OK"
type: "cors"
url: "http://xxx/api/v1/login/validate-token"
__proto__: Response
My sessionStorage is holding the token just fine. What am I doing wrong such that my route never redirects/allows me to the AuthorizedRoute?
isAuthenticated() is asynchronous and currently returns void. You'll have to make isAuthenticated return a Promise of a boolean.
Once you have isAuthenticated returning a value, you'll need to use an effect and state to pull the actual value out of the promise.
import React, { useState, useEffect } from 'react';
import { Redirect, Route } from 'react-router-dom';
import { Api } from './api';
const isAuthenticated = async () => {
try {
const response = await Api.isActiveToken(sessionStorage.getItem('token'));
return response.ok;
} catch (error) {
return false;
}
};
const AuthenticatedRoute = ({ component: Component, ...rest }) => {
const [authenticated, setAuthenticated] = useState(null);
useEffect(() => {
isAuthenticated().then((bool) => setAuthenticated(bool));
}, []);
return (
<Route
{...rest}
render={(props) => {
if (authenticated === null) return '...loading';
return authenticated ? <Component {...props} /> : <Redirect to="/login" />;
}}
/>
);
};
const UnauthenticatedRoute = ({ component: Component, ...rest }) => {
const [authenticated, setAuthenticated] = useState(null);
useEffect(() => {
isAuthenticated().then((bool) => setAuthenticated(bool));
}, []);
return (
<Route
{...rest}
render={(props) => {
if (authenticated === null) return '...loading';
return !authenticated ? <Component {...props} /> : <Redirect to="/" />;
}}
/>
);
};
export { AuthenticatedRoute, UnauthenticatedRoute };
I think the problem is that the Switch component expects certain types of child components, and you're passing it a different component type AuthenticatedRoute which it probably can't handle. Rather than making a new component type, you can convert your components to render functions that just return the Route element so that the Switch only contains routes.
const renderAuthenticatedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
isAuthenticated()
? <Component {...props} />
: <Redirect to='/login' />
)} />
);
const renderUnauthenticatedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
!isAuthenticated()
? <Component {...props} />
: <Redirect to='/' />
)} />
);
class App extends Component {
render() {
return (
<HashRouter>
<React.Suspense fallback={loading()}>
<Switch>
{
renderUnauthenticatedRoute({
exact: true,
path: "/login",
name: "Login Page",
component: Login
})
}
<Route exact path="/register" name="Register Page" component={Register} />
<Route exact path="/404" name="Page 404" component={Page404} />
<Route exact path="/500" name="Page 500" component={Page500} />
{
renderAuthenticatedRoute({
path: "/",
name: "Home",
component: DefaultLayout
})
}
</Switch>
</React.Suspense>
</HashRouter>
);
}
}
After reading tutorials, I managed to work out the usage of <Redirect />, in the code:
import React from 'react';
import Login from './Login';
import Dashboard from './Dashboard';
import {Route, NavLink, BrowserRouter, Redirect} from 'react-router-dom';
const supportsHistory = 'pushState' in window.history;
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: '',
redirectToDashboard: false,
}
}
//-----------------------LOGIN METHODS-----------------------------
onChangeInput(e) {
this.setState({[e.target.name]:e.target.value});
}
login(e) {
e.preventDefault();
const mainThis = this;
if (mainThis.state.username && mainThis.state.password) {
fetch('APILink')
.then(function(response) {
response.text().then(function(data) {
data = JSON.parse(data);
if (!data.error) {
mainThis.setState({redirectToDashboard:true});
} else {
alert(data.msg);
}
})
})
} else {
alert('Username and Password needed');
}
}
renderRedirect = () => {
if (this.state.redirectToDashboard) {
return <Redirect exact to='/company' />
} else {
return <Redirect exact to='/login' />
}
}
render() {
let renderedComp;
return(
<BrowserRouter
basename='/'
forceRefresh={!supportsHistory}>
<React.Fragment>
{this.renderRedirect()}
<Route exact path="/company" render={()=><Dashboard/>} />
<Route exact path="/login" render={()=><Login login={(e)=>this.login(e)} onChangeInput={(e)=>this.onChangeInput(e)} />} />
</React.Fragment>
</BrowserRouter>
)
}
}
This checks what component to show based on the value of this.state.redirectToDashboard, but because of:
onChangeInput(e) {
this.setState({
[e.target.name]:e.target.value
});
}
Every input re-renders the page,leaving me with:
Warning: You tried to redirect to the same route you're currently on: "/login"
I know what causes the warning, it's just that I can't think of other ways to make this work. What changes should I make or at least an idea to properly make this work?
You could wrap your Route components in a Switch which will make it so only one of its children is rendered at one time.
You could then add the redirect from / to /login as first child, and keep the redirect to /company outside of the Switch for when redirectToDashboard is true.
Example
<BrowserRouter basename="/" forceRefresh={!supportsHistory}>
<div>
{this.state.redirectToDashboard && <Redirect to="/company" />}
<Switch>
<Redirect exact from="/" to="/login" />
<Route path="/company" component={Dashboard} />
<Route
path="/login"
render={() => (
<Login
login={e => this.login(e)}
onChangeInput={e => this.onChangeInput(e)}
/>
)}
/>
</Switch>
</div>
</BrowserRouter>
Maybe you should not render Redirect when your condition in renderRedirect does not match.
Instead of:
renderRedirect = () => {
if (this.state.redirectToDashboard) {
return <Redirect exact to='/company' />
} else {
return <Redirect exact to='/login' />
}
}
you can stay there :
renderRedirect = () => {
if (this.state.redirectToDashboard) {
return <Redirect exact to='/company' />
}
return null;
}
I am a beginner in React.js and I am using Firebase & React Router. I have set up a separate Auth.js file (exporting a boolean) where I store the authenticated state of a user and it works as intended but the problem is that when I login/register or logout the content doesn't change/re-render. Logging the exported boolean in other files I see that it doesn't change for some reason.
Auth
import fire from './Fire';
var isAuth = false;
fire.auth().onAuthStateChanged(
function (user) {
if (user) {
isAuth = true;
console.log('Authed');
} else {
isAuth = false
console.log('Not Auth');
}
}
);
export default isAuth;
Then there's Router.js
import React, { Component } from 'react';
import { Route, Switch, Redirect } from "react-router-dom";
import Login from './components/common/Navbar/Login/Login';
import Register from './components/common/Navbar/Register/Register';
import Home from './components/common/Home/Home';
import isAuth from './config/isAuth';
import All from './components/All/All';
import PrivateRoute from './config/PrivateRoute';
class Router extends Component {
constructor(props) {
super(props);
this.state = ({
isAuth: isAuth
});
}
componentWillMount() {
this.setState = ({
isAuth: isAuth
});
}
render() {
console.log('Router.js => ' + this.state.isAuth)
return (
<div>
<PrivateRoute exact path='/' component={Home} />
<PrivateRoute exact path='/register' component={
this.state.isAuth ? Home : Register} />
<PrivateRoute exact path='/all' component={All} />
<Route exact path='/login' component={ this.state.isAuth ? Home : Login} />
</div>
);
}
}
export default Router;
And finally PrivateRoute component which I took from someone's code on the internet.
import React from 'react';
import { Redirect, Route } from "react-router-dom";
const PrivateRoute = ({ component: Component, ...rest }, isLogged) => (
<Route {...rest} render={(props) => (
isLogged
? (<Component {...props} /> )
: (<Redirect to={{ pathname: '/login' }} />)
)} />
)
export default PrivateRoute;
To fix this issue I changed a few things in the way you handled auth state change. See changes below:
Router.js
...
class Router extends Component {
constructor(props) {
super(props);
this.state = {
isAuth: false
};
}
componentDidMount() {
fire.auth().onAuthStateChanged(user => {
console.log("Router user", user);
if (user) {
localStorage.setItem("user", user); // Note the use of localStorage
this.setState({ isAuth: true });
}
});
}
render() {
return (
<div>
<Switch>
<PrivateRoute
exact
path="/"
component={Home}
isLogged={this.state.isAuth}
/>
<PrivateRoute
exact
path="/register"
component={this.state.isAuth ? Home : Register}
isLogged={this.state.isAuth}
/>
<PrivateRoute
exact
path="/all"
component={All}
isLogged={this.state.isAuth}
/>
<Route
exact
path="/login"
component={this.state.isAuth ? Home : Login}
/>
</Switch>
</div>
);
}
}
export default Router;
Take note of the isLogged prop I passed to the PrivateRoute components, this ensures that the current auth state is accessible from within PrivateRoute component. I also used the localStorage to persist the auth state.
PrivateRoute.js
...
const PrivateRoute = ({ component: Component, ...rest }, isLogged) => (
<Route
{...rest}
render={props =>
rest.isLogged ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: "/login" }} />
)
}
/>
);
export default PrivateRoute;
You would notice that I am using rest.isLogged instead of the isLogged argument which for some reason is always undefined. rest.isLogged was passed into this component from the Router component.
Home.js
class Home extends Component {
constructor(props) {
super(props);
this.state = {
isAuth: localStorage.getItem("user") || false
};
console.log("Home.js => " + this.state.isAuth);
}
render() {
return (
<div>
{/* If logged => Welcome ; else => Login */}
{this.state.isAuth ? <Welcome /> : <Login />}
</div>
);
}
}
export default Home;
In Home component, I removed the isAuth import as it was not working, then I set state's isAuth to get the user from localStorage.
From here on out, I'm sure you can figure the rest of your app out. Peace.
Click on the button below to view on codesandbox
I think the way you do will not change the isAuth state. Can you try this solution? I hope it will work
class Router extends Component {
constructor(props) {
super(props);
this.state = ({
isAuth: false
});
}
componentDidMount() {
fire.auth().onAuthStateChanged((user) => {
if (user) {
console.log(user);
this.setState({ isAuth: true });
} else {
this.setState({ isAuth: false });
}
});
}
render() {
console.log('Router.js => ' + this.state.isAuth)
return (
<div>
<PrivateRoute exact path='/' component={Home}/>
<PrivateRoute exact path='/register' component={
this.state.isAuth ? Home : Register} />
<PrivateRoute exact path='/all' component={All}/>
<Route exact path='/login' component={ this.state.isAuth ? Home : Login} />
</div>
);
}
}