I am creating a user Context to keep track of my app's user data like name, email, phone etc. Where & how should I be updating this user context? Currently I am updating the context in the same component functions that are calling the database to update the user record. Is this a correct pattern? Or should the context be automatically updated by the database record changes somehow? Separately on authentication, I am reading the user record from the database and setting it on the user context. Is it correct to set the user on auth or should it be on every load? What is the life cycle of the context? Under which conditions should I update the user context? Is updating the context always manual or is there some method to keep it in sync with the database changes?
I've done lots of reading and asking around and can't seem to figure this out. Thanks in advance!
Tech stack: Firebase Firestore, React Native, Expo.
index.tsx
function RootNavigator() {
const { authUser, setAuthUser, user, setUser } = useContext(AuthContext);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// onAuthStateChanged returns an unsubscriber
const unsubscribeAuth = auth.onAuthStateChanged(
async (authenticatedUser) => {
authenticatedUser ? setAuthUser(authenticatedUser) : setAuthUser(null);
setIsLoading(false);
}
);
// unsubscribe auth listener on unmount
return unsubscribeAuth;
}, [authUser]);
// calls the server to populate the user object & set it in context
useEffect(() => {
const loadUserFromFirestore = async () => {
const dbUser = await getUser(authUser?.uid);
if (dbUser !== null) {
// sets user context
setUser(dbUser);
} else {
}
};
loadUserFromFirestore();
}, [authUser]); // is this the correct condition?
EditProfileScreen.tsx
export default function EditProfileScreen({
navigation,
}: RootStackScreenProps<"EditProfile">) {
const { user, setUser } = useContext(AuthContext);
const saveProfile = () => {
// update user in database
updateUser({
id: authUser?.uid,
firstName: firstName,
lastName: lastName,
email: email,
} as User);
// updating user context
// context should only be updated from database state?
// how often does context get updated? is there a way to update this when we know db data changed?
setUser({
...user,
firstName: firstName,
lastName: lastName,
email: email,
});
navigation.goBack();
};
firebaseMethods.tsx
export async function updateUser(user: User) {
console.log("Updating user");
const userRef = doc(database, "users", user.id);
await updateDoc(userRef, {
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
updatedAt: serverTimestamp(),
});
}
Related
Tech: Firebase, Next.js, Google Sign in, Firebase Stripe exstension
Bug reproduction:
When login with Google
Subscribe on stripe
Stripe saves subscription data for that user in firestore
Logout
Login in with Google and old data are overide with new one, and Subscription is lost
Does anyone had similar problem?
Maybe my implementation of Sign-in is bad, here is the Google Sign in code:
const handleGoogleLogin = () => {
signInWithPopup(auth, googleProvider)
.then(async result => {
if (!result.user) return;
const { displayName, email, uid, providerData, photoURL, phoneNumber } =
result.user;
const name = splitName(displayName as string);
const providerId =
(providerData.length && providerData[0]?.providerId) || '';
const data = {
firstName: name?.firstName || '',
lastName: name?.lastName || '',
email,
photoURL,
phoneNumber,
providerId,
};
await updateUser(uid, data);
})
.catch(error => {
console.log('Google login error: ', error);
});
};
Update user function:
export const updateUser = async (uid: string, data: UpdateUserParams) => {
try {
if (!uid) {
return;
}
await setDoc(doc(firestore, 'users', uid), {
account: {
...data,
initials: `${data.firstName[0]}${data.lastName[0]}`,
},
});
} catch (error) {
console.error('Error updating user: ', error);
}
};
setDoc is overwriting the contents of the document with each sign-in. You should instead use set with merge to prevent overwriting the fields you don't want to lose, or check first if the document exists before creating it.
See also:
https://firebase.google.com/docs/firestore/manage-data/add-data#set_a_document
Difference Between Firestore Set with {merge: true} and Update
I sort of know why it happens, but not sure how to go on about solving it.
I have a React project that uses Cloud Firestore as database, and I have a simple login-page where you can sign in via your Google account. The first time you sign in a new document gets added to the "users" collection in Firebase.
After the document has been created it fetches that user data from Firebase and stores it in Redux.
const signInWithGoogle = async () => {
try {
const res = await signInWithPopup(auth, googleProvider);
const user = res.user;
const q = query(collection(db, "users"), where("uid", "==", user.uid));
const docs = await getDocs(q);
if(docs.docs.length === 0){
const firstName = user.displayName.split(' ')[0];
await addDoc(collection(db, "users"), {
uid: user.uid,
name: user.displayName,
firstName: firstName,
photoURL: user.photoURL,
authProvider: "google",
email: user.email,
})
dispatch(getUser(user))
}
} catch(err) {
console.error(err);
alert(err.message);
}
}
I also check whenever the user's auth state changes (here I also do another fetch and store it in Redux).
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
if(user){
dispatch(getUser(user))
} else {
console.log("user logout")
}
});
return unsubscribe;
}, []);
But when a new user signs in the first time, I get an error from the fetch:
export const getUser = createAsyncThunk("profile/getUser", async (user) => {
try {
const userQuery = query(
collection(db, "users"),
where("uid", "==", user?.uid)
);
const doc = await getDocs(userQuery);
const data = doc.docs[0].data();
return data;
} catch (err) {
console.error(err);
alert("An error occured while fetching user data");
}
});
"data" in above block is undefined for a small moment when the user signs in, so the alert in the try/catch block always goes off (it does manage to fetch the data after though).
This error only happens when it's a new user.
I understand that the fetch occurs before a document has been created in the "users" collection, but I'm not sure how to solve this. I've tried to add if/else to certain parts of the code (but just felt like I was grasping for straws).
I'm very much new to Firebase and still learning React, so every bit of help is really appreciated!
Problem is that your signInWithGoogle & useEffect both are running on user's auth status change. And, when its the new user, signInWithGoogle function makes aysnc call to create default doc, whereas useEffect runs to dispatch action, but at that moment user doesn't have any linked document. That is why you are getting undefined.
Ideally, you should remove one. You can merge the useEffect into signInWithGoogle to set the user details and dispatch as well.
const signInWithGoogle = async () => {
try {
const res = await signInWithPopup(auth, googleProvider);
const user = res.user;
const q = query(collection(db, "users"), where("uid", "==", user.uid));
const docs = await getDocs(q);
// create `doc` if its the new user
if(docs.docs.length === 0){
const firstName = user.displayName.split(' ')[0];
await addDoc(collection(db, "users"), {
uid: user.uid,
name: user.displayName,
firstName: firstName,
photoURL: user.photoURL,
authProvider: "google",
email: user.email,
})
}
// set user info and dispatch
setCurrentUser(user);
setLoading(false);
dispatch(getUser(user))
} catch(err) {
console.error(err);
alert(err.message);
}
}
Hope that answers your query.
Thanks
I have this function that fetches user's info from FireBase and saves it to useState:
const [fireBaseUser, setfireBaseUser] = useState('');
const getUserData1 = useCallback(async(uid) => {
const userRef = doc(db, "users", uid);
const docSnap = await getDoc(userRef); // not getDocs
console.log(docSnap.data());
if(docSnap.data()) {
setfireBaseUser(docSnap.data());
} else {
return false; // .data() if undefined is doc doesn't exist
}
}, [fireBaseUser]);
and this function gets run each time there is a change to a state called 'currentUser' which
is tied to Firebase's onAuthStateChanged function which provies the latest instance of user's authentication state. After successful authentication, dBCtx.getUserData1 function
gets called and brings in other user information from FireStore which saves these user information to 'fireBaseUser' state.
in useEffect like below:
const [currentUser, setCurrentUser] = useState('');
useEffect(() => {
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
setCurrentUser(user);
console.log('currentuser:');
console.log(user.uid);
dBCtx.getUserData1(user.uid)
.then((result)=>{
if(result === false) {
console.log('creating a user profile for' + user.uid)
dBCtx.createUser(user.displayName, user.email, user.uid);
}
});
// ...
} else {
console.log('not logged in')
}
});
}, [currentUser])
I have another useEffect that works just below that listens in on 'fireBaseUser' state changes to update changes whenever the user makes changes to their profile.
useEffect(() => {
console.log('firebase User');
console.log(dBCtx.fireBaseUser);
getUserData1(dBCtx.fireBaseUser.uid);
}, [dBCtx.fireBaseUser])
For example:
if a user decides to change his name:
async function updateName(newName) {
const userRef = doc(db, "users", fireBaseUser.uid);
await updateDoc(userRef, {
displayName: newName
});
const docSnap = await getDoc(userRef);
setfireBaseUser(prevState => {
return {displayName: docSnap.displayName,
...prevState}
});}
But at the moment, as soon as my app.js starts up, the app/react goes into an infinite loop.
because of this code below in the second useEffect as I have described.
So the question is...
How can I update changes to useEffect withouth ending up in infinite loop???
Please let me know!
I am able to sign up a new user, but how can I store that user in a collection in firestore? I am not sure with firebase v9 if the addDoc function is in the right place, but I don't know where else or how to code it.
export const useSignup = () => {
const [error, setError] = useState("");
const { dispatch } = useAuthContext();
const signup = (email: string, password: string, username: string) => {
setError("");
createUserWithEmailAndPassword(auth, email, password)
.then((res) => {
dispatch({ type: "LOGIN", payload: res.user });
const uid = res.user.uid;
const data = {
id: uid,
email,
username,
};
const ref = collection(db, "users");
addDoc(ref, {
data,
});
})
.catch((err) => {
setError(err.message);
});
};
return { error, signup };
};
To create a user-document for a newly signed-up user, you can do the following:
Access their bid from the user object (like you already did).
Create a document reference whose path ends in the above mid. Be aware that this document does not exist yet.
Use the setDoc method with above document reference, and user data to be stored as inputs.
It looks like the following in the form of code:
const uid = res.user.uid;
const data = {
id: uid,
email,
username,
};
const ref = collection(db, `users/${uid}`);
setDoc(ref, data)
.then(() => console.log("Created New User Document Successfully"))
.catch(() => console.log("Error"))
You must write the above code right after your dispatch call. Hope this helps!
I have the following function to authenticate a user with React Native (Expo) and Firebase:
export default function AuthenticateUser() {
const user = useStore((state) => state.user); // Gets the user from state
const setUser = useStore((state) => state.setUser);
const setLoadingUser = useStore((state) => state.setLoadingUser);
const [GQL_getOrCreateUser] = useMutation(getOrCreateUser); // GraphQL function
useEffect(() => {
let unsubscribe: any;
let urlHandler: any;
function handleUrl(event: any) {
const { url }: { url: string } = event;
if (url.includes('/account')) {
const isSignInWithEmailLink = firebase.auth().isSignInWithEmailLink(url);
if (isSignInWithEmailLink) {
AsyncStorage.getItem('unverifiedEmail').then((email) => {
if (email) {
firebase
.auth()
.signInWithEmailLink(email, url)
.then(() => {
// We are signed in
})
.catch((error) => {
// Failed to sign in
});
} else {
// Missing pending email from AsyncStorage
}
});
}
};
}
function handleLinking(userDetails: User) {
urlHandler = ({ url }: { url: string }) => {
handleUrl({ url, userDetails });
};
// Listen to incoming deep link when app first opens
Linking.getInitialURL().then((url) => {
if (url) {
handleUrl({ url, userDetails });
}
});
// Listen to incoming deep link while app is open
Linking.addEventListener('url', urlHandler);
}
if (!user) {
unsubscribe = firebase.auth().onAuthStateChanged((authenticatedUser) => {
setLoadingUser(false);
if (authenticatedUser) {
const uid = authenticatedUser.uid;
const phoneNumber = authenticatedUser.phoneNumber;
let email: string;
if (authenticatedUser.email) {
email = authenticatedUser.email;
} else {
// retreiving email from AsyncStorage. We add it there when requesting a passwordless sign in link email, as recommended by Firebase.
AsyncStorage.getItem('unverifiedEmail').then((unverifiedEmail) => {
email = unverifiedEmail ? unverifiedEmail : '';
});
}
const emailVerified = authenticatedUser.emailVerified;
// Updating user record with GraphQL
GQL_getOrCreateUser({ variables: { uid, phoneNumber } })
.then(async (document) => {
const data = document.data.getOrCreateUser;
const userDetails = { ...data, phoneNumber, email, emailVerified }
setUser(userDetails); // Setting the stateful user record
handleLinking(userDetails); // Handle deeplink
})
.catch(() => {
// GraphQL failed
});
}
});
}
return () => {
unsubscribe?.();
// Removing event listener;
Linking.removeEventListener('url', urlHandler);
};
}, [GQL_getOrCreateUser, setLoadingUser, setUser, user]);
}
My problem is that the sign in method runs too often resulting in unexpected behavior.
I suspect it is caused by re-rendering triggered by the user auth state and the GraphQL running (GraphQL call to get or create a user causes three renders, which seems to be how it should behave).
I use deeplinking to handle passwordless email sign-in (firebase.auth().isSignInWithEmailLink(url))
The URL is detected with either Linking.getInitialURL (when the deeplink opens the app) or Linking.addEventListener('url', handler) when the app is already running.
As example, let's take scenario 1: Linking.getInitialUrl
I click the link. It asks to open the app.
The app opens
The user is not logged in (user is not in the app state) so the code inside the if (!user) is triggered.
The user email is in AsyncStorage because we just requested the login link email and I save it when the user asks for it.
GraphQL fetches the user and causes two more renders
I set the user in state with setUser and run handleLinking.
Because the app was closed, getInitialURL for the URL is triggered and it goes correctly through the steps and signs me in.
HOWEVER, handleLinking runs a second time (possibly the extra two renders caused by GraphQL trigger a Linking.addEventListener event to fire?) and returns an error because the sign in link cannot be used a second time.
I think there is a fundamental flow in my logic. What is it? How can this be improved and done correctly?
Thanks for helping!
If any of the objects in your useEffect dependency changes, then the useEffect function will re-run. So if your useEffect callback runs too often, the dependency array is where you should look at.
It is important to know that a "change" can be a simple as an object being reinstantiated or a function being re-created, even though the underlying value remains the same.