Hide react component until onAuthStateChanged has fired - reactjs

I have a simple react app that uses firebase for auth. The problem with using firebase is that I need to wait for the onAuthStateChanged callback to fire before I know for certain whether or not a user is logged in or not, which only takes a second or so.
I'd like to display a loading message for that time period, so that when the app is displayed to the user I can be certain that we have determined whether or not a user. Once the callback has been triggered and we have a determinate state for the user, load the app.
The problem with the code below is that it looks like the value of loading is being changed from false to true faster than the onAuthStateChanged callback is being triggered, which means that the app is being loaded faster than we have a determinate state for the user. So if someone navigates to the dashboard there is a redirect to login there that triggers first despite the user actually being logged in.
I really need to be able to set a variable like displayApp that is set to true inside the callback. However if I do that I get an error message saying
Assignments to the 'displayApp' variable from inside React Hook useEffect will be lost after each render.
which does make sense since the component is re-rendered each time and the value will be lost.
What's the best way to persist a variable in this situation so that I can only display my app once the callback has triggered?
const auth = firebase.auth();
const AuthStateContext = React.createContext(undefined);
function App() {
const user = useFirebaseAuth();
const [value, loading] = useAuthState(auth);
console.log("Loading: ", loading);
let displayApp = false;
useEffect(() => {
firebase.auth().onAuthStateChanged(authUser => {
console.log("Auth user:", authUser);
console.log("Loading (auth): ", loading);
displayApp = true;
})
})
return (
<>
{!loading &&
<Switch>
<Route path="/" exact>
{user ? <Dashboard/> : <Login/> }
</Route>
<Route path="/dashboard">
<Dashboard/>
</Route>
<Route path="/login">
<Login/>
</Route>
</Switch>
}
{loading &&
<h4>Loading...</h4>
}
</>
)
}
export default function Dashboard(props){
const auth = firebase.auth();
const user = useFirebaseAuth();
console.log("Dashboard");
if(!user){
return (
// <h4>Not logged</h4>
<Redirect to="/login"/> //Since the app is loaded before the user is retrieved, this gets triggered despite the user being logged in
)
}
return (
<>
<h1>Dashboard</h1>
<h4>{auth.currentUser?.displayName}</h4>
<button onClick={() => firebase.auth().signOut()}>Sign out</button>
</>
)
}

