My problem is that React will check first Routing for my page and afterwards is going to run useEffect() function. This gives me kind of a problem. What im doing is that i need useEffect() to run first in order to fetch data from local storage. That specific data "decides" in which page my application will redirect.
So that's the main App.js
function App() {
const [user, setUser] = useState({id:null,authorized:false});
useEffect(() => {
const aUser = JSON.parse(localStorage.getItem("user"));
if(aUser!=null){
console.log(JSON.stringify(aUser));
setUser(aUser);
}
}, [])
return (
<div className="App">
<BrowserRouter>
<Route exact path="/" render={ () =>
!user.authorized ? <Login setUser={setUser} user={user}/>
: <Redirect to="/home" /> }
/>
<Route exact path="/login" render={ () =>
!user.authorized ? <Login setUser={setUser} user={user}/>
: null}/>
<Route exact path="/home"
render={() => !user.authorized ?
<Redirect to="/login" /> : <Forbidden/>
}/>
</BrowserRouter>
</div>
);
}
export default App;
So let's assume that user data is already stored in local storage and the user is authorized. When i start up my react app it will first show the Home page and that's because i have set the Route "/" to redirect to "/home" if the user is authorized. Ok that good.
My "problem" is when i refresh the page from /home it will redirect to /login.
Why? Because Routes will be checked first and after that the useEffect() will run.
An obvious solution will be to redirect to /home as i do for the first Route ("/").
OK i get that but why useEffect() Won't run first? That's my main question.
Update:
I can solve MY problem from the beginning but i want to know if there is a solution regarding useEffect().
One solution is as i said before to redirect to "/home" if user is authorized like that
<Route exact path="/login" render={ () =>
!user.authorized ? <Login setUser={setUser} user={user}/>
: <Redirect to="/home" />
}
/>
Basically the same code as "/" Route.
Another solution that may be the best it's to get rid of useEffect() and load the user data with useState() like this:
const [user, setUser] = useState(()=>localStorage.getItem("user"));
The way you can solve this problem is by initializing the state itself by fetching from local storage like this:
const [user, setUser] = useState(()=>localStorage.getItem("user"));
useState takes a function that can be used to initialize the state. This is used for lazy intialization.
Link to Docs
It's also good to keep in mind the order in which the different lifecycle hooks run.
Related
my site is built using MERN stack, when I refresh a page it first shows the main page and then the page where the user is. How to fix this issue?
For example:
if I refresh (/profile) page then for a meanwhile it shows (/) then it redirects to (/profile). I want if I refresh (/profile) it should be on the same page.
import { Route, Redirect } from 'react-router-dom';
const PrivateRoute = ({ component: Component, authed, ...rest }) => {
return (
<Route
{...rest}
render={(props) => authed === true
? <Component {...props} />
: <Redirect to={{ pathname: '/', state: { from: props.location } }} />}
/>
)
}
export default PrivateRoute;
Router code:
const App = () => {
const user = useSelector((state) => state?.auth);
return (
<>
<BrowserRouter>
<Container maxWidth="lg">
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" exact component={About} />
<Route path="/terms" exact component={Terms} />
<PrivateRoute authed={user?.authenticated} path='/profile' component={Profile} />
</Switch>
</Container>
</BrowserRouter>
</>
)
}
export default App;
How to fix so that user stays on the same page if its refreshed? The issue is on the pages where authentication is required.
When first authenticated the user, store the credentials(the info that you evaluate to see if the user is authenticated. Tokens etc.) in the localStorage. Of course you have to create necessary states too.
Then with useEffect hook on every render set the credential state from localStorage.
function YourComponentOrContext(){
const[credentials, setCredentials] = useState(null);
function yourLoginFunction(){
// Get credentials from backend response as response
setCredentials(response);
localStorage.setItem("credentials", response);
}
useEffect(() => {
let storedCredentials = localStorage.getItem("credentials");
if(!storedCredentials) return;
setCredentials(storedCredentials);
});
}
I guess on mounting (=first render) your user variable is empty. Then something asynchronous happen and you receive a new value for it, which leads to new evaluation of {user?.authenticated} resulting in true and causing a redirect to your /profile page.
I must say I'm not familiar with Redux (I see useSelector in your code, so I assume you are using a Redux store), but if you want to avoid such behaviour you need to retrieve the right user value on mounting OR only render route components when you've got it later.
I have reading component which must show only when user is loggedIn. I now redirect the user to /login page if the user is not authenticated. But during the redirect, the reading page is displayed for a few milli seconds allowing the un-authenticated user to see a flickering of the reading page during redirection.
I tried using useLayoutEffect instead of useEffect but the same flickering happens. Also tried without using useEffect or useLayoutEffect and within a function, but still the same
I get the userInfo from a global state, which gets the userInfo from the cookie. For state management I use recoil.
Reading Page: (which must be protected and if no user, redirect to login page)
function index() {
const router = useRouter();
const userInfo = useRecoilValue(userAtom); ---> userInfo is recoil global state
useLayoutEffect(() => {
if (!userInfo) {
router.push("/login?redirect=/reading");
}
}, []);
return (//some code)
Note:
Adding a Loader worked, but is there any better way?
Check the authentication state
show loader based on authentication state
Redirect the user
I would suggest a much better way. Instead of checking on individual pages.
You can write your Auth Check for user at route level i.e inside index.js where the React-Router is defined.
// PrivateRoute.jsx
function PrivateRoute ({component: Component, isAuth, ...rest}) {
return (
<Route
{...rest}
render={(props) => isAuth
? <Component {...props} />
: <Redirect to={{pathname: '/login', state: {userInfo} }} />}
/>
)
}
// index.jsx
.
const userInfo = useRecoilValue(userAtom);
.
<Route path='/login' component={Login} />
<Route path='/register' component={Register} />
<PrivateRoute isAuth={!!userInfo} userInfo={userInfo} path='/dashboard' component={Dashboard} />
.
.
Hope this finds it helpful.
I'm creating an authentication using react js typescript. When login has been unsuccessful or isLogin = false then I want to redirect URL to path "/" which has the Login component. When login is successful or isLogin = true then I want to redirect the URL to the dashboard path that has the dashboard component. Up here the URL has been successfully redirected but the component is not called.
function App() {
const { state } = useContext(AuthContext)
return (
<BrowserContext>
<Switch>
{!state.isLogin
? <Redirect to="/" />
: <Redirect to="/dashboard" />
}
<Route exact path="/" component={Login} />
<Route path="/dashboard" component={Dashboard} />
<Route path="/register" component={Register} />
</Switch>
</BrowserContext>
)
}
The issue is that the logic for redirection that you've written runs only once when the App component is initialised. Thus, any subsequent changes to state.isLogin doesn't trigger a re-render.
You need to make use of useEffect hook here.
Inside the hook you can, write the logic for redirection and add state.isLogin as a dependency which when updated will trigger a re-render of your redirection logic.
The problem is Switch component renders first suitable Route or Redirect component. So just as execution reaches:
{!state.isLogin
? <Redirect to="/" />
: <Redirect to="/dashboard />
}
it renders either first redirect component or the second one depending on the flag and never goes further to the Route block.
You have couple of options to make it work.
declarative:
function App() {
const { state } = useContext(AuthContext)
return (
<BrowserContext>
<Switch>
<Route exact path="/" render={() => {
if (state.isLogin) return <Redirect to="/dashboard" />
return <Login />
}} />
{!state.isLogin ? <Redirect to="/" /> : null}
<Route path="/dashboard" component={Dashboard} />
<Route path="/register" component={Register} />
</Switch>
</BrowserContext>
)
}
Here if path is / then it will check isLogin flag and redirect to /dashboard if it's set and render Login if it's not.
If path is not / and isLogin flag is not set it will redirect to /. If isLogin is set it will fall through to the next Route components.
imperative
This one is quite lengthy to show full code for. It will require a decent amount of refactoring. You'll have to move BrowserRouter to the parent component of the App. Make the App to have access to the history prop from withRouter hoc. And then use React.useEffect to execute redirects when isLogin changes:
const { history } = props
...
React.useEffect(() => {
history.push(state.isLogin ? "/dashboard" : "/")
}, [state.isLogin, history])
I made it so that if the user is already logged in and he tried to go to the login page, then he is redirected to the main page. But when sign out, the redirection occurs only after the page is refreshed (also when sign in). How to fix it?
route:
const isLoggedIn = localStorage.getItem("token");
return (
<BrowserRouter>
<Route path={'/'} component={Home} />
<Route path={'/sign-in'} exact render={() => (isLoggedIn ? (<Redirect to="/" />) : (<SignIn />))} />
</BrowserRouter>
);
sign-out:
const signOut = async () => {
localStorage.removeItem('token')
await axios.post('sign-out', {});
setRedirect(true);
}
if (redirect) {
return <Redirect to="/sign-in" />
}
Try using a state in your component and set state according to your login status.Once the state is changed component will re-render automatically.
or else try using a global state like context or redux which will ease your process.
What is the best Reactjs Router login practise that allows checking login status before rendering and rerendering if it changes
i tried using useState and useEffect and passing loggedIn (status) as props but it render before setting state so i have to refresh page to get update
function App() {
Axios.defaults.withCredentials=true
const [loggedIn, setLoggedIn] = useState(false)
useEffect(()=>{
checkLoginStatus()
},[loggedIn])
function checkLoginStatus(){
Axios.get('http://localhost:3001/loggedin')
.then(res =>{
if(res.data.Loggedin){
if(!loggedIn){
setLoggedIn(true)
}
}else{
setLoggedIn(false)
}
}
)
.catch(err => console.log(err))
}
return (
<Router>
<Switch>
<Route exact path='/' render={props => (<Home {...props} loggedIn={loggedIn} />)}/>
<Route path='/login' render={props => <LoginForm {...props} loggedIn={loggedIn} />} />
<Route path='/register' component={RegisterForm} />
<Route path='/logout' component={Logout} />
</Switch>
</Router>
)
The question is: do you set the loggedIn state somewhere outside your checkLoginStatus()?
If not - you have a vicious circle here: you check and change the loggedIn only when the loggedIn changes...
Change your useEffect() dependency to [] to make it a componentDidMount() hook, I think that's what you want.
edit
About that initial render: it's obvious that initially it doesn't render as logged in. You send a request to server and have to await the results, it's an async function of course. You have two choices here: render a loading component while waiting for response or make an optimistic asumption that a user is logged-in and if a response says that he's not - log him out.