How does this Firebase onAuthStateChanged work in this code - javascript

I have an abstract question about Firebase. Here in this code I wonder if the listener that is set up in the useEffect below is called if user is signed out?
I understand that when this code run the Firebase listener onAuthStateChanged below is attached and I wonder when user is signed out here or if user is signed out from another browser(signed in with the same credentials like Google/Facebook), will this code onAuthStateChanged run?
import React, { useEffect } from 'react';
import './App.scss';
import Dashboard from './pages/dashboard/dashboard.component';
import Login from './pages/login-page/login.component';
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
import { connect } from 'react-redux';
import { setUser } from './Redux/User/user.actions';
import { selectUserSlice } from './Redux/User/user.selectors';
import { auth } from './firebase/firebase.utils';
const App = ({ setCurrentUser, currentUser }) => {
useEffect(() => {
const unSubscribeFromAuth = auth.onAuthStateChanged(user => {
setCurrentUser(user);
});
//cleanup function
return () => {
unSubscribeFromAuth();
}
},[setCurrentUser])
return(
<div className="App">
{
currentUser ? <Dashboard /> : <Login />
}
</div>
)
};
const mapDispatchToProps = dispatch => ({
setCurrentUser: user => dispatch(setUser(user)),
});
const mapStateToProps = state => ({
currentUser: selectUserSlice(state)
})
export default connect(mapStateToProps, mapDispatchToProps)(App);

With Firebase Authentication a user is signed in to that specific browser instance. So if the user signs in or out in one browser, it has no effect on any other browser instances.
Keep in mind that different tabs are part of the same browser instance, while even separate windows may be part of the same browser instance - depending on the browser and how the window was opened.
But for example, if the browser are on different machines, or if they're say Firefox, Edge and Chrome, the onAuthStateChanged listener on one of these browser instances will not be called with sign-in state changes that are caused by another one.

Related

React app not running useEffect when tab is not in focus

I have a react app receiving updates to all subscribed clients via socket,
When a new socket message comes in, I’m firing a dispatch action
io.on(EVENT, ({ someData }) => {
console.log(newDate());
dispatch(handleEventUpdate(someData));
});
This action only gets fired when the tab gets in focus.
The date log also matching the exact time the tab comes in focus, any ideas how to make this execute even when the tab is not in focus?
Since JS doesn’t run when tab is not in focus which I think is the issue, I’m Currently trying to use a service worker approach to handle this but I’m not sure where to start from.
Context
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import ChildComp from './ChildComp';
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import ChildComp from './ChildComp';
const ParentComp = () => {
const { data } = useSelector(
(state) => state,
);
return <ChildComp data={data} />;
};
export default ParentComp;
import React, { useEffect } from 'react';
const ChildComp = ({ data }) => {
useEffect(() => {
console.log('state changed');
console.log(data);
return () => {
console.log('component cleanup');
};
}, [data]);
return <p> {data} </p>;
};
export default ChildComp;
The parent gets the state data and passes to the child component. The update doesn't reflect if the tab isn't in focus until you go back to the tab.
You can try the following code simply by adding a dependency. The issue here is that you are not providing enough code...
io.on(EVENT, ({ someData }) => {
console.log(newDate());
dispatch(handleEventUpdate(someData));
}, [/*a value of foucus and not foucus that changes ! that will rerender the component*/]);

Unable to prevent flashing unauthorized content in next.js before redirecting to a route

import JoinComponent from '#/components/join/JoinComponent'
import Footer from '#/components/Layouts/Footer'
import GuestLayout from '#/components/Layouts/GuestLayout'
import Navbar from '#/components/Layouts/Navbar'
import { useAuth } from '#/hooks/auth'
import { useRouter } from 'next/router'
const Join = () => {
const router = useRouter()
const { user } = useAuth({
middleware: 'guest',
redirectIfAuthenticated: '/',
})
if (user !== undefined) {
return (
<GuestLayout>
<title>Join</title>
<div></div>
</GuestLayout>
)
} else {
return (
<GuestLayout>
<title>Join</title>
<Navbar />
<JoinComponent />
<Footer />{' '}
</GuestLayout>
)
}
}
export default Join
It is supposed to conditionally render the layouts if the user is authenticated but it still renders the else part for a brief amount of time and then redirects to '/'.
I am unable to figure out why this is the case.
import JoinComponent from '#/components/join/JoinComponent'
import Footer from '#/components/Layouts/Footer'
import Navbar from '#/components/Layouts/Navbar'
import Loader from '#/components/Loader'
import { useAuth } from '#/hooks/auth'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
const Join = () => {
const [loading, setLoading] = useState(true)
const { user } = useAuth({
middleware: 'guest',
redirectIfAuthenticated: '/',
})
useEffect(() => {
console.log(user)
if (user) {
setLoading(true)
}
let t
if (user === undefined) {
t = setInterval(() => {
setLoading(false)
}, 2000)
}
return () => {
clearInterval(t)
}
}, [user])
return (
<div>
<title>Join</title>
<Navbar />
{loading ? <Loader /> : <JoinComponent />}
<Footer />{' '}
</div>
)
}
export default Join
Although my code have changed significantly over time but I have solved the issue in this manner.
What is important to note that for a certain time the useAuth hook doesn't return a user instance and in that period the user remains undefined.
Later if the user is already authenticated a user object is returned, otherwise user remains undefined.
In this case, it is difficult to know immediately if the user object will be defined, therefore we have to wait for some time before setting the loading state to false.
In this way a sudden flash of the undesired component can be prevented and instead a loader can be rendered.
However, the problem may arise in the future if the useAuth hook takes more time to return a user object than the hard coded time, that calls for another approach to this problem.

