React doesn't call useEffect on firebase (v9) auth update - javascript

I'm uploading a user profile image into firebase storage, then updating the photoURL value of the auth user with updateProfile(). After that I want the user to be updated without manually refreshing the page. I've been trying this for days now and the issue gets more weird every time I try to debug it.
The interesting thing is the user object seems to be already updated when I log it with console.log(currentUser) after the then promise of updateProfile() is fulfilled. So the new photoURL is already present in the currentUser object. But it seems to not call a state update or console.log("!!!!currentAuthUserUpdate", user);. So the user image wouldn't refresh in my page.
I even tried it with doing a useEffect with the currentUser as a dependency but it wasn't fired. Still, the currentUser object changed when logging it after updateProfile()
Updating the profile, UpdateUserImage.tsx:
import { useAuth } from "../../contexts/AuthContext";
const { currentUser } = useAuth();
// updating the user profile
updateProfile(currentUser, { photoURL })
AuthContext.tsx:
import { auth } from "./../firebase/firebase";
const [currentUser, setCurrentUser] = useState(null);
const auth = getAuth(app);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
console.log("!!!!currentAuthUserUpdate", user);
// I tried setting the user as a custom new object: const userData = { ...user };
setCurrentUser(user);
});
return unsubscribe;
}, []);
firebase.js
import { getAuth } from "firebase/auth";
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
export {
auth
};
What I tried additionally: (But this wouldn't work as the user is already updated without a state refresh from react, so it's trying to replace the same object)
const reloadUser = async () => {
try {
const res = await currentUser.reload();
const user = auth.currentUser;
console.log("currentUser:", user);
setCurrentUser(auth.currentUser);
console.log("res", res);
} catch (err) {
console.log(err);
}
};

it's not auth.onAuthStateChanged. You need to import onAuthStateChanged from 'firebase/auth'
import { getAuth, onAuthStateChanged } from "firebase/auth"
const auth = getAuth(); // leave getAuth empty if you only have one app
Then in your useEffect it should be
onAuthStateChanged(auth, async (currentUser) => { ... }

The setCurrentUser function returned from useState isn't always the same function in my experience.
You can try passing it as a dependency into the useEffect - but I don't think that's what you want.
React lets you use an old useState setter if you give it an updater function, rather than a value: setCurrentUser(()=>auth.currentUser)
The React docs dispute this though.

Using useStates are good for re-rendering components. However going into utilizing useRefs are best for updating the actual variable and will not cause a re-render of the component.
Declare it like:const currentUser = useRef(null)
Update it like: currentUser.current = updatedUserData
Use in code like: currentUser.current.photoURL

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])

Waiting for Firebase RTDB to update state

I am creating a data context for my react native app. There is an array of property objects in a firebase node and the aim is to pull this data into the app on load and provide that data throughout the app. I have a loader that shows if the loading state is true and should only be false if the data has been successfully pulled from firebase.
The issue I am having is the Loader ends before all the data is called and then the properties are not shown on the home page of the app until i refresh the app again.
Below is the code for the app:
import { onValue, ref } from 'firebase/database'
import React, { useEffect, useState } from 'react'
import { projectDatabase } from '../../config'
import useAuth from '../hooks/useAuth'
export const DataContext = React.createContext()
const DataLayer = ({ children }) => {
const { userCredentials: user } = useAuth() //get user data
const [company, setCompany] = useState('');
const [loading, setLoading] = useState(true);
const [properties, setProperties] = useState([]);
useEffect(() => {
const userRef = ref(projectDatabase, 'users/' + user.uid + '/company');
onValue(userRef, (snapshot) => {
setCompany(snapshot.val());
const propertiesRef = ref(projectDatabase, 'providers/' + snapshot.val() + '/properties');
onValue(propertiesRef, (propertiesSnapshot) => {
// console.log(propertiesSnapshot.val())
setProperties(propertiesSnapshot.val());
setLoading(false);
});
});
}, [])
return (
<DataContext.Provider
value={{
properties:properties,
company: company,
loading: loading,
userEnquiries:userEnquiries,
chatId:chatId,
acceptedBookings:acceptedBookings
}}
>
{children}
</DataContext.Provider>
)
}
export default DataLayer
Is there a way to make the call to the firebase RTDB complete BEFORE the loading is resolved as false or do i just do a useEffect call to every page the data is needed? For context the array has 200 properties.
I would really appreciate any help I can get. I love firebase and would really want this to work.

Is there a way to make this asynchronous?