In react hooks the best way to assign values to variables is using useState
import {useState} from "react"; //Add this import
const auth = firebase.auth();
const AuthStateContext = React.createContext(undefined);
function App() {
const user = useFirebaseAuth();
const [value, loading] = useAuthState(auth);
console.log("Loading: ", loading);
const [displayApp, setDisplayApp] = useState(false); //change the initialization of the variable
useEffect(() => {
firebase.auth().onAuthStateChanged(authUser => {
console.log("Auth user:", authUser);
console.log("Loading (auth): ", loading);
setDisplay(true); //Set the value that you want
})
})
This prevent that the "displayApp" variable reset in each render

Related

This React Private Route isn't catching Firebase Auth in time?

I'm working with React to build a admin panel for a website, and using Firebase as the authentication system (and data storage, etc).
I've gone through a few Private Route versions, but finally settled on the one that seems to work best with Firebase. However, there is a minor problem. It works well when the user logins in, and according to Firebase Auth documentation, by default, it should be caching the user.
However, if I log in, and then close the tab and re-open it in a new tab, I get ejected back to the login page (as it should if the user isn't logged in).
I am running the site on localhost via node, but that probably shouldn't matter. A console.log reports that the user is actually logged in, but then gets kicked back anyway. Everything is encapsulated in a useEffect, which watches the LoggedIn value, and checks the Auth State.
Is there any way to prevent this from kicking a logged-in user out when they re-open the tab?
import { FunctionComponent, useState, useEffect } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { getAuth, onAuthStateChanged } from 'firebase/auth';
import Routes from '../../helpers/constants';
export const PrivateRoute: FunctionComponent<any> = ({
component: Component,
...rest
}) => {
const [LoggedIn, SetLoggedIn] = useState(false);
// Check if the User is still authenticated first //
useEffect(() => {
const auth = getAuth();
onAuthStateChanged(auth, (user:any) => {
if (user.uid !== null)
{
SetLoggedIn(true);
// This gets reached after the tab is re-opened, indicating it's cached.
console.log("Logged In");
}
});
}, [LoggedIn]);
// On tab reload however, this acts as if LoggedIn is set to false after the cache check
return (
<Route
{...rest}
render={(props:any) => {
console.log(LoggedIn);
return LoggedIn ? (
<Component {...props} />
) : (
<Redirect to={Routes.LOGIN} />
);
}}
/>
);
};
It redirects because in the first render of your private route the code that sets the LoggedIn state to true hasn't been executed yet. You could use an extra boolean state to avoid rendering the Routes when you haven't checked the auth status.
...
const [LoggedIn, SetLoggedIn] = useState(false);
const [loading, setLoading] = useState(true);
...
if (user.uid !== null) {
setLoading(false);
SetLoggedIn(true);
}
...
// On tab reload however, this acts as if LoggedIn is set to false after the cache check
if(loading) return <div>Loading...</div>; // or whatever UI you use to show a loader
return (
<Route
...
/>
);
};
You'll need to check for the user only on the component mount, you can have an empty dependency array in the useEffect hook, and also stop listening for updates in the hook clean up
useEffect(() => {
const auth = getAuth();
const unsubscribe = onAuthStateChanged(auth, (user:any) => {
...
});
return unsubscribe; // stop listening when unmount
}, []);
But you'll be reinventing the wheel a little, there is already a hook you could use https://github.com/CSFrequency/react-firebase-hooks/tree/master/auth#useauthstate

React login redirect based on a state set by an API call

I have a react webapp that uses an API to validate a user (using a cookie). I would also like to implement route protection in the frontend.
Basically I have a state:
const [loggedIn, setLoggedIn] = useState(false);
then I use the useEffect hook to check whether this user is logged in:
async function loggedInOrNot() {
var loggedIn = await validateUserAsync();
setLoggedIn(loggedIn);
}
}
useEffect(() => {
loggedInOrNot();
}, []);
I use the react router dom to redirect to "/login" page if the user failed the validation:
function ProtectedRoute({ children, ...restOfProps }) {
return (
<Route {...restOfProps}>
{loggedIn ? React.cloneElement(children) : <Redirect to="/login" />}
</Route>
);}
the route is composed like this:
<ProtectedRoute exact path="/post/edit">
<EditPosts
inDarkMode={inDarkMode}
userName={userName && userName}
/>
</ProtectedRoute>
If I don't refresh the page, it works well, but if I logged in, then I refresh my page, the initial render will cause the redirect, since the setState is asynchronous, and the loggedIn state (false by default) is not updated before the route switches to the log in page.
I have tried to use useRef hook together with the state, didn't manage to make it work.
I checked online to use local storage, but it seems to be a work around and in this case there will be two sources of truth, local storage and the state.
so I would like to ask you guys what is the cleaner way to make this work.
Thanks in advance!
You might have to have a loading view or something to display while checking if logged in.
Perhaps loggedIn can be boolean | null:
function ProtectedRoute({ children, ...restOfProps }) {
const [loggedIn, setLoggedIn] = useState(null);
// some code omitted for brevity
if (loggedIn === null) {
return 'Loading...'
}
return (
<Route {...restOfProps}>
{loggedIn ? React.cloneElement(children) : <Redirect to="/login" />}
</Route>
);
}
I think you should also be maintaining a session to check whether the user is logged in or not and set state as per that. Else for every refresh your state will get reinitiated and also you'll endup making numerous api calls to validate user, that's not advised.
So, before you make the first api call to validate user, you should be checking whether a user session is available, use cookies to maintain session. If session is there then load the session status as your state else make the api call and load the state and initiate the user session with the api response.
Also, if you don't want to maintain session and wanted to make api calls. You can show loader until you finish your call and direct them to respected page. Please see below code to implement the same.
function ProtectedRoute({ children, ...restOfProps }) {
const [loggedIn, setLoggedIn] = useState(false);
const [isLoading, setLoading] = useState(true);
const loggedInOrNot = async () => {
var loggedIn = await validateUserAsync();
setLoggedIn(loggedIn);
setLoading(false);
};
useEffect(() => {
loggedInOrNot();
}, []);
return (
<Route>
{isLoading ? (<SomeLoader />) : (<LoadRoute loggedIn={loggedIn} >{children}</LoadRoute>)}
</Route>);
}
function LoadRoute({ children, loggedIn }){
return (
{loggedIn ? React.cloneElement(children) : <Redirect to="/login" />}
);
}

