handling useEffect and server side changes - reactjs

I want to confirm that a new user has a unique nickname or not. And if it is not unique i want to show a message to user. For that reason i wrote that code:
const [vNicknameServer, setvNicknameServer] = useState(true);
...
const validateNicknameServer = async (nickname) => {
let flag = 0;
await firebase
.firestore()
.collection("Users")
.where("nickname", "==", nickname)
.get()
.then((querySnapshot) => {
querySnapshot.forEach((documentSnapshot) => {
if (documentSnapshot.data().nickname == nickname) {
setvNicknameServer(false); //Changing state
flag = 1;
}
});
if (flag == 0) {
setvNicknameServer(true); // changing state
}
})
.catch((err) => console.log(err));
};
....
{vNickname ? (
<Text style={{ color: "#DC8989" }}>
Nickname min 4 max 14 caracters
</Text>
) : vNicknameServer == false ? ( // according to state it appears or not
<Text style={{ color: "#DC8989" }}>Nickname must be unique</Text>
) : null}
But the render process always done after some other process. I should use useEffect but i am a little bit confused how to use it in there? Anyone can help me to use it?
Note:
This validation made it in a submit handler :
const Submit = () => {
validateEmail(email) ? setvEmail(false) : setvEmail(true);
validateNickname(nickname) ? setvNickname(false) : setvNickname(true);
validatePassword(password) ? setvPassword(false) : setvPassword(true);
validateNicknameServer(nickname);
console.log(vEmailServer);
console.log(email);
if (
validateEmail(email) &&
validateNickname(nickname) &&
validatePassword(password) &&
vNicknameServer &&
vEmailServer
) {
navigation.navigate("UserCredential", {
email: email,
password: password,
nickname: nickname,
});
}
};
I want to control it just when i clicked the button not in every case.
NOTE: Please check the below answer and comments.

If I understand your question correctly, you are trying to use an async in a useEffect hook. To do that, you can use the following pattern
// probably need to setup a useState hook to contain nickname data
const [nickname, setNickname] = useState(true);
useEffect(() => {
// get nickname from the component state instead of passing the data as a param
const validateNicknameServer = async () => {
let flag = 0;
await firebase
.firestore()
.collection("Users")
.where("nickname", "==", nickname)
.get()
.then((querySnapshot) => {
querySnapshot.forEach((documentSnapshot) => {
if (documentSnapshot.data().nickname == nickname) {
setvNicknameServer(false); //Changing state
flag = 1;
}
});
if (flag == 0) {
setvNicknameServer(true); // changing state
}
})
.catch((err) => console.log(err));
};
validateNicknameServer();
}, [nickname])
Update based on the note
I can't verify if it runs without any error. But hopefully, it provides some ideas for you to solve the issue.
const validateNicknameServer = async (nickname) => {
let flag = 0;
const querySnapshot = await firebase
.firestore()
.collection("Users")
.where("nickname", "==", nickname)
.get()
.catch((err) => console.log(err));
querySnapshot.forEach((documentSnapshot) => {
if (documentSnapshot.data().nickname == nickname) {
setvNicknameServer(false); //Changing state
flag = 1;
return false
}
});
if (flag == 0) {
setvNicknameServer(true); // changing state
return true
}
};
const Submit = async () => {
// .... other code
// validate the name before navigation happens
// and use vNicknameServer to displaying messages
const isValidName = await validateNicknameServer(nickname);
// .... other code
if (
validateEmail(email) &&
validateNickname(nickname) &&
validatePassword(password) &&
isValidName &&
vEmailServer
) {
navigation.navigate("UserCredential", {
email: email,
password: password,
nickname: nickname,
});
}
};

I think you don't need to use useEffect, and use for in instead forEach for easy control the flow.
const validateNicknameServer = async (nickname) => {
await firebase
.firestore()
.collection("Users")
.where("nickname", "==", nickname)
.get()
.then((querySnapshot) => {
for (var documentSnapshot in querySnapshot) {
if (documentSnapshot.data().nickname == nickname) {
return false;
}
}
return true;
})
.catch((err) => console.log(err));
};
const Submit = async () => {
// Other validation ...
// Waiting validate nickname on server. Should display loading state for better UX.
const validateNicknameServer = await validateNicknameServer(nickname);
// Change state after validate
setvNicknameServer(false);
// Form valid -> Navigate to UserCredential screen
if (
validateEmail(email) &&
validateNickname(nickname) &&
validatePassword(password) &&
vEmailServer
validateNicknameServer &&
) {
navigation.navigate("UserCredential", {
email: email,
password: password,
nickname: nickname,
});
}
};

