Value from state not there | React - reactjs

My Problem
I am using React and Apollo in my frontend application. When I login as a regular user I want to be navigated to a certain path, but before that I need to set my providers state with my selectedCompany value. However, sometimes I get my value from my provider in my components CDM and sometimes I don't, so I have to refresh in order to get the value.
I've tried to solve this, but with no luck. So I am turning to the peeps at SO.
My setup (code)
In my login component I have my login mutation, which looks like this:
login = async (username, password) => {
const { client, qoplaStore, history } = this.props;
try {
const result = await client.mutate({
mutation: LOGIN,
variables: {
credentials: {
username,
password,
}}
});
const authenticated = result.data.login;
const { token, roles } = authenticated;
sessionStorage.setItem('jwtToken', token);
sessionStorage.setItem('lastContactWithBackend', moment().unix());
qoplaStore.setSelectedUser(authenticated);
qoplaStore.setUserSessionTTL(result.data.ttlTimeoutMs);
if (roles.includes(ROLE_SUPER_ADMIN)) {
history.push("/admin/companies");
} else {
// This is where I'm at
this.getAndSetSelectedCompany();
history.push("/admin/shops");
}
} catch (error) {
this.setState({ errorMessage: loginErrorMessage(error) });
}
};
So if my login is successful I check the users roles, and in this case, I will get into the else statement. The function getAndSetSelectedCompany look like this:
getAndSetSelectedCompany = () => {
const { client, selectedValues, qoplaStore } = this.props;
client.query({ query: GET_COMPANIES }).then(company => {
selectedValues.setSelectedCompany(company.data.getCompanies[0]);
});
};
So I am fetching my companies try to set one of them in my providers state with the function setSelectedCompany. selectedValues is what im passing down from my consumer to all my routes in my router file:
<QoplaConsumer>
{(selectedValues) => {
return (
...
)
}}
</QoplaConsumer
And in my provider I have my setSelectedCompany function which looks like this:
setSelectedCompany = company => {
this.persistToStorage(persistContants.SELECTED_COMPANY, company);
this.setState({
selectedCompany: company
})
};
And my selectedValues are coming from my providers state.
And in the component that has the route I'm sending the user to I have this in it's CDM:
async componentDidMount() {
const { client, selectedValues: { selectedCompany, authenticatedUser } } = this.props;
console.log('authenticatedUser', authenticatedUser)
if (selectedCompany.id === null) {
console.log('NULL')
}
Sometimes I get into the if statement, and sometimes I don't. But I rather always come into that if statement. And that is my current problem
All the help I can get is greatly appreciated and if more info is needed. Just let me know.
Thanks for reading.

Your getAndSetSelectedCompany is asynchronous and it also calls another method that does setState which is also async.
One way to do this would be to pass a callback to the getAndSetSelectedCompany that would be passed down and executed when the state is actually set.
changes to your login component
if (roles.includes(ROLE_SUPER_ADMIN)) {
history.push("/admin/companies");
} else {
// This is where I'm at
this.getAndSetSelectedCompany(()=>history.push("/admin/shops"));
}
changes to the two methods that are called
getAndSetSelectedCompany = (callback) => {
const { client, selectedValues, qoplaStore } = this.props;
client.query({ query: GET_COMPANIES }).then(company => {
selectedValues.setSelectedCompany(company.data.getCompanies[0], callback);
});
};
setSelectedCompany = (company, callback) => {
this.persistToStorage(persistContants.SELECTED_COMPANY, company);
this.setState({
selectedCompany: company
}, callback)
};

Related

LocalStorage updating inconsistently with setInterval

After calling the refresh token endpoint to refresh the user's auth tokens, the local storage does not update the token field consistently. Sometimes, the local storage is updated properly and the app works well, other times the token and admin/student fields are deleted from local storage despite no error being logged and the endpoint returning a success response. How do I fix this? Code below
import { parseTokens, parseAdmin, parseUser } from "../utils/auth-parser";
import { adminAuthFetch } from "../config/axios/axios-admin.config";
import { studentAuthFetch } from "../config/axios/axios-user.config";
export const refresher = async () => {
const admin = parseAdmin();
const student = parseUser();
const token = parseTokens();
if (!admin && !student) {
return;
}
if (admin && !student) {
console.log(
"==========================refreshing token==================",
new Date().getMilliseconds()
);
try {
const response = await adminAuthFetch.post(`/auth/refresh-tokens`, {
refresh_token: token.refresh,
});
const data = response?.data;
console.log(data);
localStorage.setItem(
"tokens",
JSON.stringify({
access: data?.data?.auth_token,
refresh: data?.data?.refresh_token,
})
);
} catch (error) {
console.log(error);
localStorage.removeItem("tokens");
localStorage.removeItem("admin");
}
} else if (student && !admin) {
console.log(
"==========================refreshing student token==================",
new Date().getMilliseconds()
);
try {
const response = await studentAuthFetch.post(`/auth/refresh-tokens`, {
refresh_token: token.refresh,
});
const data = response?.data;
console.log(data);
localStorage.setItem(
"tokens",
JSON.stringify({
access: data?.data?.auth_token,
refresh: data?.data?.refresh_token,
})
);
} catch (error) {
console.log(error)
localStorage.removeItem("tokens");
localStorage.removeItem("student");
}
}
};
Here's the Effect that is called from the root app
const refreshFunction = () => {
if (!refreshRef.current) {
refreshRef.current = true;
refresher();
} else {
refreshRef.current = false;
}
};
useEffect(() => {
const timer = setInterval(refreshFunction, 1000 * 60 * 2);
return () => clearInterval(timer);
}, []);
Despite receiving a success response from the endpoint and ensuring refresher function is called only once with the useref check, the token field in the local storage doesn't update consistently. Sometimes the values are updated, sometimes they are deleted without an error being logged to the console. Tried removing strict mode but it still does not work
Without being certain about how everything in your code works, it's possible that despite your best intentions, the refresher function is rendering twice.
Could you share more context around the React version you're using? If you're using version 17, try doing something like this:
let log = console.log
at the top level of your code, and use it for logging instead. My working theory is that some form of console.log suppression is happening on a second render, which is why you're not getting the logs, even though the localStorage removeItem call is still executing.
Let me know the React version, and we can continue debugging.

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;
}
},
});