How to conditionally render in a react component with state updated in useEffect?

In react I have a private route component that has an isAuthenticated state value that is used to either render the component it is wrapping, or redirect back to my login page.
In order to detect a refresh and perform an authentication check, I added a useEffect hook in the hope that I could check the authentication status, then update isAuthenticated, then either return the component or redirect accordingly.
const PrivateRoute: React.FC<IPrivateRouteProps> = (props) => {
const dispatch = useDispatch();
const [isAuthenticated, setIsAuthenticated] = React.useState(false);
useEffect(() => {
authService.getIdentity().then(response => {
if (response) {
dispatch(signIn(new Identity(response.account)));
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
});
}, [])
return isAuthenticated ? (
<Route {...props} component={props.component} render={undefined} />
) : (
<Redirect to={{ pathname: props.redirectPath }} />
);
};
Unfortunately this doesn't work and I'm not sure why or how to solve it.
The value of isAuthenticated is always false at the point of rendering and I get the following error in the console....
index.js:1 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in PrivateRoute (at App.tsx:23)
I can confirm that it does flow through to setIsAuthenticated(true) but this is never reflected in the value of isAuthenticated before rendering.
How can/should I solve this?
Thanks,
I think the issue is that the first time that the code run, it evaluates the render with isAuthenticated false, then the useEffect kicks in and tries to update the value but you have been already redirected to another page.
I will suggest to use another variable to know if the authentication has been validated, and only continue after the authentication has been done.
const PrivateRoute: React.FC<IPrivateRouteProps> = (props) => {
const dispatch = useDispatch();
const [loading, setLoading] = React.useState(true);
const [isAuthenticated, setIsAuthenticated] = React.useState(false);
useEffect(() => {
authService.getIdentity().then(response => {
if (response) {
dispatch(signIn(new Identity(response.account)));
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
setLoading(false);
});
}, [])
if (isLoading) {
//we are not ready yet
return null;
}
return isAuthenticated ? (
<Route {...props} component={props.component} render={undefined} />
) : (
<Redirect to={{ pathname: props.redirectPath }} />
);
};
As you can see it says Can't perform a React state update on an unmounted component..
To prevent this issue, can you plz try to put <Route> and <Redirect> inside of <React.Fragment> like this?
return isAuthenticated ? (
<React.Fragment>
<Route {...props} component={props.component} render={undefined} />
</React.Fragment>
) : (
<React.Fragment>
<Redirect to={{ pathname: props.redirectPath }} />
</React.Fragment>
);

useEffect optimize, but unable to re-render when the passed array has changed

So I have a Loader component that fetches data from the server and decides where to push/redirect based on the response.
loader.tsx
export const Loader = observer(({ children }: CorrectlyTyped) => {
const history = useHistory();
const location = useLocation();
const [loaded, setLoaded] = React.useState(false);
useEffect(() => {
api.fetch(globalStore).then((response) => {
setLoaded(true);
if (response.autheticated) {
history.replace('/authenticated-page');
} else {
history.push('/unauth-page');
}
});
}, [globalStore]);
loader = (
<AnimatedStuff/>
);
return (
<>
{loaded ? children : loader}
</>
);
});
app.tsx
<Router>
<Switch>
<Loader>
<Route path={SomePath1} component={SomeComponent1}/>
<Route path={SomePath2} component={SomeComponent2}/>
<Route path={SomePath3} component={SomeComponent3}/>
// ...and so on
<Loader/>
<Switch/>
<Router/>
on initial state globalStore has properties that are null or undefined. However, once the response is received, those properties get filled with values. However, the effect doesn't get applied. (e.g. when a user logs in, user will get redirected to /authenticated-page even user clicks browser's back still gets redirected to /authenticated-page, same idea with logging out). Or perhaps, I'm putting the wrong value at the useEffect second array argument.
On hindsight making the response as a dependency array on useEffect is the way to go but that doesn't seem to work as response is used specifically inside then. help.
In case globalStore is a an object try initlize it like globalStore ={};

How to prevent component displaying before redirecting?

i'm having some trouble with my react/redux app.
I've successfully implimented login functionality - that redirects to the home page once logged in.
The issue I have is if I go the URL, and type in /login. The login screen displays for a second, then redirects to the home page. How can I make it redirect straight away?
Note: The only way i've been able to make it work is instead of checking the state isAuthenticated, check localStorage for a token, then redirecting.
const App = () => {
useEffect(() => {
if (localStorage.token) {
store.dispatch(loadUser());
}
});
return (
<Provider store={store}>
<Router>
<div id='js-scroll'>
<Switch>
<Route exact path='/' component={Home} />
<Route exact path='/register' component={Register} />
<Route exact path='/login' component={Login} />
</Switch>
</div>
</Router>
</Provider>
);
};
const Login = ({ login, isAuthenticated }) => {
if (isAuthenticated) {
return <Redirect to='/' />;
}
return ( ... )
const mapStateToProps = state => ({
isAuthenticated: state.authReducer.isAuthenticated
});
export default connect(
mapStateToProps,
{ login }
)(Login);
The useEffect hook is called after rendering is complete. Making isAuthenticated false on the first render and after it has been commited to the dom then changed by your action.
Probably the loadUser function is a thunk making this intermediate state even more pronounced since there is a delay in getting the user out of the server.
If that is the case, the solution would be to convert the isAuthenticated value from a boolean flag to something with three possible values.
ex: authenticating, authenticated, unauthenticated
then you would change your route to look like this:
const Login = ({ login, authenticationStatus }) => {
if(authenticationStatus === 'authenticating') {
return null; // Or loading page
}
if(authenticationStatus === 'authenticated') {
return <Redirect to='/' />
}
return ( ... )
EDIT
What I understand is that you have a token stored in local storage and from your effect you are dispatching an action which is then loading user from that token.Once the user is loaded then you set the isAuthenticated property to true.
If thats the case then it will take some time for browser to fetch the user details thus the login screen is momentarily displayed.In this scenario then you need a loading screen while the api call for fetching user details.
What I would suggest is also keep one isLoading state apart from isAuthenticated. set isLoading to true before making api call and once the api call is completed(success/failure) then set isLoading to false also set isAuthenticated accordingly.
In your login component check if isLoading is set to true then display loading spinner(or something else) else the regular code that you have.
Let me know if I have understood it incorrectly.
I generally have below effect for my api calls.
export const useHttp = (url, dependencies) => {
const [isLoading, setisLoading] = useState(true);
const [data, setdata] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setisLoading(false);
setdata(data);
})
.catch((err) => {
setisLoading(false);
console.log(err);
})
}, [url, ...dependencies])
return [isLoading, data];
}

Resources