On click of submit you're doing async operation but not waiting for it that's why
render process always done after some other process
So submit method's if's code will execute before validateNicknameServer's async operation is completed.
I think You don't need to use useEffect here.
also after setvNicknameServer() whole component is gonna re-render so Submit method still gonna have vNicknameServer's previous value even if you've updated it.
Here's what you can do
For method validateNicknameServer when using await you should use try/catch instead of then/catch
const validateNicknameServer = async (nickname) => {
let flag = 0;
try {
const querySnapshot = await firebase
.firestore()
.collection('Users')
.where('nickname', '==', nickname)
.get();
querySnapshot.forEach((documentSnapshot) => {
if (documentSnapshot.data().nickname == nickname) {
flag = 1;
}
});
return flag == 0 ? true : false;
} catch (err) {
console.log(err);
}
return false;
};
now in Submit method wait for validateNicknameServer to complete and same for other methods (validateEmail, validateNickname, validatePassword) too if they're also asynchronous.
const Submit = async () => {
validateEmail(email) ? setvEmail(false) : setvEmail(true);
validateNickname(nickname) ? setvNickname(false) : setvNickname(true);
validatePassword(password) ? setvPassword(false) : setvPassword(true);
// wait for this method to complete, same for above methods too if they are async
const isNickNameUnique = await validateNicknameServer(nickname);
setvNicknameServer(isNickNameUnique);
console.log(vEmailServer);
console.log(email);
if (
validateEmail(email) &&
validateNickname(nickname) &&
validatePassword(password) &&
isNickNameUnique &&
vEmailServer
) {
navigation.navigate('UserCredential', {
email: email,
password: password,
nickname: nickname,
});
}
};
here these type of operations are expected to be async so it's best practice to show LoadingIndicator to the user while waiting.

Related

What is the best way about send multiple query in useMutation (React-Query)

I developed login function use by react-query in my react app
The logic is as follows
First restAPI for checking duplication Email
If response data is true, send Second restAPI for sign up.
In this case, I try this way
// to declare useMutation object
let loginQuery = useMutation(checkDuple,{
// after check duplication i'm going to sign up
onSuccess : (res) => {
if(res === false && warning.current !== null){
warning.current.innerText = "Sorry, This mail is duplicatied"
}else{
let res = await signUp()
}
}
})
//---------------------------------------------------------------------------------
const checkDuple = async() => {
let duple = await axios.post("http://localhost:8080/join/duple",{
id : id,
})
}
const signUp = async() => {
let res = await axios.post("http://localhost:8080/join/signUp",{
id : id,
pass : pass
})
console.log(res.data)
localStorage.setItem("token", res.data)
navigate("/todo")
}
I think, this isn't the best way, If you know of a better way than this, please advise.
Better to have another async function that does both things.
something like
const checkDupleAndSignUp = async () => {
await checkDuple();
await signUp();
};
And then use that in your useMutation instead.
Other things to consider:
Maybe move the logic to set local storage and navigate to another page in the onSuccess instead.
You can also throw your own error if one or the other request fails and then check which error happened using onError lifecycle of useMutation, and maybe display a message for the user depending on which request failed.
You can handle both of them in a single function and in mutation just add token in localStorage and navigate
like this:
const checkDupleAndSignUp = async () => {
return checkDuple().then(async res => {
if (res === false) {
return {
isSuccess: false,
message: 'Sorry, This mail is duplicatied',
};
}
const { data } = await signUp();
return {
isSuccess: true,
data,
};
});
};
and in mutation :
let loginQuery = useMutation(checkDupleAndSignUp, {
onSuccess: res => {
if (res.isSuccess) {
console.log(res.data);
localStorage.setItem('token', res.data);
navigate('/todo');
} else if (warning.current !== null) {
warning.current.innerText = res.message;
}
},
});

how can i wait for firebase to check the user is valid before sending a POST request with reactjs?