This seems like an easy one and that I'm just missing something obvious, but a little background:
I am making a mock "bug reporting" web app. The app has two types of user: "user" and "engineer". Upon signup the user is assigned a type, and it's saved in a firebase collection "users" under a firebase unique identifier "uid".
Upon login to the web app, a firebase user object for the logged in user is retrieved from firebase. This object has "user.uid" on it.
In order to grab the user type (saved as userType in the firebase document) I need to take that user.uid and send the uid part to a react hook I have made. This hook then fetches the document with that uid and returns the userType.
I've made the following dummy file to demonstrate.
import {React, useEffect, useState} from 'react'
import { useAuthContext } from '../../hooks/useAuthContext'
import { useDocument } from '../../hooks/useDocument'
export default function User() {
const { user } = useAuthContext();
console.log("uid: " + user.uid)
const id = user.uid;
console.log("id: " + id)
let { document, error } = useDocument("users", id)
console.log("userType: " + document.userType)
return (
<div>
</div>
)
}
Now the problem I have is that "user" isn't initalised from Context in time before the program tries to go fetch it, using a uid that, again, isn't initialised in time.
Basically I just need a way to delay using the useDocument hook, but I can't make hooks asynchronous. My async skills aren't my strongest point, to say the least.
Any help is massively appreciated, I've spent so many hours trying to crack this.
EDIT: Upon request, here is the useDocument hook:
import { useEffect, useState } from "react"
import { projectFirestore } from "../firebase/config"
export const useDocument = (collection, id) => {
const [document, setDocument] = useState(null)
const [error, setError] = useState(null)
// realtime document data
useEffect(() => {
const ref = projectFirestore.collection(collection).doc(id)
const unsubscribe = ref.onSnapshot(snapshot => {
// need to make sure the doc exists & has data
if(snapshot.data()) {
setDocument({...snapshot.data(), id: snapshot.id})
setError(null)
}
else {
setError('No such document exists')
}
}, err => {
console.log(err.message)
setError('failed to get document')
})
// unsubscribe on unmount
return () => unsubscribe()
}, [collection, id])
return { document, error }
}

How to properly update user information in firebase?

I have a code that is a form, basically is suppose to update the user information.
According to Firebase documentation manage user but for some reason I'm getting this errors
This is what I have I don't think I'm missing anything is pretty much the same
The only difference is that on my firebase.js I made new cons to make codes shorter but that shouldn't affect at all.
firebase.js
const db = firebase.firestore();
const auth = firebase.auth();
const storage = firebase.storage();
export {db, auth, storage};
Code
import {auth} from "./firebase"
const authUser = auth.currentUser
const update = (e) => {
e.preventDefault();
authUser.updateProfile({
displayName: {fullName}
})
}
I just wanted to test it on the user name first and then do all the others but I don't understand why is not working is super simple.
Update
authUser is undefined. This means that auth.currentUser was undefined at the time when it was assigned to authUser. This probably means that the user is not logged in. You will need to handle the case when the user is not logged in, like
import {auth} from "./firebase"
const authUser = auth.currentUser
const update = (e) => {
e.preventDefault();
if (authUser) {
authUser.updateProfile({
displayName: fullName
})
} else {
//User is not logged in, handle that case here
}
}
import {auth} from "./firebase";
const authUser = auth.currentUser;
const update = (e) => {
e.preventDefault();
authUser?.updateProfile({
displayName: fullName
});
}

Firebase createUserWithEmailAndPassword not doing anything in Next.js

I am trying to use Firebase Authentication on my Next.js website. For some reason, when I press the sign up button, nothing happens and no errors are logged. It just refreshes the page. It doesn't even set any cookies or create a user.
Here are my two files related to authentication:
utils/authProvider.js:
import firebase from 'firebase/app';
import 'firebase/auth';
if(!firebase.apps.length) {
firebase.initializeApp({
// config
});
}
const auth = firebase.auth();
module.exports = { auth };
pages/signup.js:
import { useState } from 'react'
import { LockClosedIcon } from '#heroicons/react/solid'
import { auth } from '../utils/authProvider'
export default function CreateAccount() {
const [emailField, setEmailField] = useState('');
const [passwordField, setPasswordField] = useState('');
// emailField and passwordField are set correctly, I used console.log to test it
const createAccount = () => {
// this event does get triggered, I used console.log to test it
auth.createUserWithEmailAndPassword(emailField, passwordField)
.then((userCredential) => {
window.location.replace('/');
console.log('logged in!');
}).catch((error) => {
console.error(error);
});
}
Edit: After experimenting some more, I saw that the request to https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser never gets completed. It just shows as red in the network traffic tab of Chrome dev tools. Any reason why this might happen?
Thanks in advance.

Categories