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

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

Related

How to add/append routes programatically in React18

I'm a quite new to React, but I'd like to achieve the following:
FrontendApp should:
check if user is authenticated -> draw MainLayout and build menu dynamically, based on JSON received from server api. For the menu to work, I accordingly need to add data to Routes
if user is not authenticated -> draw Login
What I did:
component, which is checking isAuthenticated and loading json from data from server:
const Preloader = () =>
{
const [menu, setMenu] = useState([]);
const {isAuthenticated} = useAuth()
async function fetchMenu()
{
const loadedMenu = await MenuService.getMenu();
setMenu(loadedMenu)
}
useEffect(() =>
{
fetchMenu()
}, [])
if(isAuthenticated)
return <Login/>;
return <MainLayout/>
}
And I'm using this component Preloading in my App
const App = () => {
return (
<BrowserRouter>
<Preloader />
</BrowserRouter>
);
};
auth check works fine, I receive MainLayout or Login depending on isAuthenticated status.
But, how to add all received from server routes to BrowserRouter?
UPD 1:
I'm receiving routes from server to determine, what routes and components are available to current authenticated user. And draw MainMenu correspondingly.

how to execute a component before another one in next.js?

I've been struggling with this problem for a while. I have an Auth component inside which I try to access to local storage to see if there is a token in there and send it to server to validate that token.
if token is valid the user gets logged-in automatically.
./components/Auth.tsx
const Auth: React.FC<Props> = ({ children }) => {
const dispatch = useDispatch(); // I'm using redux-toolkit to mange the app-wide state
useEffect(() => {
if (typeof window !== "undefined") {
const token = localStorage.getItem("token");
const userId = localStorage.getItem("userId");
if (userId) {
axios
.post("/api/get-user-data", { userId, token })
.then((res) => {
dispatch(userActions.login(res.data.user)); // the user gets logged-in
})
.catch((error) => {
localStorage.clear();
console.log(error);
});
}
}
}, [dispatch]);
return <Fragment>{children}</Fragment>;
};
export default Auth;
then I wrap every page components with Auth.tsx in _app.tsx file in order to manage the authentication state globally.
./pages/_app.tsx
<Provider store={store}>
<Auth>
<Component {...pageProps} />
</Auth>
</Provider>
I have a user-profile page in which user can see all his/her information.
in this page first of all I check if the user is authenticated to access this page or not.
if not I redirect him to login page
./pages/user-profile.tsx
useEffect(() => {
if (isAuthenticated) {
// some code
} else {
router.push("/sign-in");
}
}, [isAuthenticated]);
The problem is when the user is in user-profile page and reloads . then the user always gets redirected to login-page even if the user is authenticated.
It's because the code in user-profile useEffect gets executed before the code in Auth component.
(user-profile page is a child to Auth component)
How should i run the code in Auth component before the code in user-profile page ?
I wanna get the user redirected only when he's not authenticated and run all the authentication-related codes before any other code.
Are you sure that the problem is that user-profile's useEffect is executed before Auth's useEffect? I would assume that the outermost useEffect is fired first.
What most probably happens in your case is that the code that you run in the Auth useEffect is asynchronous. You send a request to your API with Axios, then the useEffect method continues to run without waiting for the result. Normally, this is a good situation, but in your profile, you assume that you already have the result of this call.
You would probably have to implement an async function and await the result of both the axios.post method and dispatch method. You would need something like this:
useEffect(() => {
async () => {
if (typeof window !== 'undefined') {
const token = localStorage.getItem("token")
const userId = localStorage.getItem("userId")
if (userId) {
try {
const resp = await axios.post("/api/get-user-data", {userId, token})
await dispatch(userActions.login(res.data.user)) // the user gets logged-in
} catch(error) {
localStorage.clear()
console.log(error)
}
}
}
}()
}, [dispatch])
I think this should work, but it would cause your components to wait for the response before anything is rendered.

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" />}
);
}

Hide react component until onAuthStateChanged has fired

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

ReactJS update data in component when user is logged in

I have dashboard which should show data if a user is logged in and other data if no user is logged in. I already managed to figure out if a user is logged in it is not reflected on the page. It only changes after reloading the page.
This is what I have: An Account object with a userstatus component to hold details of the user. The Account object is placed in a context that is wrapped in the App.js. It also has a getSession function which gets the user details from the authentication mechanism. getSession also updates the userstatus according to the result (logged_in or not_logged_in). Second I have a dashboard component which runs the getSession method and puts the result in the console. Everythings fine. But the render function did not get the changed userstatus.
This is my code (Accounts.js):
export const AccountContext = createContext();
export const Account = {
userstatus: {
loggedinStatus: "not_logged_in",
values: {},
touched: {},
errors: {}
},
getSession: async () =>
await new Promise((resolve, reject) => {
...
}
}),
}
This is the Dashboard.js:
const Dashboard = () => {
const [status, setStatus] = useState();
const { getSession, userstatus } = useContext(AccountContext);
getSession()
.then(session => {
console.log('Dashboard Session:', session);
userstatus.loggedinStatus = "logged_in"
setStatus(1)
})
.catch(() => {
console.log('No Session found.');
userstatus.loggedinStatus = "not_logged_in"
setStatus(0);
});
const classes = useStyles();
return (
<div className={classes.root}>
{userstatus.loggedinStatus}
{status}
{userstatus.loggedinStatus === "logged_in" ? 'User logged in': 'not logged in'}
<Grid
container
spacing={4}
...
I already tried with useState and useEffect, both without luck. The userstatus seems to be the most logical, however, it does not update automatically. How can I reflect the current state in the Dashboard (and other components)?
React only re-renders component when any state change occur.
userstatus is simply a variable whose changes does not reflect for react. Either you should use userstatusas your app state or you can pass it in CreateContext and then use reducers for update. Once any of two ways you use, you would see react's render function reflect the changes in userstatus.
For how to use Context API, refer docs

Resources