I am using the following code to obtain the users idToken before sending it to the backend as an authorisation header:
const user = firebase.auth().currentUser
const idToken = await user.getIdToken()
sent like this:
var res = await axios.post(backUrl + "account/load_balance", {
uid: uid,
id: id
},
{
headers: {
Authorization: 'Bearer ' + idToken
}});
It works well but on one of my pages the request is sent to the server before idtoken variable has filled and the user is still null.
I have read that i need to implement onAuthStateChanged as it waits for the token before triggering: https://firebase.google.com/docs/auth/web/manage-users#web-version-8
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
var uid = user.uid;
// ...
} else {
// User is signed out
// ...
}
});
However i am unsure how to implement this in to my code.
Can anyone advise?
Full code:
const RoutingForPortfolio = (props) => {
let uid = localStorage.getItem("account-info");
let { id } = useParams();
const loadBlockchainData = async (dispatch) => {
if (id === null || id === undefined) {
id = "test";
}
const user = firebase.auth().currentUser
const idToken = await user.getIdToken()
console.log(idToken)
var res = await axios.post(backUrl + "account/load_balance", {
uid: uid,
id: id
},
{
headers: {
Authorization: 'Bearer ' + idToken
}});
if (res.data === null) {
await wait(2);
document.location.href = "/logout"
return;
}
else {
// const web3 = new Web3(new Web3.providers.HttpProvider('https://data.stocksfc.com:3200'));
// dispatch(web3Loaded(web3));
const account = res.data.address;
dispatch(web3AccountLoaded(account));
localStorage.setItem("account-address", account);
if (res.data.token_flag && res.data.exchange_flag) {
await dispatch(setLoginUserName(res.data.name));
await dispatch(setLoginUserEmail(res.data.email));
if (res.data.balance !== null) {
await dispatch(etherBalanceLoaded(res.data.balance[0]));
await dispatch(tokenBalanceLoaded(res.data.balance[1]));
await dispatch(exchangeEtherBalanceLoaded(res.data.balance[2]));
await dispatch(exchangeTokenBalanceLoaded(res.data.balance[3]));
}
}
else {
Swal.fire({
icon: "error",
title: "Error...",
text: "Error 485 - Please report to admin",
});
return;
}
}
};
useEffect(() => {
if (uid) {
loadBlockchainData(props.dispatch);
}
}, [props.dispatch, uid]);
return (
<>
{uid ? (
<div>
<Portfolio id={id} />
</div>
) : (
<Login />
)}
</>
);
};
As you correctly identified, firebase.auth().currentUser is a synchronous action that only gets the user object when it is called. You've also correctly surmised that you instead need to use firebase.auth().onAuthStateChanged() to wait to check if the user is logged in.
This can be achieved by wrapping an onAuthStateChanged listener into a Promise where it is immediately detached after being called once.
function getValidatedUser() {
return new Promise((resolve, reject) => {
const unsubscribe = firebase.auth()
.onAuthStateChanged(
(user) => {
unsubscribe();
resolve(user);
},
reject // pass up any errors attaching the listener
);
});
}
This now allows you to use:
const user = await getValidatedUser();
if (!user) {
// todo: handle no user signed in, such as:
throw new Error("User not signed in!");
}
// if here, user is User object
const idToken = await user.getIdToken()

How to set a custom username in Firebase?

The first time a user logins with Google Auth provider a "username" field with an empty value is set in Users collection user.uid document. Now I want to first check if the username length is greater than 3 (which will be the minimum for a username). If greater than 3 usernames are already set, else a modal should open for the user to set a username.
The code below does not work and not sure if it's the correct approach I was trying. The code runs once the user logs in.
const [user] = useAuthState(auth);
const CheckUsername = async () => {
const docRef = doc(db, "UsersData", user.uid);
const docSnap = await getDoc(docRef);
if (!docSnap.exists() && docSnap.data().username.length > 3) {
//<Show SetUserName Modal - Recoil>
} else if (docSnap.exists() && docSnap.data().username.length > 3) {
//<Don't show SetUserName Modal>
}
};
useEffect(() => {
if (user) {
CheckUsername();
}
}, [user]);
SetUsername Modal:
const [user] = useAuthState(auth);
const [usernameValue, setUsernameValue] = useState("");
const SetUsername = async () => {
try {
const UserRef = collection(db, "UsersData")
const UsernameQuery = query(UserRef, where("username", "==", usernameValue))
const Username = await getDoc(UsernameQuery)
if(!Username.exists()) {
await updateDoc(doc(db, "UsersData", user.uid), {
username: usernameValue,
});
} else {
console.log("Username already exists, please try another one");
}
} catch (error) {
console.log("error in try catch")
}
}
return (
<div>
<input type="text" onChange={(e) => setUsernameValue(e.target.value)} />
<button onClick={SetUsername}>Set username</button>
</div>
);
Solution I came up with:
This is in layout:
const [user] = useAuthState(auth);
const [open, setOpen] = useRecoilState(setUsernameModal);
const [update, setUpdate] = useState(true);
const CheckUser = async () => {
try {
//Where Users are stored
const userDocRef = doc(db, "UsersData1", user.uid);
//Using Transaction for if something goes wrong mid process no action taken at all
await runTransaction(db, async (transaction) => {
const userDoc = await transaction.get(userDocRef);
//Read ELSE first
//If userDoc exists means they logged in before AND/OR never finished the registration process
if (userDoc.exists()) {
const User = await getDoc(userDocRef);
//if usernameSet = false means they never set the username before
if (User.data().usernameSet === false) {
//Opens a modal to set username - (for my case it's the last process for registration)
setOpen(true);
}
} else {
//If User doesn't exist in "UsersData" means it's the first time they are logging in
await setDoc(doc(db, "UsersData1", user.uid), {
//usernameSet to check if username is set or not.
usernameSet: false,
username: "",
//usernameValue for search if username is taken and should be in uppercase OR lowercase since Fx: John & john are not the same
usernameValue: "",
//Add's default Firebase info
user: JSON.parse(JSON.stringify(user)),
});
//Updates useEffect so the user falls in userDoc.exists
setUpdate(!update);
}
});
} catch (error) {}
};
useEffect(() => {
if (user) {
CheckUser();
}
}, [user, update]);
Then a modal to update: username: "" and usernameSet: to true and then use usernameValue to check if user already exists