Logout component redirects before logout function is executed

I'm making React application, and I have small problem when logging user out. "Sign out" button is Link element which leads to Logout component. Then Logout component calls logout function with access token which is sent to the backend, user and access token are cleared from my store, and page redirects user to Login page. This all works (User is logged out successfully), but I have some slight problem with user experience. In fact, my Logout component first tries to redirect to Login page before logout function is executed, and my router checks if there is a user (there still is), and redirects him to homepage. After that user is logged out and redirected to login page.
How can I handle this?
Here is Sign out button
<Link to='/logout' className='menu-link px-5'>
Sign Out
</Link>
import React, { useEffect } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { Redirect, Switch } from 'react-router-dom';
import * as auth from './redux/AuthRedux';
import { logout } from './redux/AuthCRUD';
import { RootState } from '../../../setup';
export function Logout() {
const accessToken: string = useSelector<RootState>(({ auth }) => auth.accessToken, shallowEqual) as string;
const dispatch = useDispatch();
useEffect(() => {
logout(accessToken).then((response) => {
dispatch(auth.actions.logout());
});
}, [dispatch]);
return (
<Switch>
<Redirect to='/auth/login' />
</Switch>
);
}
accessToken gets removed from the store when you dispatch the logout action, so that allows you to use it's presence as an indication you still have a user. If you have an access token, show a spinner (for example), the user isn't logged out. If you don't have an access token, then you're logged out and can redirect.
export function Logout() {
const accessToken: string = useSelector<RootState>(({ auth }) => auth.accessToken, shallowEqual) as string;
const dispatch = useDispatch();
useEffect(() => {
logout(accessToken).then((response) => {
dispatch(auth.actions.logout());
});
}, [dispatch]);
return accessToken ? <Spinner /> : <Redirect to='/auth/login' />
}
While I'm here, you don't need shallowEqual as the second argument to your useSelector because it's just a string.

React - Firebase handling Auth persistence using a separate auth class

