I am using Firebase Realtime Database for a site I am developing with React. In a useEffect method, I am using Firebase's get method to receive all the data from the database and it works when I switch from the home page back to the page I am displaying the data on but it doesn't work when I refresh my page. I have tried using an async await function, console.logging everything I could think of, and re-writing the entire code.
This is my useEffect method that fetches an input that was previously saved to the database. If I switch from the 'Journal' Router page to Home page and back, it loads correctly but it doesn't load correctly if I refresh the page. When I refresh, it console.logs 'No Data' but I know the data exists because when I switch between router pages it does load.
useEffect(() => {
const dbRef = ref(getDatabase())
//Fetches dreams from firebase's database
get(child(dbRef, `/${user.uid}/dreams`)).then(snapshot => {
if (snapshot.exists()){
const dreams = snapshot.val()
Object.values(dreams).forEach(dream => {
setUserDreams(prev => [...prev, dream])
})
} else {
console.log('No Data')
}
}).catch(err => {
console.error(err);
})
...
}, [])
The JSON structure of the database is basically this
"USER_ID" : {
"dreams" : [{"RANDOM_UUID" : {...}}],
"tags" : [{"RANDOM_UUID" : {...}}]
}
The user ID is the uid that firebase generates in their user authentication feature and it doesn't change and the random uuid is a random string generated from the firebase uuidv4 method.
This is how the user variable is populated:
import {createContext, useContext, useEffect, useState} from 'react'
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
updateProfile,
onAuthStateChanged
} from 'firebase/auth';
import { auth } from '../firebase-config';
const UserContext = createContext();
export const AuthContextProvider = ({children}) => {
const [user, setUser] = useState({})
const createUser = (email, password) => {
return createUserWithEmailAndPassword(auth, email, password);
}
const updateUsername = (username) => {
return updateProfile(auth.currentUser, {
displayName: username
})
}
const signIn = (email, password) => {
return signInWithEmailAndPassword(auth, email, password);
}
const logout = () => {
return signOut(auth);
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
console.log(currentUser)
setUser(currentUser)
})
return () => {
unsubscribe()
}
}, [])
return (
<UserContext.Provider value={{createUser, user, logout, signIn, updateUsername}}>
{children}
</UserContext.Provider>
)
}
export const UserAuth = () => {
return useContext(UserContext)
}
Sorry if this is a bit weird but I figured out the issue. After logging the user variable in my journal file, I learned that it isn't populated until after that useEffect is ran so I just put user as the dependency variable in my useEffect hook so it waits until it is populated to run that hook.
useEffect(() => {
const dbRef = ref(getDatabase())
//Fetches dreams from firebase's database
get(child(dbRef, `/${user.uid}/dreams`)).then(snapshot => {
if (snapshot.exists()){
const dreams = snapshot.val()
Object.values(dreams).forEach(dream => {
setUserDreams(prev => [...prev, dream])
})
} else {
console.log('No Data')
}
}).catch(err => {
console.error(err);
})
...
}, [user])
This is what worked, the only thing changed was the dependency array. Meaning, the user variable was populated after the useEffect hook ran which is what made me have issues. Thanks for the commenter that helped me out!
Related
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])
I know the reason why this is haapening, but what I want is a solution for this.
Code:
useAuth.hook.ts
import { useEffect, useState } from "react";
import { getAuth, onAuthStateChanged, User } from "firebase/auth";
const auth = getAuth();
export function useAuth() {
const [user, setUser] = useState<User>();
useEffect(() => {
const unsubscribeFromAuthStateChanged = onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
setUser(user);
} else {
// User is signed out
setUser(undefined);
}
});
return unsubscribeFromAuthStateChanged;
}, []);
return user;
}
usage:
const user = useAuth();
// return user ? <LandingScreen /> : <AuthStack />;
if (user) {
return <UserStack />;
} else {
return <AuthStack />;
}
value returned first time: undefined
value returned second time: user from firebase
I know the reson that useEffect renders twice. But is there a solution for this. Because of this in react native app login screen is rendered first and then after few milliseconds main screen.
I looked at multiple answers on Stackoverflow and github issue. I need a workaround this thing so only main screen is rendered.
As your code currently exist, you will always initially render your AuthStack, as the initial value of user is undefined. You could add an additional piece of state to useAuth:
import { useEffect, useState } from "react";
import { getAuth, onAuthStateChanged, User } from "firebase/auth";
const auth = getAuth();
export function useAuth() {
const [user, setUser] = useState<User>();
const [isLoading,setIsLoading] = useState(true);
useEffect(() => {
const unsubscribeFromAuthStateChanged = onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
setUser(user);
} else {
// User is signed out
setUser(undefined);
}
setIsLoading(false);
});
return unsubscribeFromAuthStateChanged;
}, []);
return {user, isLoading};
}
Usage:
const {user, isLoading} = useAuth();
// return user ? <LandingScreen /> : <AuthStack />;
if(isLoading){
return <Text>Loading...</Text>
}
if (user) {
return <UserStack />;
} else {
return <AuthStack />;
}
While this won't fix the fact that user is initial undefined, it will prevent going to the AuthStack while auth is confirming the user. If you are willing to use different forms of state management you could jotai to store the user. It stores state as atoms independent of components, meaning easy and simple global state. It also have an util function atomWithStorage which reads from storage for the initial value, and rewrites storage on atom updates:
import { useEffect, useState } from "react";
import { getAuth, onAuthStateChanged, User } from "firebase/auth";
import AsyncStorage from "#react-native-async-storage/async-storage";
const auth = getAuth();
// configure atomWithStorage to use AsyncStorage
export const userAtom = atomWithStorage<user>(
"#firebaseUser",
undefined,
{
getItem: (key) => {
return AsyncStorage.getItem(key)
.then((str) => JSON.parse(str))
.catch((err) => {
console.log("Error retrieving value:", err);
return undefined;
});
},
setItem: (key, newValue) => {
return AsyncStorage.setItem(key, JSON.stringify(user)).catch(
(err) => {
console.log("Error storing value:", err);
}
);
},
removeItem: (key) => AsyncStorage.removeItem(key),
delayInit: true,
}
);
export function useAuth() {
const [user, setUser] = useAtom<User>();
const [isLoading,setIsLoading] = useState(true);
const [isAuthorized, setIsAuthorized] = useState(Boolean(user))
useEffect(() => {
const unsubscribeFromAuthStateChanged = onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
setUser(user);
} else {
// overwriting user here will cause AsyncStorage to overwrite
}
setIsLoading(false);
setIsAuthorized(Boolean(user))
});
return unsubscribeFromAuthStateChanged;
}, []);
return {user, isLoading, isAuthorized};
}
Usage:
const {user, isLoading, isAuthorized} = useAuth();
// return user ? <LandingScreen /> : <AuthStack />;
if(isLoading){
return <Text>Loading...</Text>
}
else if (isAuthorized) {
return <UserStack />;
} else {
return <AuthStack />;
}
I would like to know how to store the data in the cache when the user come to my website first time and for the subsequent visit I want to fetch the data from cache and only want to fetch new data/updated data from the server.
Right now, it fetches data every time user comes to my website, which causes a lot of reads, so I want to reduce those reads by storing data in the cache.
My code:
import { useEffect, useState } from "react"
// firebase import
import { collection, limit, onSnapshot, orderBy, query, where } from "firebase/firestore"
import { db } from "../firebase/config"
export const useCollection = (c) => {
const [documents, setDocuments] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let ref = collection(db, c)
const unsubscribe = onSnapshot(ref, (snapshot) => {
const results = []
snapshot.docs.forEach(
(doc) => {
results.push({ ...doc.data(), id: doc.id })
},
(error) => {
console.log(error)
setError("could not fetch the data")
}
)
// update state
setDocuments(results)
setIsLoading(false)
setError(null)
})
// unsubscribe to the previous listener before running the side effect again
return () => unsubscribe()
}, [openTab])
return { documents, error, isLoading }
}
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 }
}
I am trying to route to '/' after the user successfully logs in.
Currently, in my Login.JS file, I have the handleSubmit function that gets the result from the form:
async function handleSubmit(e) {
e.preventDefault()
try {
setError("")
setLoading(true)
await login(emailRef.current.value, passwordRef.current.value)
history.push("/")
} catch(error) {
console.log(error)
setError("Failed to Log In")
}
setLoading(false)
}
Then, I have a AuthContext that passes the Login context
import React, { useContext, useEffect, useState } from 'react';
import { onAuthStateChanged, getAuth, signInWithEmailAndPassword} from 'firebase/auth';
import app from '../firebase'
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const auth = getAuth()
const [currentUser, setCurrentUser] = useState()
const [loading, setLoading] = useState(true)
function login(email, password) {
return signInWithEmailAndPassword(auth, email, password)
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setCurrentUser(user)
setLoading(false)
})
return unsubscribe;
}, [auth])
const value = {
currentUser,
login
}
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
)
}
I can see that the user is able to LogIn, however, it doesn't render anything in the '/' page and shows this error message in the console:
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.
There are two possible spots that might be causing this; i'm not sure which one it is.
First possibility: in the use effect, you set state twice back to back. React tries to batch multiple set states and do a single render, but since this is an asynchronous callback outside of react's control, it can't do so here. So you'll probably get one render for changing the user, then another render for changing loading. If that first render causes the component to unmount, it might result in the error when you set loading.
In react 18, this batching issue will be gone, but until then, you can make the two set states happen as a group like this:
import { unstable_batchedUpdates } from "react-dom";
// ...
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
unstable_batchedUpdates(() => {
setCurrentUser(user);
setLoading(false);
});
});
return unsubscribe;
}, [auth]);
Second possibility: it could be in handleSubmit. You set some state, kick off the login and await it, push to history, then set state again. If the component unmounts while waiting for the promise, or when pushing to history, you would get this issue. If this is the cause, then you can have a ref which gets updated when the component unmounts, and check that ref before doing your final set state:
const mounted = useRef(true);
useEffect(() => {
return () => {
mounted.current = false;
}
}, []);
async function handleSubmit(e) {
e.preventDefault();
try {
setError("");
setLoading(true);
await login(emailRef.current.value, passwordRef.current.value);
history.push("/");
} catch (error) {
console.log(error);
if (mounted.current) {
setError("Failed to Log In");
}
}
if (mounted.current) {
setLoading(false);
}
}
P.S, they will be removing this warning from react because of all the false positives it results in, your case being one of them. There's no actual memory leak in your code. You set state a single time after unmounting, which react harmlessly ignores, and then that's it You are correctly tearing down your onAuthStateChanged listener.
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setCurrentUser(user)
setLoading(false)
})
return unsubscribe;
}, [auth])
You can not use a return statement rather than a cleanup function in useEffect hook.