How can I make my function wait until the user data is updated in my useContext provider?

new to react here, I want a new user to enter their details on their first sign in. This includes enterting a username, name, profile picture etc.
When they have submitted their details, I wait for confirmation from firebase and then I want to forward them to their profile (the link structure is domain/p/:username).
However, every time I try it, it ends up trying to head to domain/p/undefined?
When I use react dev tools to inspect, I can see that the username was successfully sent up to my state provider, so I think it's just a matter of timing thats the problem.
Heres the welcome page functions:
//The first method begins the update and checks if the username already exists.
const update = async (e) => {
if (
firstName.trim() === "" ||
lastName.trim() === "" ||
username.trim() === "" ||
bio.trim() === "" ||
addressOne.trim() === "" ||
city.trim() === "" ||
county.trim() === "" ||
postCode.trim() === "" ||
photos.length === 0
) {
window.alert("Invalid data!\nOnly Address line 2 can be empty");
} else {
var usernameRef = db
.collection("users")
.where("username", "==", username);
usernameRef.get().then((docs) => {
if (docs.size === 1) {
docs.forEach((doc) => {
if (doc.id === currentUser.uid) {
sendUpdate();
} else {
window.alert("Username taken");
}
});
} else {
sendUpdate();
}
});
}
};
//This method puts the initial data into firebase except the profile picture
function sendUpdate() {
setLoading("loading");
db.collection("users")
.doc(currentUser.uid)
.set(
{
username: username,
name: firstName,
surname: lastName,
bio: bio,
address1: addressOne,
address2: addressTwo,
notifications: [],
city: city,
county: county,
postcode: postCode,
newUser: false,
},
{ merge: true }
)
.then(() => {
updatePhoto();
})
.catch((err) => console.log(err));
}
//This method uploads the profile picture, then gets the downloadURL of the photo just uploaded and puts it into the user document created in method 2.
//It also trys to send the user to their profile afterwards, but it always ends up as undefined.
const updatePhoto = async () => {
const promises = [];
var userREF = db.collection("users").doc(currentUser.uid);
photos.forEach((photo) => {
const uploadTask = firebase
.storage()
.ref()
.child(
`users/` + currentUser.uid + `/profilePicture/profilePicture.jpg`
)
.put(photo);
promises.push(uploadTask);
uploadTask.on(
firebase.storage.TaskEvent.STATE_CHANGED,
(snapshot) => {
const progress =
(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
if (snapshot.state === firebase.storage.TaskState.RUNNING) {
console.log(`Progress: ${progress}%`);
}
},
(error) => console.log(error.code),
async () => {
const downloadURL = await uploadTask.snapshot.ref.getDownloadURL();
userREF
.update({
profilePicture: downloadURL,
})
.then(async () => {
updateUserData().then(() => {
setLoading("complete");
setTimeout(() => {
history.push("/p/" + userData.username);
}, 3000);
});
});
}
);
return "completed";
});
};
Here is my AuthContext provider: (the function UpdateUserData() is what updates the data after its been put into firebase)
import React, { useContext, useState, useEffect } from "react";
import { auth, db } from "../firebase";
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [userData, setUserData] = useState();
const [loading, setLoading] = useState(true);
function signup(email, password) {
return auth.createUserWithEmailAndPassword(email, password);
}
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password);
}
async function updateUserData() {
if (currentUser) {
var userData = db.collection("users").doc(currentUser.uid);
await userData
.get()
.then((doc) => {
if (doc.exists) {
setUserData(doc.data());
return "success";
}
})
.catch((error) => {
console.log("Error getting document:", error);
return "error";
});
}
}
function logout() {
setUserData();
return auth.signOut();
}
function resetPassword(email) {
return auth.sendPasswordResetEmail(email);
}
function updateEmail(email) {
return currentUser.updateEmail(email);
}
function updatePassword(password) {
return currentUser.updatePassword(password);
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
if (user) {
var userData = db.collection("users").doc(auth.currentUser.uid);
userData
.get()
.then((doc) => {
if (doc.exists) {
setUserData(doc.data());
}
})
.catch((error) => {
console.log("Error getting document:", error);
});
}
});
return unsubscribe;
}, []);
const value = {
currentUser,
userData,
updateUserData,
login,
signup,
logout,
resetPassword,
updateEmail,
updatePassword,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
And as you can see, once the undefined page has been attempted to load, we can see the username did in fact end up in userData from my context provider:
TIA!
You can resolve this issue by move the redirect link out side of you updatePhoto and put it in useEffect (or any other option base on code flow) then just set an state or check the needed data like userdata.userName is already exists, if its undefined prevent redirect and you can display loader component for example, else execute redirect...
Basic Example:
useEffect(() => {
if(userData.username){
history.push("/p/" + userData.username);
}
}, [userData.username])
const myUpdateFunction = useCallBack(() => {
fetch().then(v => {
setUserData(v);
})
}, [])

How to post with Axios in React?

This my first time in React and Axios. I have a login form and sign up form and don't have any database. I want any mock API to simulate a login and sign up which provides a token in the response, in order to save it in a local storage in order to keep the user logged in. Also how do I prevent the user to go the home page (login/logout screen). When they type for example www.blabla.com, I want, if the token exists they still in the app, otherwise the token will be erased.
I tried to fetch data from mock API by axios.get(), it worked but it still static
componentDidMount() { // this For Testing Until Now
axios.get('https://jsonplaceholder.typicode.com/users')
.then(res => {
console.log(res);
this.setState({
users: res.data
}, () => {
console.log('state', this.state.users)
})
});
}
I want to communicate with API that allows my to fetch data and post data to it. This is my login function
handleLogin(e) {
e.preventDefault();
const email = e.target.elements.email.value;
const password = e.target.elements.password.value;
let userData = {};
if(validator.isEmpty(email) || validator.isEmpty(password) || !validator.isEmail(email)) {
this.setState({
error: 'You Have To Complete Your Data Correctly'
}, () => {
console.log('failed');
});
} else {
userData = {email, password};
const { users } = this.state;
if(users.find(item => item.email === userData.email)) {
const index = users.findIndex(item => item.email === userData.email);
this.props.history.push(`./create/${users[index].username}`);
}
}
}
and this is my signup function
handleAddNewUser(e) {
e.preventDefault();
const name = e.target.elements.userName.value.toLowerCase().trim();
const email = e.target.elements.userEmail.value.toLowerCase().trim();
const password = e.target.elements.pass.value;
const repassword = e.target.elements.re_pass.value;
let userInfo = {};
const { users } = this.state;
console.log(name, email);
if (validator.isEmpty(name) || validator.isEmpty(email) ||
validator.isEmpty(password) || validator.isEmpty(repassword) ||
!validator.isEmail(email) || !validator.equals(password, repassword)) {
this.setState({
error: 'You Have to enter valid data, Make Sure That The Fields are Complete',
open: true
});
} else {
userInfo = { name, email, password };
if (
users.find(item => item.name === userInfo.name) ||
users.find(item => item.email === userInfo.email)
) {
this.setState({
error: 'This username or email is used',
open: true
});
} else {
this.setState({
users: this.state.users.concat(userInfo),
success: true
}, () => {
// this.props.history.push(`./create/${userInfo.name}`);
// console.log(users)
});
console.log(users)
}
}
}
You can use axios.post() to send post request.
// POST
const userData = {
email: 'demouser#gmail.com',
username: 'demouser',
password: '1a2b3c4d5e' //This should be encoded
}
axios.post('https://example.com/createUser', userData)
.then(res => {
responseData = res.data
if (responseData.status == 'success') {
const user = responseData.user
...
} else {
alert('Something went wrong while creating account')
}
})

Resources