Apologies for this seemingly repated question but it is really biting me. I am trying to use the new ReactRouter v6 private routes and I think the best practice for me would be to make a call to the server to make sure the token is valid and has not been revoked. I am being badly beatean by an infinite loop entering the private route with the typical error
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
my private route looks like this:
import React, {useCallback, useEffect, useState} from "react"
import {Outlet, Navigate} from "react-router-dom"
import {Auth} from "../../api/authApi"
const PrivateRoute = () => {
const [auth, setAuth] = useState(false)
const checkAuth = useCallback(() => {
let authApi = new Auth()
authApi.isAuth().then(isAuthorized => (
setAuth(isAuthorized)
))
}, [])
useEffect(() => {
checkAuth()
}, [checkAuth])
return auth ? <Outlet /> : <Navigate to="/login" />;
}
export default PrivateRoute
and my routes are:
function App() {
return (
<HashRouter>
<Routes>
<Route exact path="/login" element={<LoginPage />} />
<Route exact path="/register" element={<RegisterPage />} />
<Route path="/" element={
<PrivateRoute><HomePage /></PrivateRoute>
} />
</Routes>
</HashRouter>
)
}
export default App
I changed the PrivateRoute component to this:
import React, {useEffect, useRef} from "react"
import {Outlet, Navigate} from "react-router-dom"
import {Auth} from "../../api/authApi"
const PrivateRoute = () => {
let auth = useRef(false)
useEffect(() => {
const checkAuth = () => {
let authApi = new Auth()
authApi.isAuth().then(isAuthorized => (
auth.current = isAuthorized
))
}
checkAuth()
}, [])
return (auth.current ? <Outlet /> : <Navigate to="/login" />)
}
export default PrivateRoute
but still have the same issue. Is it something I am missing?
I tried it and found two issues:
The initial state auth is false so it will navigate to /login and unmount PrivateRoute at the first time, and then the component PrivateRoute(unmounted) got api response, I think maybe that's why you got warning. (I made an solution at the bottom)
The Route and Outlet components are used in the wrong way.
<Route path="/" element={
<PrivateRoute><HomePage /></PrivateRoute>
} />
should be modified to
<Route element={<PrivateRoute />}>
<Route path="/" element={<Home />} />
</Route>
The Code Sample :
Related
I am new to reactjs and have simple question. I want to pass auth props to Login component and want to update its value from within Login component. How can I pass auth to Login component and update its value from Login component.
Secondly, I have one protected route and only logged-in users can open that component. I added Private method to implement this. Is this right approach of making some components protected ?
import "./App.css";
import {
BrowserRouter,
Routes,
Route,
Switch,
Navigate,
NavLink,
} from "react-router-dom";
import Home from "./pages/Home";
import Login from "./pages/Login";
import RequestDemo from "./pages/RequestDemo";
import ProtectedRoute from "./Protected.Route";
const auth = true; //your logic
const Private = ({ Component }) => {
return auth ? <Component /> : <Navigate to="/login" />;
};
function App() {
return (
<BrowserRouter>
<Routes>
<Route exact path="/" element={<Home />} />
<Route exact path="/Login" element={<Login />} />
<Route
path="/RequestDemo"
element={<Private Component={RequestDemo} />}
/>
</Routes>
</BrowserRouter>
);
}
export default App;
For auth variable I would suggest using as a State or context, below I have provided an example using context, you can do it with State also
import './App.css';
import { BrowserRouter,Routes, Route, Switch, Navigate, NavLink } from 'react-router-dom';
import Home from './pages/Home';
import Login from './pages/Login';
import RequestDemo from './pages/RequestDemo';
import ProtectedRoute from './Protected.Route';
import { useContext } from 'react';
// In the login Component
const LoginComponent = () => {
const authContext = useContext(MyContext);
const handleLogin = () => {
authContext.onAuthChange(true); // this will make the user login that change the value of auth to true
}
return (
<div>Login JSX</div>
)
}
const MyContext = React.createContext(null);
const Private = ({Component}) => {
const authContext = useContext(MyContext);
return authContext.auth ? <Component /> : <Navigate to="/login" />
}
function App() {
const [auth, setAuth] = React.useState(true);
const handleAuthChange = (newAuthState) => {
setAuth(newAuthState);
}
return (
<MyContext.Provider value={{
auth,
onAuthChange: handleAuthChange
}}>
<BrowserRouter>
<Routes>
<Route exact path='/' element={<Home />} />
<Route exact path='/Login' element={<Login />} />
<Route path='/RequestDemo' element={<Private Component={RequestDemo} />} />
</Routes>
</BrowserRouter>
</MyContext.Provider>
);
}
export default App;
1. In your case first you have to change auth from const to let as you want to change auth. Then you have to pass auth and an function which can update auth in the same file(where auth is declared) hence you will pass 2 props auth and function which updates auth.
From your code syntax I'm assuming you are using v6.
import "./App.css";
import {
BrowserRouter,
Routes,
Route,
Switch,
Navigate,
NavLink,
} from "react-router-dom";
import Home from "./pages/Home";
import Login from "./pages/Login";
import RequestDemo from "./pages/RequestDemo";
import ProtectedRoute from "./Protected.Route";
let auth = true; // you can mutate let and not constant
const authUpdate = (argument) => {
// update auth in this function and pass this function to the component
console.log(auth + 1);
// if you want some argument or parameter you can do so by declaring argument
console.log(argument); // only if you want to send some argument or else no need
};
const Private = ({ Component }) => {
return auth ? <Component /> : <Navigate to="/login" />;
};
function App() {
return (
<BrowserRouter>
<Routes>
<Route exact path="/" element={<Home />} />
<Route
exact
path="/Login"
element={<Login auth={auth} authUpdate={authUpdate} />} // passed auth and authUpdate as props
/>
<Route
path="/RequestDemo"
element={<Private Component={RequestDemo} />}
/>
</Routes>
</BrowserRouter>
);
}
export default App;
2. There's more than one way for protected route. Yes, you can do it like that as mentioned in code snippet.
PS: I suggest you to use state for auth if you want to trigger re-rendering and want to update auth value at parent and child component level both. If component doesn't re-render's then prop/variable won't be updated
import { useState } from "react";
const [auth,setauth]=useState(true)
const authUpdate=()=>{
// pre-steps
setauth(auth=>!auth)
// you should avoid passing setauth directly as a prop
// down the line in child component you can forget what setauth does and you can set something different
}
If you want to use certain variable/state in all child component you can look into Store or Global State concept. You can use react-redux or useContext which does not need to pass props from parent component to child component (prop drilling)
I just learn react and this is the problem I haven't been able to figure out for hours.
I have the App component and I want to pass data about user login to the ProtectedRoute component so I can handle access to the admin page.
In the App there is a function which manages lifting data up from the Login component (based on Firebase user authentication).
The problem is, the userLogged state is set too late so Protected route receives default value of the state.
How can I send the last state to the ProtectedRoute component?
import {useState} from "react"
import './App.css'
import Login from "./components/admin/Login"
import Homepage from "./components/homepage/Homepage"
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute"
import AdminPage from "./components/admin/AdminPage"
function App() {
const [userLogged, setUserLogged] = useState()
const getUser = (data) => {
setUserLogged(data)
console.log("data: " + data)
console.log("state: " + userLogged)
}
return (
<>
<Routes>
<Route path="/" element={<Homepage />} />
<Route path="/login" element={<Login sendUser={getUser}/>} />
<Route element={<ProtectedRoute isLoggedIn={userLogged} />}>
<Route path="/admin" element={<AdminPage />} />
</Route>
</Routes>
</>
);
}
export default App
This is the ProtectedRoute component, where I tried to add useEffect. It still doesn't work this way.
import { Navigate, Outlet } from "react-router-dom"
import {useState, useEffect} from "react"
const ProtectedRoute = (props) => {
const [isLogged, setIsLogged] = useState("")
useEffect(() => {
setIsLogged(props.isLoggedIn);
console.log(isLogged);
}, [props.isLoggedIn]);
return isLogged ? <Outlet /> : <Navigate to="/" />;
};
export default ProtectedRoute;
I'm having some problems with this slice of code, I'm using Redux for state management. The behavior I expect is to use the dispatcher (the one with the comment) once the user is logged in (so just once!) but it is called three times at every render of App.js
The action contains an API call, so I got three API calls! I don't get how to make it trigger just once. Any idea of the problem?
import React from 'react';
import {
Route, Switch, Redirect,
} from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import Login from './components/auth/login';
import Dashboard from './components/Dashboard';
import * as actions from './store/actions/indexActions';
export default function App() {
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
const jwt = useSelector((state) => state?.auth?.user?.token);
const dispatch = useDispatch();
if (isAuthenticated) {
dispatch(actions.getRole(jwt)); //THIS DISPATCHER IS CALLED 3 TIMES
}
return(
<Provider store={store}>
<BrowserRouter basename="/">
<Switch>
<Route path="/login" exact>
{isAuthenticated ? <Redirect to="/" /> : <Login />}
</Route>
<Route exact path="/">
{isAuthenticated ? <Dashboard /> : <Redirect to="/login" />}
</Route>
</Switch>
</BrowserRouter>
</Provider>
)
}
You currently dispatch it every time the component renders.
If you want it to be called only when the user gets logged in, you need to wrap the call in a useEffect like this:
useEffect(() =>
if (isAuthenticated) {
dispatch(actions.getRole(jwt));
}
}, [isAuthenticated]);
Now, the code inside the useEffect will run only when isAuthenticated changes.
My main App component keeps track of the user that is currently logged in via the firebase onAuthStateChanged callback, which I can then use to redirect the user to the /login route if the user object is null. This works fine, but if you navigate to a different route while on the login page, you don't get redirected back, which causes errors as other routes require you to be logged in to function properly. Here is the code:
export function App() {
const auth = firebase.auth();
const [user, setUser] = useState(null);
useEffect(()=>{
auth.onAuthStateChanged(()=> {
setUser(auth.currentUser);
})
}, []);
return (
<BrowserRouter>
<Switch>
<Route path="/login" exact component={LoginPage}/>
<Route path="/" exact component={HomePage}/>
{!user ? <Redirect to="/login"/> : null}
</Switch>
</BrowserRouter>
);
}
I've tried moving !user ? <Redirect to="/login"/> to the top of the Switch component, but that just makes it so you log out every time the page is refreshed. Any ideas on how to solve this? Thanks.
Why not recompose your Route element to have private routers and public routes? Private routes will be those requiring authentication and public once will not require it. When someone tries to access a private route without authentication, they will automatically be sent away.
Create an element called PrivateRoute and put your firebase auth inside it. Example:
const PrivateRoute = ({children, ...props}) => {
const auth = firebase.auth();
const [user, setUser] = useState(null);
useEffect(()=>{
auth.onAuthStateChanged(()=> {
setUser(auth.currentUser);
})
}, []);
return (
<Route {...props} render={() => {
return valid === null ?
<div>Some kind of loader/spinner here...</div>
:
user ?
children
:
<Redirect to='/login' />
}} />
)
}
Then in your App, use it like so:
return (
<BrowserRouter>
<Switch>
<PrivateRoute exact path="/">
<HomePage />
</PrivateRoute>
<Route exact path="/login" component={LoginPage} />
</Switch>
</BrowserRouter>
);
This will redirect anybody trying to access / to /login if they are not authenticated.
Later any route you create can be wrapped like this if it requires authentication.
I am using the following approach and it works fine (just copy my existing project that works):
import React, {useState, useEffect} from 'react'
import {BrowserRouter as Router, Switch, Route, Redirect} from "react-router-dom"
import {connect} from "react-redux"
import useAuth from './hooks/useAuth'
import styles from './styles.js'
import Landing from './components/Landing'
import Login from './components/Login'
import Logout from './components/Logout'
import Post from './components/Post'
import Users from './components/Users'
import User from './components/User'
import Signup from './components/Signup'
import Profile from './components/Profile'
import AddSocieta from './components/AddSocieta'
import Constructor from './components/Constructor'
const mapStateToProps = state => ({
...state
});
function ConnectedApp() {
const [dimension, setDimention] = useState({windowWidth: 0, windowHeight: 0})
const currentStyles = {
...styles,
showFooterMenuText: styles.showFooterMenuText(dimension),
showSidebar: styles.showSidebar(dimension),
topMenuCollapsed: styles.topMenuCollapsed(dimension),
topMenuHeight: styles.topMenuHeight(dimension),
paddingLeftRight: styles.paddingLeftRight(dimension),
fullScreenMenuFontSize: styles.fullScreenMenuFontSize(dimension),
showSubLogoText: styles.showSubLogoText(dimension),
roundedImageSize: styles.roundedImageSize(dimension)
};
const [auth, profile] = useAuth()
const [isLoggedIn, setIsLoggedIn] = useState(false)
useEffect(() => {
if (auth && auth.uid) {
setIsLoggedIn(true)
} else {
setIsLoggedIn(false)
}
updateDimensions();
window.addEventListener("resize", updateDimensions);
return function cleanup() {
window.removeEventListener("resize", updateDimensions);
}
}, [auth, profile]);
function updateDimensions() {
let windowWidth = typeof window !== "undefined"
? window.innerWidth
: 0;
let windowHeight = typeof window !== "undefined"
? window.innerHeight
: 0;
setDimention({windowWidth, windowHeight});
}
return (<Router>
<Redirect to="/app/gare"/>
<div className="App">
<Switch>
<Route path="/constructor"><Constructor styles={currentStyles}/></Route>
<Route path="/post"><Post/></Route>
<Route path="/login"><Login styles={currentStyles}/></Route>
<Route path="/logout"><Logout styles={currentStyles}/></Route>
<Route path="/users"><Users styles={currentStyles}/></Route>
<Route path="/user/:uid"><User styles={currentStyles}/></Route>
<Route path="/app"><Landing styles={currentStyles}/></Route>
<Route path="/signup" render={isLoggedIn
? () => <Redirect to="/app/gare"/>
: () => <Signup styles={currentStyles}/>}/>
<Route path="/profile" render={isLoggedIn
? () => <Profile styles={currentStyles}/>
: () => <Redirect to="/login"/>}/>
<Route path="/add-societa"><AddSocieta styles={currentStyles}/></Route>
</Switch>
</div>
</Router>);
}
const App = connect(mapStateToProps)(ConnectedApp)
export default App;
I encouter a problem with my react app, using react-router-dom v5.
When I change the route manually or when I use , the component does not update, even when I'm refreshing the page.
Here is my code :
import React, { useEffect, useState } from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import openSocket from "socket.io-client"
import ChannelSelection from './auth/ChannelSelection'
import Home from './home/Home'
import AppContext from '#context/AppContext'
const App = () => {
const [socket, setSocket] = useState()
const [channel, setChannel] = useState('')
const [pseudo, setPseudo] = useState('')
const store = {
user: {
pseudo: pseudo,
setPseudo: (pseudo) => setPseudo(pseudo)
},
app: {
channel: channel,
setChannel: (channel) => setChannel(channel)
}
}
useEffect(() => {
const host = '127.0.0.1'
const port = '8080'
setSocket(openSocket(host + ':' + port))
}, [])
return (
<AppContext.Provider value={store}>
<Router>
<Switch>
<Route exactPath="/" component={() => <ChannelSelection socket={socket} />} />
<Route path="/:channel" component={() => <Home socket={socket} />} />
</Switch>
</Router>
</AppContext.Provider>
)
}
export default App
I'm a little bit confused rigth now because I've already use react-router-dom in the past and never encouter this problem.
Thank you in advance !
You should be using path not exactPath I think?
<Route path="/" exact component={() => <ChannelSelection socket={socket} />}
The path will be useful if you have multiple paths with similar names. Like path="/some" and path="/some/path" which is where you'd need the exact keyword.
Add the exact props in the routes and change the exactPath to path.
There is no prop with exactPath name in react-router-dom.
<Route path="/" exact component={() => <ChannelSelection socket={socket} />} />