I'm trying to make a Context API that supplies a connected user (not in Firebase Auth) and its respective DB entry in Firestore. I'm using Magic links as the auth method.
Here's my Provider:
const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [signer, setSigner] = useState<ethers.Signer | null>(null);
const [address, setAddress] = useState<string | null>(null);
const [balance, setBalance] = useState<string | null>(null);
const [userDoc, dbLoading] = useCollection(
query(
collection(firestore, "users"),
where("walletAddress", "==", address ?? "") // issue is here <----
)
);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isConnected, setIsConnected] = useState<boolean>(false);
useEffect(() => {
if (!dbLoading && userDoc?.empty) {
addDoc(collection(firestore, "users"), {
walletAddress: address,
});
}
setIsLoading(false);
}, [dbLoading]);
const connect = () => {
setIsLoading(true);
provider
.listAccounts()
.then((accounts) => {
if (accounts.length > 0) {
const signer = provider.getSigner(accounts[0]);
setSigner(signer);
signer.getAddress().then((address) => {
setAddress(address);
});
signer.getBalance().then((balance) => {
setBalance(balance.toString());
});
setIsConnected(true);
}
})
.catch((error) => {
console.log(error);
});
};
const disconnect = () => {
setIsLoading(true);
magic.connect
.disconnect()
.then(() => {
setAddress(null);
setBalance(null);
setIsConnected(false);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
setIsLoading(false);
});
};
return (
<AuthContext.Provider
value={{
userDoc: userDoc?.docs[0]?.data() ?? {},
address,
balance,
isLoading: isLoading || dbLoading,
isConnected,
connect,
disconnect,
}}
>
{children}
</AuthContext.Provider>
);
};
So the problem I'm having is quite straight forward: when the user is not logged in, it creates a new entry with 'walletAddress' of null. Before, I had the same Auth Provider as above, just stripped of all the Firestore logic. But after running into some issues when creating Route Guards, I decided to put it all into a single Provider. Is there a simple solution to this, or do I need to go back to two providers and work from there?
Related
Gets list of emails from firestore and checks if current user is registered and then redirects them to sign up if they are new user.
The code is functional(it redirects succesfully) but get the following error:
arning: Cannot update a component (BrowserRouter) while rendering a different component You should call navigate() in a React.useEffect(), not when your component is first rendered.
const navigate = useNavigate();
let hasEmail = false;
const [emailList, setEmailList] = useState([]);
const emailRef = collection(db, "emails");
useEffect(() => {
const getEmails = async () => {
const data = await getDocs(emailRef);
setEmailList(
data.docs.map((doc) => ({
...doc.data(),
}))
);
};
getEmails();
}, []);
const emailCheck = (emails) => { //checks if email exists
hasEmail = emails.some((e) => e.email === auth.currentUser.email);
};
const direct = () => { // redirects to required page
if (hasEmail) {
navigate("/index");
} else {
navigate("/enterdetails");
}
};
emailCheck(emailList);
direct();
Move the email checking logic into a useEffect hook with a dependency on the emailList state.
const navigate = useNavigate();
const [emailList, setEmailList] = useState([]);
const emailRef = collection(db, "emails");
useEffect(() => {
const getEmails = async () => {
const data = await getDocs(emailRef);
setEmailList(
data.docs.map((doc) => ({
...doc.data(),
}))
);
};
getEmails();
}, []);
useEffect(() => {
if (emailList.length) {
const hasEmail = emailList.some((e) => e.email === auth.currentUser.email);
navigate(hasEmail ? "/index" : "/enterdetails");
}
}, [auth, emailList, navigate]);
This might not run without the proper firebase config but check it out
https://codesandbox.io/s/elated-bell-kopbmp?file=/src/App.js
Things to note:
Use useMemo for hasEmail instead of emailCheck. This will re-run only when emailList changes
const hasEmail = useMemo(() => {
//checks if email exists
return emailList.some((e) => e.email === auth.currentUser.email);
}, [emailList]);
There isn't really a point in having this in a react component if you are just redirecting away. Consider having the content of 'index' at the return (</>) part of this component. Only redirect if they aren't authorized
useEffect(() => {
if (!hasEmail) {
navigate("/enterdetails");
}
//else {
// navigate("/index");
//}
}, [hasEmail, navigate]);
I am using the Auth Provider to manage my Firebase auth information. I want to be able to use currentUser as soon as I sign up, but it won't set without reloading.
I tried to setCurrentUser out of the Auth Provider and set it, but I could not get it to work either.
contexts/Auth.tsx
const AuthContext = createContext<IAuthContext>(null!)
export const AuthProvider = ({
children,
}: {
children: ReactNode
}) => {
const [currentFBUser, setCurrentFBUser] = useState<firebase.User | null>(null)
const [currentUser, setCurrentUser] = useState<any>(null)
const [isLoading, setIsLoading] = useState<boolean>(true)
const { update } = useIntercom()
/**
* SUBSCRIBE user auth state from firebase
*/
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async (user) => {
if (!user) {
setCurrentFBUser(null)
setIsLoading(false)
return
}
await setCurrentFBUser(user)
const storeUser = await userRepository.findById(user.uid)
if (!storeUser) {
setCurrentUser(null)
setIsLoading(false)
return
}
await setCurrentUser(storeUser)
/* UPDATE Intercom props */
if(currentUser) {
update({
name: currentUser.name,
email: currentUser.email
})
}
setIsLoading(false)
return () => {
unsubscribe()
}
})
}, [])
const logout = useCallback(() => {
const auth = getAuth();
signOut(auth).then(() => {
window.location.reload()
}).catch((err) => {
toast.error(err.message)
});
}, [])
return (
<AuthContext.Provider value={{
currentFBUser,
currentUser,
setCurrentUser,
isLoading,
logout,
}}>
{children}
</AuthContext.Provider>
)
}
export const useAuthContext = () => {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within the AuthProvider')
}
return context
}
signup.tsx
...
const { currentFBUser, isLoading, setCurrentUser } = useAuthContext()
const signup = handleSubmit(
async (data) => {
if (data.password != data.confirmPassword) {
toast.error('Your password is not matched!')
return
}
const auth = getAuth()
createUserWithEmailAndPassword(auth, data.email, data.password)
.then((userCredential) => {
const auth = userCredential.user
if (!auth) return
const { email, uid } = auth
if (!email) return
const user = userRepository.findOrCreate(email, uid)
setCurrentUser(user)
})
.catch((err) => {
toast.error(err.message)
});
},
(err: any) => {
toast.error(err.message)
},
)
...
Try to call again getAuth() instead of using the response from createUserWithEmailAndPassword
const { currentFBUser, isLoading, setCurrentUser } = useAuthContext()
const signup = handleSubmit(
async (data) => {
if (data.password != data.confirmPassword) {
toast.error('Your password is not matched!')
return
}
const auth = getAuth()
createUserWithEmailAndPassword(auth, data.email, data.password)
.then((userCredential) => {
// const auth = userCredential.user
const auth = getAuth().currentUser
if (!auth) return
const { email, uid } = auth
if (!email) return
const user = userRepository.findOrCreate(email, uid)
setCurrentUser(user)
})
.catch((err) => {
toast.error(err.message)
});
},
(err: any) => {
toast.error(err.message)
},
)
Problem: I want to create an admin page where a user can create new posts, this site shall also show the already published posts of that user. To get the already published posts I am running a firestore query - when I log the data of the query I get the correct posts, however they are logged twice. Now I need help finding out what causes my function to execute twice?
Admin Page
const getUserPostsWithID = async (userID) =>{
const q = query(collection(db,`users/${userID}/posts`));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});
}
//TODO fix double execution of getUserPostsWithID
export default function AdminPage() {
const {userID} = useContext(authContext);
const posts = getUserPostsWithID(userID);
return (
<Authcheck>
<h1>The Admin Page</h1>
<PostList></PostList>
</Authcheck>
);
}
function PostList() {
return <PostFeed></PostFeed>;
}
Context Component
export const authContext = createContext({
user: "null",
username: "null",
userID: "null",
});
export default function AuthenticationContext(props) {
const [googleUser, setGoogleUser] = useState(null);
const [username, setUsername] = useState(null);
const [userID, setUserID] = useState(null);
useEffect(() => {
const getUsername = async (uid) => {
const docRef = doc(db, `users/${uid}`);
const docSnap = await getDoc(docRef);
console.log("Database Read Executed");
if (docSnap.exists()) {
setUsername(docSnap.data().username);
} else {
setUsername(null);
}
};
onAuthStateChanged(auth, (user) => {
if (user) {
setGoogleUser(user.displayName);
setUserID(user.uid);
getUsername(user.uid);
} else {
setGoogleUser(null);
}
});
}, []);
return (
<authContext.Provider value={{ user: googleUser, username: username , userID: userID}}>
{props.children}
</authContext.Provider>
);
}
Edit: Screenshot of Console
Thank you for your help, it is very appreciated! :)
useEffect(() => {
const getUserPostsWithID = async (userID) => {
const q = query(collection(db, `users/${userID}/posts`));
const querySnapshot = await getDocs(q);
console.log("Database Read Executed");
const postArr = [];
querySnapshot.forEach((doc) => {
postArr.push(postToJSON(doc));
});
setPostState(postArr);
};
getUserPostsWithID(userID);
}, []);
This code now prevents double execution even though I am not quite sure why.
const [user, setUser] = useState({});
const [pass, setPass] = useState('')
const [name, setName] = useState('')
const [isLoading, setIsLoading] = useState(true)
const auth = getAuth();
const inputHandler = e => {
setUser(e?.target.value)
}
const passHandler = e => {
setPass(e?.target.value)
}
const nameHandler = e => {
setName(e?.target.value)
}
const toggleLogin = event => {
setIsLogIn(!event.target.checked);
}
const signUpHandler = (e) => {
signUp(user, pass)
.then(result => {
setUserName();
history.push(url)
// console.log(url)
})
.finally(() => {
setIsLoading(false)
})
.catch((error) => {
setError(error.message)
// ..
});
e.preventDefault()
}
const signUp = (user, pass) => {
setIsLoading(true)
return createUserWithEmailAndPassword(auth, user, pass)
}
useEffect(() => {
onAuthStateChanged(auth, (user) => {
if (user) {
setUser(user)
// console.log("auth changed",user.email)
} else {
setUser({})
}
setIsLoading(false)
});
}, [auth])
const setUserName = () => {
updateProfile(auth.currentUser, {
displayName: name
});
Before displayName property being updated its redirecting to the route it came from. Is this happening for asynchronous nature?
I'm trying to set the displayName property on the navbar.displayName is getting set but not showing on ui, but showing after when I refresh the page. How can I fix this issue?
Based on https://dev.to/bmcmahen/using-firebase-with-react-hooks-21ap I have a authentication hook to get user state and firestore hook to get user data.
export const useAuth = () => {
const [state, setState] = React.useState(() => { const user = firebase.auth().currentUser return { initializing: !user, user, } })
function onChange(user) {
setState({ initializing: false, user })
}
React.useEffect(() => {
// listen for auth state changes
const unsubscribe = firebase.auth().onAuthStateChanged(onChange)
// unsubscribe to the listener when unmounting
return () => unsubscribe()
}, [])
return state
}
function useIngredients(id) {
const [error, setError] = React.useState(false)
const [loading, setLoading] = React.useState(true)
const [ingredients, setIngredients] = React.useState([])
useEffect(
() => {
const unsubscribe = firebase
.firestore()
.collection('recipes')
.doc(id)
.collection('ingredients') .onSnapshot( snapshot => { const ingredients = [] snapshot.forEach(doc => { ingredients.push(doc) }) setLoading(false) setIngredients(ingredients) }, err => { setError(err) } )
return () => unsubscribe()
},
[id]
)
return {
error,
loading,
ingredients,
}
}
Now in my app I can use this to get user state and data
function App() {
const { initializing, user } = useAuth()
const [error,loading,ingredients,] = useIngredients(user.uid);
if (initializing) {
return <div>Loading</div>
}
return (
<userContext.Provider value={{ user }}> <UserProfile /> </userContext.Provider> )
}
Since UID is null before auth state change trigger, firebase hook is getting called with empty key.
How to fetch data in this scenario once we understand that user is logged in.
May be you can add your document read inside auth hook.
export const useAuth = () => {
const [userContext, setUserContext] = useState<UserContext>(() => {
const context: UserContext = {
isAuthenticated: false,
isInitialized: false,
user: auth.currentUser,
userDetails: undefined
};
return context;
})
function onChange (user: firebase.User | null) {
if (user) {
db.collection('CollectionName').doc(user.uid)
.get()
.then(function (doc) {
//set it to context
})
});
}
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(onChange)
return () => unsubscribe()
}, [])
return userContextState
}
You can use some loading spinner in your provider to wait for things to complete.