I am building a react app that uses a simple login feature. I am only using google sign in, and am calling the signInWithPopop function to handle that. I have created a separate class to handle all the auth related code. On the navbar of my website I have a login button if the user is not signed in which switches to a profile button when the user has signed in.
This is how I am currently checking if the user is signed in (not working):
console.log(authHandler.getUser());
const[loginState, setLogin] = useState(authHandler.getUser()? true : false);
return(
<div className="navbar">
<div className="nav-options">
<NavItem name="About"></NavItem>
<NavItem name="Listings"></NavItem>
<NavItem name="Dashboard"></NavItem>
{loginState ? <NavItem name="Profile"><DropDown loginState={setLogin}></DropDown></NavItem> : <NavItem name="Login" click={() => authHandler.signIn(setLogin)}></NavItem>}
</div>
</div>
);
This is what I have for my authHandler class:
import firebase from 'firebase';
export default class Auth{
constructor(){
var firebaseConfig = {
...
};
!firebase.apps.length? firebase.initializeApp(firebaseConfig) : firebase.app();
firebase.analytics();
this.provider = new firebase.auth.GoogleAuthProvider();
}
signIn(state){
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION).then(() => {
return firebase.auth().signInWithPopup(this.provider).then((result) => {
console.log("signed in " + result.user.uid);
this.user = result.user
state(true);
}).catch((error) => {
console.log(error.message);
});
}).catch((error) => {
console.log(error.message);
})
}
getUser(){
return firebase.auth().currentUser;
}
logout(state){
//TODO: logout of firebase
state(false);
}
}
I have tried adding session and local persistence on firebase, but when I refresh the page, the user is signed out. What would be the proper way of maintaining persistence, in a separate class like this? I am trying to build this app with best practices in mind so that the code will be split up properly, and security is maintained.
Thanks!
You're supposed to use an auth state observer to get a callback whenever the user's sign in state changes. When a page first loads, the user is always immediately considered to be signed out. The callback will be invoked some time soon after the user's token has been loaded from persistence and verified. Use this state callback to determine what to render.
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
var uid = user.uid;
// ...
} else {
// User is signed out
// ...
}
});
You might want to show a loading screen until the first callback tells you for sure if the user was previously signed in or is definitely signed out.
I suggest reading this for more information.
The way I implemented the auth state in react :
Auth.provider.tsx
import React, {
FC,
createContext,
useContext,
useEffect,
useState,
} from 'react';
import { User, auth } from 'firebase/app';
interface AuthContext {
user: User | null;
loading: boolean;
}
const defaultAuthContext = { user: null, loading: false };
const AuthUserContext = createContext<AuthContext>({ ...defaultAuthContext });
export const AuthUserProvider: FC = ({ children }) => {
const [authContext, setAuthContext] = useState<AuthContext>({
user: null,
loading: true,
});
useEffect(
() =>
auth().onAuthStateChanged((authUser) =>
setAuthContext({ user: authUser, loading: false }),
),
[],
);
return (
<AuthUserContext.Provider value={authContext}>
{children}
</AuthUserContext.Provider>
);
};
export const useAuthUser = () => useContext(AuthUserContext);
App.tsx
const App: React.FC = () => {
return <AuthUserProvider>
// anything
</AuthUserProvider>;
}
anycomponent.tsx
const { user, loading } = useAuthUser();
return loading ? <Loader /> : !user ? <NotLogged /> : <Logged />;
You could implement the observer in your class but everytime you'll need your user you should implement an useEffect watching the user. Making it global in a provider make it easier to use.
There are many other way but I think this one is the easiest to use.

How to persist authentication in Next.js using firebase after token expiration and or refreshing the page

I'm building an app with firebase and next.js. I'm having a hard time with firebase's authentication flows. Everything was coming along well until I noticed a bug/error. If I leave my computer logged in to the app for a while and whenever I refresh the page, It seems like both cause me to be redirected back to my login page. I can see why a refresh would trigger my redirect, as there may not be enough time for the onAuthStateChange to be checked in time for const { user } = useAuth() to run so since there is no user at initial page load (after refresh.) It will go in to the { else } causing me to redirect at this point. But the funny thing is that if I simply click my dashboard (a protected page) link, I'm still authenticated. No redirections. Below is my code for my auth component:
AuthComp.js:
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useAuth } from "../../context/AuthContext";
function LoadingScreen() {
return <div className="fixed top-0 right-0 h-screen w-screen z-50 flex justify-center items-center">
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-gray-900"></div>
</div>
}
export function AuthComp(props) {
const [isAuthenticated, setIsAuthenticated] = useState(false)
const router = useRouter();
const { user } = useAuth();
useEffect(() => {
let out;
if (user) {
setIsAuthenticated(true)
return () => out ? out() : null
} else {
router.push('/auth')
}
return () => out;
}, [user])
if (!user && (!isAuthenticated)) {
return <LoadingScreen />;
}
return <props.Component user={user} />
};
Here is my code for my auth context file: AuthContext.js:
import React, { useState, useEffect, createContext, useContext } from 'react'
import { fbase } from '../utils/auth/firebaseClient'
export const AuthContext = createContext()
export default function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [loadingUser, setLoadingUser] = useState(true) // Helpful, to update the UI accordingly.
useEffect(() => {
// Listen authenticated user
const unsubscriber = fbase.auth.onAuthStateChanged(async (user) => {
try {
if (user) {
setUser(user);
} else {
setUser(null)
return;
}
} catch (error) {
// Most probably a connection error. Handle appropriately.
console.log('an error occurred', error);
} finally {
setLoadingUser(false)
}
})
// Unsubscribe auth listener on unmount
return () => unsubscriber()
}, [])
return (
<AuthContext.Provider value={{ user, setUser, loadingUser }}>
{children}
</AuthContext.Provider>
)
}
// Custom hook that shorthands the context!
export const useAuth = () => useContext(AuthContext)
there may not be enough time for the onAuthStateChange to be checked in time
The first user result from onAuthStateChanged is definitely not guaranteed to happen immediately. You should expect that the first callback will take some time, as the user's persisted token is first loaded then verified.
Before the callback triggers the first time, you should assume an "unknown" state for the user. The user is neither signed in nor signed out until that first callback. I suggest writing your app with this trinary state in mind. (Related, firebase.auth().currentUser will always be null when a page first loads.) To read more about this behavior, I suggest reading this blog post.

Categories