social login - how to use variables outside of function in react?

I try to use social login.
if I success to login in kakao. they give me access_token and I use it to mutation to my server
below is my code
import { useMutation } from "react-apollo-hooks";
import { KAKAO_LOGIN } from "./AuthQuery";
export default () => {
const kakaoLoginMutation = useMutation(KAKAO_LOGIN, {
variables: { provder: "kakao", accessToken: authObj.access_token },
});
const kakaoLogin = (e) => {
e.preventDefault();
window.Kakao.Auth.login({
success: function (authObj) {
console.log(authObj.access_token);
},
});
};
if (authObj.access_token !== "") {
kakaoLoginMutation();
}
return (
<a href="#" onClick={kakaoLogin}>
<h1>카카오로그인</h1>
</a>
);
};
if I success to login using by function kakaoLogin, it give authObj.
console.log(authObj.access_token) show me access_token
and I want to use it to useMutation. but it show to me authObj is not defined.
Looks like you're looking for a local state to hold authObj
const [authObj, setAuthObj] = useState({});
const kakaoLogin = (e) => {
e.preventDefault();
window.Kakao.Auth.login({
success: function(authObj) {
setAuthObj(authObj);
},
});
};
const kakaoLoginMutation = useMutation(KAKAO_LOGIN, {
variables: {
provder: "kakao",
accessToken: authObj.access_token
},
});
if (authObj.access_token !== "") {
kakaoLoginMutation();
}
After reading Apollo auth docs (you read it, right?) you should know tokens should be sent using headers.
... if auth link is used and reads the token from localStorage ... then any login function (mutation, request) result should end storing token in localStorage (to be passed by header in next queries/mutations) ... it should be obvious.
In this case
... we have a bit different situation - kakao token is passed into login mutation as variable ...
We can simply, directly (not using state, effect, rerenderings) pass the kakao token to 'mutation caller' (kakaoLoginMutation):
// mutation definition
const [kakaoLoginMutation] = useMutation(KAKAO_LOGIN);
// event handler
const kakaoLogin = (e) => {
e.preventDefault();
window.Kakao.Auth.login({
success: function(authObj) {
// run mutation
kakaoLoginMutation({
variables: {
provder: "kakao",
accessToken: authObj.access_token
}
})
},
});
};
When required, login mutation (KAKAO_LOGIN) result can be processed within onCompleted handler:
const [kakaoLoginMutation] = useMutation(KAKAO_LOGIN,
onCompleted = (data) => {
// save mutation result in localStorage
// set some state to force rerendering
// or redirection
}
);

React Linking.addEventListener runs a Firebase sign in link twice in a useEffect

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.

Connect Firebase Auth with Firestore in React

I'm trying to figure out how to connect users with data in my firestore. I didn't find anything about how to do it in React.
My idea, in the register page of my app, is:
async function register() {
try {
await auth.createUserWithEmailAndPassword(email, password).then(data => {
auth.currentUser.updateProfile({
displayName: name
})
db.collection('usersdata').doc(data.user.uid).set({nome: name})
props.history.replace('/success')
})
} catch(error) {
alert(error.message)
}
}
In this way I'm creating a new user and also a new entry in my "usersdata" collection. That entry has the same ID and the same name of the user.
After the login I can get the active user name using:
const userName = getCurrentUsername()
function getCurrentUsername() {
return auth.currentUser && auth.currentUser.displayName
}
All of this is working. But here it comes my problem:
I would like to add new data to the current user, for example "age" and "nationality".
But I don't know how to exactly get access to that user from firestore instead of auth.
And I also need to return only his data. Then, after a research, I need to return data from all users, and I guess that is the method:
const [datidb, setDatidb] = useState([])
useEffect(() => {
const fetchData = async () => {
const data = await db.collection('usersdata').get()
setDatidb(data.docs.map(doc => doc.data()))
}
fetchData()
}, [])

Resources