I'm using Firebase Web version 9 in a react app. I have a sign-up form with first name, email, and password inputs. When the form is submitted, I need to create an authorized user with Firebase and immediately update that new user's first name and profile picture.
I'm using two Firebase auth functions - createUserWithEmailAndPassword() and updateProfile(). A new user is always created when the form is submitted, but the profile updates only happen once in a while. I haven't been able to pinpoint the cases when the profile update is successful.
Any ideas on what I'm missing? Would love some feedback and guidance. Thank you!
Here's the code for the sign-up page.
import { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import {
StyledHeader,
StyledFooter,
StyledDropdown,
StyledForm,
} from '../styles';
import {
getAuth,
createUserWithEmailAndPassword,
updateProfile,
} from 'firebase/auth';
import { ShortFooter, LanguageSelect, Form } from '../components';
import { logo, p1, p2, p3, p4, p5 } from '../assets';
const SignUp = ({ children }) => {
const [firstName, setfirstName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isFirstNameEmpty, setIsFirstNameEmpty] = useState(true);
const [isEmailEmpty, setIsEmailEmpty] = useState(true);
const [isPasswordEmpty, setIsPasswordEmpty] = useState(true);
const [error, setError] = useState('');
const [checked, setChecked] = useState(true);
const [isShown, setIsShown] = useState(true);
const isInvalid = firstName === '' || email === '' || password === '';
const navigate = useNavigate();
const userProfileImgs = [p1, p2, p3, p4, p5];
useEffect(() => {
setTimeout(() => {
// 👇️ scroll to top on page load
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
}, 100);
}, []);
const handlefirstNameChange = (firstName) => {
if (firstName.length !== 0) {
setIsFirstNameEmpty(false);
} else {
setIsFirstNameEmpty(true);
}
};
const handleEmailChange = (email) => {
if (email.length !== 0) {
setIsEmailEmpty(false);
} else {
setIsEmailEmpty(true);
}
};
const handlePasswordChange = (password) => {
if (password.length !== 0) {
setIsPasswordEmpty(false);
} else {
setIsPasswordEmpty(true);
}
};
const handleCheckbox = () => {
setChecked((checked) => !checked);
};
const handleLearnMore = (e) => {
e.preventDefault();
setIsShown((isShown) => !isShown);
};
const handleSignUp = async (e) => {
e.preventDefault();
try {
// firebase work!
const auth = getAuth();
let { user } = await createUserWithEmailAndPassword(
auth,
email,
password
);
console.debug(`User ${user.uid} created`);
// updating the user's profile with their first name and a random profile image
await updateProfile(user, {
displayName: firstName,
photoURL:
userProfileImgs[Math.floor(Math.random() * userProfileImgs.length)],
});
console.debug(`User profile updated`);
// navigate to the profile page
navigate('/profile');
} catch (e) {
if (e.message === 'Firebase: Error (auth/email-already-in-use).') {
setError('That email is already in use, please try again.');
}
}
};
return (
<>
<StyledHeader height="120">
<div className="header__background">
<div className="header__frame">
<div>
<Link to="/">
<img className="header__logo" src={logo} alt="Netflix Logo" />
</Link>
</div>
</div>
<form onSubmit={handleSignUp} className="form__container">
<StyledForm padding="20px 68px 40px">
<Form
error={error}
isEmailEmpty={isEmailEmpty}
email={email}
setEmail={setEmail}
handleEmailChange={handleEmailChange}
isPasswordEmpty={isPasswordEmpty}
password={password}
setPassword={setPassword}
handlePasswordChange={handlePasswordChange}
isInvalid={isInvalid}
checked={checked}
handleCheckbox={handleCheckbox}
handleLearnMore={handleLearnMore}
isShown={isShown}
isFirstNameEmpty={isFirstNameEmpty}
firstName={firstName}
setfirstName={setfirstName}
handlefirstNameChange={handlefirstNameChange}
method="post"
/>
</StyledForm>
</form>
<div className="signIn__footer-container">
<div className="signIn__footer-background">
<StyledFooter
backgroundColor="transparent"
padding="10px 70px 40px 70px"
>
<ShortFooter>
<StyledDropdown>
<LanguageSelect />
</StyledDropdown>
</ShortFooter>
</StyledFooter>
</div>
</div>
</div>
{children}
</StyledHeader>
</>
);
};
export default SignUp;
This is the error I'm getting in the console.
Error
Based on how my app was set up I ultimately decided to remove the photoURL property altogether since it could be defined elsewhere in the app. After testing, only one of my images worked. This is why I experienced the full profile updates working out sometimes since random images were selected each time a new user signs up. I ended up sticking wth this image as the default image.
As #adsy mentioned, my profile updates weren't happening consistently because certain profile images I had exceeded the allowed input body length for this property. The example in the Docs shows a link with 43 characters but no other information. The path to the profile images in the question are much shorter than this so I still need some understanding.
After researching a bit, I didn't come across the maximum limit set. It's now something to contact Google Developer's support team about. If anyone has the information for the allowed input body length for this property, feel free to share it on this post.
Thanks!
const handleSignUp = async (e) => {
e.preventDefault();
try {
// firebase work!
const auth = getAuth();
let { user } = await createUserWithEmailAndPassword(
auth,
email,
password
);
console.debug(`User ${user.uid} created`);
// updating the user's profile with their first name
await updateProfile(user, {
displayName: firstName,
});
console.debug(`User profile updated`);
// navigate to the profile page
navigate('/profile');
} catch (e) {
if (e.message === 'Firebase: Error (auth/email-already-in-use).') {
setError('That email is already in use, please try again.');
}
}
};
Related
I am trying to get my sign-up form to work using Firebase createUserWithEmailAndPassword and updateProfile functions in my React App. The sign-in function works (right panel), and I am able to see users in Firebase, however, when I try to create the displayName (left panel) I'm running into a reference issue.
My AuthContext provider has my signup function written like so:
export const useAuth = () => {
return useContext(AuthContext);
};
const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
const signup = (email, password) => {
return auth.createUserWithEmailAndPassword(email, password)
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = {
currentUser,
signup,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
};
I imported my useAuth function to my SignUp component and placed it inside my handleSubmit:
const { signup } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
if (passwordRef.current.value !== passwordConfRef.current.value) {
return setError("Please make sure passwords match.");
}
try {
setError("");
setLoading(true);
const user = await signup(
emailRef.current.value,
passwordRef.current.value
)
await updateProfile(user, {
displayName: usernameRef,
uid: user.user.uid,
})
// console.log(user);
navigate(`/dashboard/${user.user.uid}`);
} catch (e) {
setError("Something went wrong, please try again.");
console.log(e.message)
}
setLoading(false);
};
When the following console.log is run on the browser the following populates in the console:
find the error below
"Cannot access 'user' before initialization"
but I don't know what user reference it's referring to.
I read the following docs in reference to creating more login credentials for firebase:
https://firebase.google.com/docs/reference/js/v8/firebase.User#updateprofile
https://github.com/firebase/firebase-functions/issues/95
I originally had the updateProfie in a .then fucntion, however after some assistance I changed it await and now I'm getting a new error:
userInternal.getIdToken is not a function
The Promise chain isn't correct in the handleSubmit handler. The then-able should take a function instead of the result of immediately calling updateProfile. The user parameter likely hasn't been instantiated yet. In other words updateProfile is called when the component is rendered instead of when handleSubmit is called. You are also mixing the async/await and Promise-chain patterns. Use one or the other.
const handleSubmit = async (e) => {
e.preventDefault();
if (passwordRef.current.value !== passwordConfRef.current.value) {
setError("Please make sure passwords match.");
return;
}
try {
setError("");
setLoading(true);
await signup(
emailRef.current.value,
passwordRef.current.value,
);
navigate(`/dashboard/${user.user.id}`);
} catch (e) {
console.warn(e));
setError("Something went wrong, please try again.");
} finally {
setLoading(false);
}
};
You can call separately the updateProfile function when the currentUser state updates.
const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
const signup = (email, password) => {
return auth.createUserWithEmailAndPassword(email, password)
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
useEffect(() => {
if (currentUser) {
updateProfile({
/* profile properties you want to update */
});
}
}, [currentUser]);
const value = {
currentUser,
signup,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
};
I have two users, one is admin and the other client, so when I login as admin I want to display admin screen immediately and also when I login as client display client screen but login as admin I still can see client screen for few seconds then take me to the admin screen and when I logout I go back to client screen not login screen
Login screen code:
const SignIn = ({navigation}) => {
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
useEffect(()=>{
const unsubscribe = auth.onAuthStateChanged(user =>{
if(user){
navigation.navigate('Tabs')
setEmail()
setPassword()
}
})
return unsubscribe
},[])
App screen code:
const App = ({navigation}) => {
const [user, setUser] = React.useState(null) // This user
const [users, setUsers] = React.useState([]) // Other Users
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async user => {
if (user) {
const userData = await db.collection("users").doc(user.uid).get();
setUser(userData.data());
} else {
setUser(null);
}
});
return () => unsubscribe();
}, [])
return (
<NavigationContainer independent={true}>
{user?.role === 'admin'? (<AdminNavi />):(<UserNavi/>)}
</NavigationContainer>
);
}
export default App;
I feel like I'm not structure the code correctly, because If you look at app screen code I'm repeating the use Effect again and I put the condition between Container Navigator. Please guys anyone of you can Re-structure my code is highly appreciated.
My best guess is that those few seconds are when you're loading the user data from Firestore. This may take some time, and you'll need to decide what you want to display during that time.
The simplest fix is to set the user to null while you're loading their data from Firestore:
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async user => {
if (user) {
setUser(null); // 👈
const userData = await db.collection("users").doc(user.uid).get();
setUser(userData.data());
} else {
setUser(null);
}
});
return () => unsubscribe();
}, [])
This will work, but causes the app to temporarily show the login screen while you're loading the data. If that is not acceptable, you'll want to introduce another state variable for while you're loading the user profile from Firestore:
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async user => {
if (user) {
setLoadingUser(true); // 👈
const userData = await db.collection("users").doc(user.uid).get();
setLoadingUser(false); // 👈
setUser(userData.data());
} else {
setUser(null);
}
});
return () => unsubscribe();
}, [])
And then you'd use the loadUser state property to render a "Loading user profile..." type indicator.
I want to only show the "Load More" button when I have extra documents to show on my React and Firebase website.
Right now, I'm fetching only 2 documents from a firestore and I want to show the "Load More" button when I have more than 2 documents in my firestore. If I only have 2 or fewer than 2 documents in my firestore, I don't want to show the "Load More" button.
And I want to hide the "Load More" button after fetching all the documents that I have on the firestore.
Anyone, please help me with this!
useCollection Hook:
import { useEffect, useRef, useState } from "react"
// firebase import
import {
collection,
getDocs,
limit,
onSnapshot,
orderBy,
query,
startAfter,
where,
} from "firebase/firestore"
import { db } from "../firebase/config"
export const useCollection = (c, _q, _l, _o) => {
const [documents, setDocuments] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
// if we don't use a ref --> infinite loop in useEffect
// _query is an array and is "different" on every function call
const q = useRef(_q).current
const o = useRef(_o).current
useEffect(() => {
let ref = collection(db, c)
if (q) {
ref = query(ref, where(...q))
}
if (o) {
ref = query(ref, orderBy(...o))
}
if (_l) {
ref = query(ref, limit(_l))
}
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 on unmount
return unsubscribe
}, [])
const fetchMore = async (doc) => {
setIsLoading(true)
const q = query(
collection(db, c),
orderBy(...o),
startAfter(doc.createdAt),
limit(_l)
)
const newDocuments = await getDocs(q)
updateState(newDocuments)
setIsLoading(false)
}
const updateState = (docs) => {
if (!docs.empty) {
const tempPosts = []
docs.forEach((document) => {
tempPosts.push({
id: document.id,
...document.data(),
})
})
setDocuments([...documents, ...tempPosts])
}
}
return { documents, fetchMore, error, isLoading }
}
SolutionComments.js (In this file I'm showing the "Load More Comments" button)
import React, { useState } from "react"
import { useParams } from "react-router-dom"
import { useCollection } from "../../hooks/useCollection"
import Comment from "./Comment"
import CommentForm from "./CommentForm"
const SolutionComments = () => {
const [activeComment, setActiveComment] = useState(null)
const { id } = useParams()
const { documents, fetchMore, isLoading } = useCollection(
`solutions/${id}/comments`,
null,
2,
["createdAt", "desc"]
)
const fetchMoreComments = () => {
fetchMore(documents[documents.length - 1])
}
return (
<div className="mt-10">
<CommentForm docID={id} />
<div>
{documents &&
documents.map((comment) => (
<Comment
key={comment.id}
comment={comment}
replies={comment.replies}
activeComment={activeComment}
setActiveComment={setActiveComment}
/>
))}
</div>
{documents.length > 2 && (
<button onClick={fetchMoreComments} className="text-white bg-purple-500">
{!isLoading ? "Load More Comments!" : "Loading..."}
</button>
)}
</div>
)
}
export default SolutionComments
Firestore does not have a mechanism for automatically telling you how many documents are in a query. You will need to manage that aggregate count yourself. Then you could fetch that aggregate count and use that to determine whether to show a load more button or not.
However this is harder than it sounds. And most modern apps we don't use load more buttons rather we use infinite scroll.
To be honest, it has never been efficient or smart to show a result set count in an app. The fact that people have done it in the past doesn't mean that it is the right thing to do today. It might have made sense when you had small databases, typically running off of a desktop database. But in a cloud-based solution with millions of documents and millions of users, and complex queries, knowing how many documents are in a result set is a very hard problem to solve.
So I was making a blog application where only logged in users can add new blogs. To start, when a user logs in, they will see all the blogs they have previously created on the frontend along with a form to add new ones. However, when the logged in user tries to add a new blog, it updates on the frontend but returns back to the original list they had before when the page is refreshed. I can see the updated blog list when I log out and log back in again. I actually used local storage to make sure that logged in users remain logged in after a new render. I just need help in making sure the new blogs added after login remain on the frontend after a render.
I think I have an idea why it is happening but I am not sure.
So whenever my loginService function is called within the handleLogin function, the server sends back the user info which includes all the blogs they have created. The problem with refreshing is due to the same list of blogs that were there at the time of login unless you log out and log in again.
Any help would be greatly appreciated.
ReactJS code
import { useState, useEffect } from 'react'
import Blog from './components/Blog'
import blogService from './services/blogs'
import loginService from './services/login'
import userService from './services/user'
const App = () => {
const [blogs, setBlogs] = useState([])
const [newBlogs, setNewBlogs] = useState([])
const [user, setUser] = useState(null)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [errorMsg, setErrorMsg] = useState('')
const [blogTitle, setBlogTitle] = useState('')
const [blogAuthor, setBlogAuthor] = useState('')
const [blogUrl, setBlogUrl] = useState('')
useEffect( () => {
if(user != null){
setBlogs(user.blog.concat(newBlog))
}
console.log("blogs is", blogs)
}, [user])
//Seeing if a user is logged in on rerender
useEffect(() => {
const loggedInUser = window.localStorage.getItem('loggedBlogUser')
if(loggedInUser){
const user = JSON.parse(loggedInUser)
setUser(user)
}
},[])
// Logging in users
const handleLogin = async (event) => {
event.preventDefault()
console.log("Logging in,", username, password)
try {
const user = await loginService({username, password})
blogService.setToken(user.token)
window.localStorage.setItem('loggedBlogUser', JSON.stringify(user))
setUser(user)
setUsername('')
setPassword('')
}
catch(error){
setErrorMsg('Wrong credentials')
setTimeout(() => {
setErrorMsg(null)
},[])
}
}
//Logging out users
const handleLogout = () => {
window.localStorage.removeItem('loggedBlogUser')
setUser(null)
setBlogs([])
}
//Adding new blogs
const addNewBlog = async (e) => {
e.preventDefault()
console.log("User here is", user)
try {
const newBlog = {
title: blogTitle,
author: blogAuthor,
url: blogUrl
}
await blogService.createBlog(newBlog)
setBlogs(blogs.concat(newBlog))
setNewBlogs(newBlogs.concat(newBlog))
setBlogTitle('')
setBlogAuthor('')
setBlogUrl('')
}
catch(error){
console.log("error adding new blog", error)
}
console.log("blogs is", blogs)
}
return (
<div>
<h2>blogs</h2>
{user == null && <div className="login-form">
<form onSubmit={handleLogin}>
<div className="username-container">
username
<input type='text' value={username} onChange={(e) => setUsername(e.target.value)} name='username'/>
</div>
<div className="password-container">
password
<input type='password' value={password} onChange={(e) => setPassword(e.target.value)} name='password'/>
</div>
<button type='submit'>Login</button>
</form>
</div>}
{user != null && <div className="notes">
<p>{user.name} logged in <button onClick={handleLogout}>logout</button></p>
</div>}
{user != null && <div className="addBlog-container">
<b>create new</b>
<form onSubmit={addNewBlog}>
<label>Title:</label><input type="text" value={blogTitle} onChange={(e) => setBlogTitle(e.target.value)} name="blog-title"/>
<label>Author:</label><input type="text" value={blogAuthor} onChange={(e) => setBlogAuthor(e.target.value)} name="blog-author"/>
<label>Url:</label><input type="text" value={blogUrl} onChange={(e) => setBlogUrl(e.target.value)} name="blog-url"/>
<button type='submit'>create blog</button>
</form>
</div>}
{blogs != null && blogs.map(blog =>
<Blog key={blog.id} blog={blog} />
)}
</div>
)
}
export default App
At first you are updating blogs from user.blog but in update blog you are only updating blogs variable, that's why new blog disappears as soon as you refresh. Try after updating user.blog with new blog.
I am having issues with my NextJS app. I am trying to display a class (loading spinner) to a button when it tries to log a user in. I am doing this by setting the loading state to true before it calls the login function, and then set the state to false after its done (in the submitForm function), but it doesn't seem to be setting it. Whenever i click the button the state stays at false. Any help would be greatly appreciated.
import { useAuth } from '#/hooks/auth'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import ButtonSpinner from '#/styles/buttonspinner.module.css'
export default function Home() {
const router = useRouter()
const { login } = useAuth({
middleware: 'guest',
redirectIfAuthenticated: '/dashboard',
})
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [errors, setErrors] = useState([])
const [status, setStatus] = useState(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
if (router.query.reset?.length > 0 && errors.length === 0) {
setStatus(atob(router.query.reset))
} else {
setStatus(null)
}
})
const submitForm = async event => {
event.preventDefault()
setLoading(true)
login({ email, password, setErrors, setStatus })
setLoading(false)
}
return (
<div>
<form className="mt-8 space-y-6" onSubmit={submitForm} autoComplete="off">
<button className={"shadow-sm relative w-full " + (loading ? ButtonSpinner['btn-loading'] : "")}>
Sign in
</button>
</form>
</div>
)
}
you are not waiting for login response
const submitForm = async event => {
event.preventDefault()
setLoading(true)
try {
await login({ email, password, setErrors, setStatus });
} finally {
setLoading(false)
}
}