firebase snapshot not getting currentUser - javascript

i am trying to get current user in console but getting undefined in react-native. firebase 8.3 it is.
this is my firebase init
import firebase from "firebase";
// import { initializeApp } from "firebase/app";
const firebaseConfig = {
//api
};
// Initialize Firebase
const app = firebase.initializeApp(firebaseConfig);
export default firebase;
this is action code , which is same as docs in internet
firebase
.firestore()
.collection("user")
.doc(firebase.auth().currentUser.uid)
.get()
.then((snapshot) => {
if (snapshot.exists) {
console.log(snapshot.data());
dispatch({ type: USER_STATE_CHANGE, currentUser: snapshot.data() });
} else {
console.log("does not exist, console from action ");
}
and here is my redux store code, which is more doubtful in my knowledge
const store = createStore(Reducers, applyMiddleware(thunk))
return (
<Provider style={styles.center} store={store}>
<Main/>
</Provider>
);
and main.js
function Main(props) {
useEffect(() => {
props.fetchUser();
}, []);
if (props.currentUser == undefined) {
return (
<View>
<Text>No Data</Text>
</View>
);
} else {
return (
<View>
<Text>{props.currentUser.name} is logged in now !</Text>
</View>
);
}
}
const mapStateToProps = (state) => {
return {
currentUser: state.user.currentUser,
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchUser: () => dispatch(fetchUser()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Main);

My guess (it is hard to be certain from the fragments of code you shared) is that the code that loads the user data from Firestore runs when the page/app loads, and that Firebase isn't done restoring the current user when it runs.
When the page/app loads, Firebase automatically restores the user credentials from local storage. This requires it to call the server though (amongst others to see if the account has been disabled), which may take some times. While this call is going on, your main code continues, and the value of firebase.auth().currentUser is null at this point.
So if you don't synchronize your code that loads the user profile to Firebase''s restore actions, you will end up loading the data too early, when the user hasn't been re-signed yet.
The solution is to listen for auth state changes, and respond to those, instead of assuming that firebase.auth().currentUser is always correct. For an example of how to do this, see the first code snippet in the Firebase documentation on getting the current user:
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;
// 🤞 This is where you can load the user profile from Firestore
// ...
} else {
// User is signed out
// ...
}
});

Related

Load data from Firestore on page load with useEffect

Simplified code that I am using:
import { useState, useEffect, useContext } from 'react'
import { useRouter } from 'next/router'
import { firestore } from './firebase-config'
import { getDoc, doc } from 'firebase/firestore'
export default function HomePage() {
const router = useRouter()
const user = useContext(AuthContext) // contains user object -> user.user
const [loading, setLoading] = useState(true)
useEffect(() => {
const getData = async() => {
setLoading(true)
const uid = user.user.uid // uid of user in firebase auth
const id = router.query.id // id param of url
const docRef = doc(firestore, `...`)
// doc in a collection that references the above uid and id
const docSnap = await getDoc(docRef)
// get the document from firestore
if (docSnap.exists()) {
importData(docSnap.data()) // add data to store to re-render page
setLoading(false)
} else {
router.push('/main')
// if the user isn't logged in to return to '/'
// ^^ not sure how to do these separately
// if the user is logged in but the document does not exist to return to '/main'
}
}
getData()
}, [router.query, user.user])
return (
<>
{/* */}
</>
)
}
I need to load the document associated with the user's uid and the id param of the currently loaded page, i.e. /main/[id].
These retrieve a Firestore document that is then inserted into the store which causes the HomePage function to re-render to show the data.
uid is found in user.user.uid which is set via onAuthStateChanged in app.js
id is found in router.query.id which is set via useRouter() at the top level
The useEffect() above works, but only temporarily, soon after the data is loaded and the component re-renders, I am linked to '/main' as initially uid and id start as undefined meaning that on the first run of the useEffect hook the else condition is run, it then re-runs as the user and router object is retrieved to load the data, but by the time that has occurred the page is transitioned to './main'.
Would greatly appreciate some help to make this function work.
Additionally, the user should go back to './main' if the document doesn't exist but they are logged in, and if they are not logged in to then be returned to the root ('./')
Thanks in advance!
You can add a loading state for the document retrieval in addition to the loading state that you already have to make sure that the document retrieval is completed before navigating away from the page.
import { firestore } from './firebase-config'
import { getDoc, doc } from 'firebase/firestore'
export default function HomePage() {
const router = useRouter()
const user = useContext(AuthContext) // contains user object -> user.user
const [loading, setLoading] = useState(true)
const [docLoading, setDocLoading] = useState(true)
useEffect(() => {
const getData = async() => {
setLoading(true)
const uid = user.user.uid // uid of user in firebase auth
const id = router.query.id // id param of url
if (!uid) {
setLoading(false)
router.push('/')
return
}
if (!id) {
setLoading(false)
router.push('/main')
return
}
const docRef = doc(firestore, `...`)
// doc in a collection that references the above uid and id
setDocLoading(true)
const docSnap = await getDoc(docRef)
// get the document from firestore
setDocLoading(false)
if (docSnap.exists()) {
importData(docSnap.data()) // add data to store to re-render page
setLoading(false)
} else {
router.push('/main')
}
}
getData()
}, [router.query, user.user])
if (loading || docLoading) {
return <div>Loading...</div>
}
return (
<>
{/* render your component here */}
</>
)
}
So I have managed to fix the issue:
To be able to use uid in the useEffect() hook, onAuthStateChanged is called again rather than using the AuthContext that is created at the top level as this will wait until the user exists
To wait for router.query to be updated you can call router.isReady which returns a Boolean value on whether it has been updated.
Using both of these in this way:
useEffect(() => {
onAuthStateChanged(auth, async (user) => {
if (user) {
if (router.isReady) {
// do stuff -> user exists
} else {
// user exists but the document does not
router.push('/main')
}
}
else {
// user is not logged in
router.push('/')
}
})
}, [router.isReady, router.query])

Prevent Router from Loading Page Briefly before Redirect

I have a session context for my NextJS application where anyone accessing /app/ directory pages have to go through an authorization check prior to allowing the user to access the page.
While my logic works in redirecting users without proper authentication, it is a bit glitchy because when someone navigate to the URL, /app/profile/ the page briefly loads before being redirected by Router.
I am wondering what is the best way to have this check happen prior to router loading the unauthorized page and redirecting them to the /login/ page.
Here are the steps in the authorization check:
Check is the user object has a property, authorized
Query the server for a session token
if the object from the server request comes back with authorized = false, then redirect user to /login/
Here is the code:
import React, { createContext, useContext, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import axios from 'axios'
export const SessionContext = createContext(null);
const AppSession = ({ children }) => {
const router = useRouter()
const routerPath = router.pathname;
const [user, setUser] = useState({ user_id: '', user_email: '', user_avatar: ''})
useEffect(()=> {
// Check for populated user state if pages are accessed with the path /app/
if (routerPath.includes("/app/")){
if (user){
if(user.authenticated === undefined){
// Check if user session exists
axios.get('/api/auth/session/')
.then(res => {
const data = res.data;
// Update user state depending on the data returned
setUser(data)
// If user session does not exist, redirect to /login/
if (data.authenticated === false){
router.push('/login/')
}
})
.catch(err => {
console.log(err)
});
}
}
}
}, [])
return (
<SessionContext.Provider value={{user, setUser}}>
{children}
</SessionContext.Provider>
)
}
export const getUserState = () => {
const { user } = useContext(SessionContext)
return user;
}
export const updateUserState = () => {
const { setUser } = useContext(SessionContext)
return (user) => {
setUser(user);
}
}
export default AppSession;
Since user.authenticated isn't defined in the initial user state you can conditionally render null or some loading indicator while user.authenticated is undefined. Once user.authenticated is defined the code should either redirect to "/login" or render the SessionContext.Provider component.
Example:
const AppSession = ({ children }) => {
const router = useRouter();
const routerPath = router.pathname;
const [user, setUser] = useState({ user_id: '', user_email: '', user_avatar: ''});
...
if (user.authenticated === undefined) {
return null; // or loading indicator/spinner/etc
}
return (
<SessionContext.Provider value={{ user, setUser }}>
{children}
</SessionContext.Provider>
);
};
Check out getServerSideProps, redirects in getServerSideProps and this article.
In your client-side, if you export the NextJS function definition named getServerSideProps from a page, NextJS pre-renders the page on each request using the data returned by getServerSideProps.
In other words, you can use getServerSideProps to retrieve and check the user while pre-rendering the page and then choose to redirect instead of render if your condition is not met.
Here is an example.
function Page({ data }) {
// Render data...
}
export async function getServerSideProps(context) {
const { req, res } = context;
try {
// get your user
if (user.authenticated === undefined) {
return {
redirect: {
permanent: false,
destination: `/`,
},
};
}
return {
props: {
// any static props you want to deliver to the component
},
};
} catch (e) {
console.error("uh oh");
return;
}
}
Good luck!

Can't get cookie during _app initialization with getInitialProps

I'm using next-cookies to store auth credentials from user session, however, I can't get them during app initialization -let's say, the user refreshes the page or comes back later-, however, after app has been initialized -or user has loged in-, I get them navigating in the app.
This is important, because I want to fetch some initial data to be available in to the redux store from the beginning.
// pages/_app.js
import { useStore } from '../store/store';
import nextCookie from 'next-cookies';
function MyApp({ Component, pageProps }) {
const Layout = Component.layout || MainLayout;
const store = useStore(pageProps.initialReduxState); // custom useStore method to init store
const pageTitle = Component.title || pageProps.title || 'Título de la página';
return (
<Provider store={store}>
<Layout pageTitle={pageTitle}>
<Component {...pageProps} />
</Layout>
</Provider>
);
}
export default MyApp;
MyApp.getInitialProps = async (ctx) => {
const { token, user } = nextCookie(ctx);
/* TO DO
the idea is to get cookie from the server side
and pass it to the client side, if the cookie is
active, initial data will be triggered from the
initializers
*/
return { pageProps: { token, user } };
};
Check it out:
Is it a better way or native option to get the cookie without having to a different cookie dependency?
Btw, I already have a middleware that protects routes from non authenticated user which works fine:
// pages/_middleware.js
export function middleware(req) {
const activeSession = req.headers.get('cookie');
const url = req.nextUrl.clone();
if (activeSession) {
if (req.nextUrl.pathname === '/login') {
url.pathname = '/';
return NextResponse.redirect(url);
}
return NextResponse.next();
}
url.pathname = '/login';
return NextResponse.rewrite(url);
}

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 treat global user/user data in react?

I'm building a ReactJS project and I'm using something like this, to provide user data trough the app:
function Comp1() {
const [user, setUser] = useState({});
firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
return (
<div>
<UserProvider.Provider value={{user, setUser}}>
<Comp2 />
<Comp3 />
</UserProvider.Provider>
</div>
);
}
function Comp2(props) {
const { user, setUser } = useContext(UserProvider);
return (
<div>
{user.exists}
</div>
)
}
function Comp3(props) {
const { user, setUser } = useContext(UserProvider);
return (
<div>
{user.exists}
</div>
)
}
//User Provider
import React from 'react';
const UserProvider = React.createContext();
export default UserProvider;
So, in this case, Comp1 provides user data to Comp2 & Comp3. The only problem is that when the user state changes or the page loads, it creates an infinite loop. If I'm not using an useState for storing the user data, then when it changes, the components do not get re-rendered. I also tried to do something like this in the index.js file:
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
ReactDOM.render(<Comp1 user={user} />, document.getElementById('root'));
} else {
ReactDOM.render(<Comp1 user={{exists: false}} />, document.getElementById('root'));
}
});
But this worked a bit weirdly sometimes, and it's kinda messy. What solutions are there? Thanks.
Edit: I'm triening to do it in the wrong way? How should I provide all user data with only one firebase query?
I suggest using some state container for the application to easily manipulate with a user. The most common solution is to use Redux. With redux, you will be able to have a global state of your app. Generally, all user data stored in it. https://redux.js.org/
The other solution is to use MobX with simple store access. It doesn't use Flux pattern if you don't like it.
If you don't want to use a global store you can use HOCs to propagate your user data. Finally, you can use Context to React, but it is bad approach.
Let's, for example, choose the most popular representer of Flux architecture - Redux.
The first layer is the View layer. Here we will dispatch some action to change global, e.g user data.
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { logIn, logOut } from 'actions'
export default class Page extends React.Component {
useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
logIn(user)
} else {
logOut()
})
}, [])
render () {
...
}
}
const mapDispatchToProps = dispatch => bindActionCreators({
logIn,
logOut
}, dispatch)
export default connect(null, mapDispatchToProps)(App)
The second layer are actions. Here we work we with our data, working with api, format state and so on. The main goal of actions is to create data to pass it to the reducer.
actions.js
export const logIn = user => dispatch => {
// dispatch action to change global state
dispatch({
type: 'LOG_IN',
payload: user
})
}
export const logOut = user => dispatch => {
dispatch({ type: 'LOG_OUT' })
}
The last thing is the reducers. The only goal of them is to change state. We subscribe here for some actions. Reducer always should be a pure function. State shouldn't be mutated, only overwritten.
appReducer.js
const initialState = {
user: null
}
export default function appReducer (state = initialState, action) {
const { type, payload } = action
switch(type) {
case 'LOG_IN':
return {
...state,
user: payload
}
case: 'LOG_OUT':
return {
...state,
user: null
}
}
}
Then, we can work with the global app state whenever we want.
To do it, we should use react-redux library and Provider HOC
const App = () =>
<Provider store={store}>
<Navigation />
</Provider>
Now, we can have access to any stores inside any component though react-redux connect HOF. It works with React Context API inside of it.
const Page2 = ({ user }) => {
//... manipulate with user
}
// this function gets all stores that you have in the Provider.
const mapStateToProps = (state) => {
user: state.user
}
export default connect(mapStateToProps)(Page2)
By the way, you should choose middleware to work with async code in redux. The most popular that is used in my example is redux-thunk.
More information you can find in the official documentation.
There you can find information about how to make initial store configuration
In Comp1, a new onAuthStateChanged observer is added to firebase.Auth on every render.
Put that statement in a useEffect like:
useEffect(() => {
firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
}, []);
Issue:-
The issue for the loop to happen was due to the way Comp1 was written.
Any statement written within the Comp1 functional component will get executed after ever change in prop or state. So in this case whenever setUser was called Comp1 gets re-render and again it subscribes to the auth change listener which again executes setUser on receiving the data.
function Comp1() {
const [user, setUser] = useState({});
firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
return (
<div>
<UserProvider.Provider value={{user, setUser}}>
<Comp2 />
<Comp3 />
</UserProvider.Provider>
</div>
);
}
Solution:-
You can use useEffect to make statements execute on componentDidMount, componentDidUdate and componentWillUnmount react's life cycles.
// [] is passed as 2 args so that this effect will run once only on mount and unmount.
useEffect(()=> {
const unsubscribe = firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
// this method gets executed on component unmount.
return () => {
unsubscribe();
}
}, []);
I created a replica for the above case, you can check it running here
Take a look at this logic:
function Comp1() {
const [user, setUser] = useState({});
firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
return (
<div>
<UserProvider.Provider value={{user, setUser}}>
<Comp2 />
<Comp3 />
</UserProvider.Provider>
</div>
);
}
You are calling setUser in any case. Instead, you should check whether user is already set and if so, whether it matches _user. Set user to _user if and only if _user differs from user. The way it goes, setUser is triggered, which triggers Comp2 and Comp3 change, which triggers the event above which triggers setUser.

Categories