React state does not update on time, asynchronous issue - reactjs

I've build a web application using React that has user registration.
The user data:
{
username: 'string',
password: 'string'
}
exists in a postgresql database and as state.
I'm trying to handle a case where a user is trying to register with a previously used username.
Here's a screenshot of my ui component and react dev tools :
Here's a summary of how I'm handling the case where a username (i'll use the username 'bob' as seen of the screenshot) has already been used:
// Login.js
The 'Register' <input> in the <Login/> component (which contains all the .jsx for the ui form seen on the screen) invokes the userRegister() function via an onClick event. It receives this function as a prop from my <App/> (App.js) component, then the following happens...
// App.js
1 - userRegister() is invoked
2- it invokes 2 functions that check whether the password and username are valid
3- if they are, it invokes a function to check if the username has already been used by checking if it exists as a record in the postgresql database
4- it does this by sending an axios request with the current username + password object as params to the '/users' endpoint, written out in my server.js file
axios({
method: 'get',
url: '/users',
params: { username: 'string', password: 'string'
})
// server.js
1- the request is received by an express route handler at the '/users' endpoint
2- inside the route handler, it invokes an imported getUser() function from postgres.js that communicates directly with the database
const { getData, getUser, addUser } = require('./db/postgres.js');
app.get('/users', (req, res) => {
getUser(req.query)
});
// postgres.js
1- the getUser() function sends the following query to the postgres database
const getUser = (user) => {
return client.query(`select * from users where username='${user.username}'`)
};
after sending that query
this object should reach the client
response.rows: [
{ id: null, username: 'bob', password: 'Pi2#' },
{ id: null, username: 'bob', password: '8*lA' },
{ id: null, username: 'bob', password: '8*lA' },
{ id: null, username: 'bob', password: '7&lL' },
{ id: null, username: 'bob', password: '*lL0' },
{ id: null, username: 'bob', password: 'mM0)' },
{ id: null, username: 'bob', password: '8*fF' },
{ id: null, username: 'bob', password: '7&yY' },
{ id: null, username: 'bob', password: '55$rR' },
{ id: null, username: 'bob', password: '7&yY' },
{ id: null, username: 'bob', password: '8*LAAa' },
{ id: null, username: 'bob', password: '8*lAa' },
{ id: null, username: 'bob', password: '8*lA' }
]
which proves that the username does, in fact already exist in the database
which should, in turn, update the alreadyRegistered state to true and invoke the alert 'user already exists', but, as demonstrated in the top screenshot, alreadyRegistered doesn't update in time
it DOES show the update in Rect devtools a few ms later
I'm fairly certain that the issue is due to state hooks and lifecycle methods being asynchronous, but with that being said how do I update the alreadyRegisted state to true before the code on line 63 executes?
Here are snippets of the code files working together. Let me know if more information/context is needed
Login.js
import React from 'react';
import { Link } from 'react-router-dom';
import { NavBar, Footer } from'../Imports.js';
const Login = ({ userAuth, getUserAuth, userLogin, authorized, userRegister }) => {
console.log('userAuth:', userAuth);
return (
<div id='login'>
<NavBar userAuth={userAuth} getUserAuth={getUserAuth} authorized={authorized} />
<section id='login-main'>
<form>
<input type='text' placeholder='username' onChange={() => {getUserAuth({...userAuth, username: event.target.value})}}/>
<input type='text' placeholder='password'onChange={() => {getUserAuth({...userAuth, password: event.target.value})}}/>
<input type='submit' value='Login' onClick={() => {event.preventDefault(); userLogin()}}/>
<input type='submit' value='Register' onClick={() => {event.preventDefault(); userRegister();}}/>
</form>
</section>
<Footer/>
</div>
)
};
export default Login;
App.js
import React, { useState, useEffect } from 'react';
import Router from './Router.js';
const axios = require('axios');
const App = () => {
// Login/user state
const [userAuth, getUserAuth] = useState({username: '', password: ''});
const [authorized, authorizeUser] = useState(false);
const [alreadyRegistered, userExists] = useState(false);
// new user register
const userRegister = () => { // <- 1)
if (validUsername() && validPassword()) {
checkIfExists();
console.log('alreadyRegistered:', alreadyRegistered);
if (alreadyRegistered) {
alert('user already exists');
} else {
console.log('ERROR: should update alreadyRegistered state to TRUE and invoke ALERT')
}
}
}
// check if username is already registered
const checkIfExists = () => { // <- 3)
axios({
method: 'get',
url: '/users',
params: userAuth
})
.then((res) => {
console.log(res.data.length)
if (res.data.length) {
console.log(res.data.length, 'INNER')
userExists(true);
}
})
.catch((err) => {
throw err;
})
}
};
server.js
// ----- external modules -----
require('dotenv').config();
const express = require('express');
const path = require('path');
const { getData, getUser, addUser } = require('./db/postgres.js');
// check if user exists
app.get('/users', (req, res) => {
console.log('req.body:', req.query)
getUser(req.query)
.then((response) => {
console.log('response.rows:', response.rows);
res.send(response.rows);
})
.catch((err) => {
if (err) {
throw err;
}
});
});
postgres.js
const { Client } = require('pg');
const getUser = (user) => {
return client.query(`select * from users where username='${user.username}'`)
};

Before answering the state question, it's important to know your application as written is vulnerable to SQL injection. Instead of a template string in your postgres.js file, you should use parameterized queries.
As for the value of alreadyRegistered, you're correct that the issue lies in the asynchronous nature of state hooks.
You can use the useEffect function to handle updates to alreadyRegistered like so:
useEffect(() => {
if (alreadyRegistered) {
alert('user already exists');
} else {
// create user...
}
}, [alreadyRegistered]);
You could also avoid tracking alreadyRegistered in state and just call checkIfExists as needed:
// first, remove alreadyRegistered state
const [userAuth, getUserAuth] = useState({username: '', password: ''});
const [authorized, authorizeUser] = useState(false);
// then redefine checkIfExists to return Promise<boolean>
const checkIfExists = async () => {
const res = await axios({
method: 'get',
url: '/users',
params: userAuth
})
return !!res.data.length; // convert fetch result to boolean
}
// then call checkIfExists in userRegister
const userRegister = async () => {
if (validUsername() && validPassword()) {
const alreadyRegistered = await checkIfExists();
if (alreadyRegistered) {
alert('user already exists');
} else {
// handle user creation
}
}
}

Related

React and Typescript, createContext where the default value is a Promise

I am learning React and Typescript and following this tutorial (AWS Cognito + React JS Tutorial: Getting Sessions and Logging out (2020) [Cognito Episode #3] https://youtu.be/T7tXYHy0wKE). I am trying to call my new React Context with AccountContext.Provider but the default value is not working because there is a Promise in the function that is being used in the value field, but no Promise in the default value. Can I write something like this?
const AccountContext = createContext<UserAuthTypes>(username: '', password: '' => await new Promise);
I defined AccountContext to have the default value username: '', password: '', but I get this error in the return statement, on the value= line:
TS2739: Type '{ authenticate: (username: string, password: string) => Promise<unknown>; }' is missing the following properties from type 'UserAuthTypes': username, password
I see that authenticate has username and password as the inputs and a Promise as the output.
The code which is based off of this tutorial: AWS Cognito + React JS Tutorial: Getting Sessions and Logging out (2020) [Cognito Episode #3] https://youtu.be/T7tXYHy0wKE):
export interface UserAuthTypes {
username: string;
password: string;
authenticate?: any;
}
export const initialUserAuthState = {
username: '',
password: '',
};
........
const AccountContext = createContext<UserAuthTypes>(initialUserAuthState);
const Account = (props: { children: ReactNode }) => {
const authenticate = async (username: string, password: string) =>
await new Promise((resolve, reject) => {
const user = new CognitoUser({
Username: username,
Pool: UserPool,
});
const authDetails = new AuthenticationDetails({
Username: username,
Password: password,
});
user.authenticateUser(authDetails, {
onSuccess: (data) => {
resolve(data);
},
onFailure: (err) => {
reject(err);
},
newPasswordRequired: (data) => {
resolve(data);
},
});
});
return (
<AccountContext.Provider value={{ authenticate }}>
{props.children}
</AccountContext.Provider>
);
};
export { Account, AccountContext };

React and Websocket messaging to server

So I'm having issues with a single component that displays a list pulled from a resource server. Then it uses Stompjs to establish a websocket and send messages. When I load the client, the Dev Console shows logs that it tries to call onConnected method() twice as my logs show two newUser messages sent from a single load of the component.
When I try to call the submitBid() method it throws a type error saying that
"TypeError: Cannot read properties of undefined (reading 'send')
at submitBid (AuctionList.js:76:1)"
Which I'm not sure why it would be undefined on that line when it's defined and running fine in the function on line 36 which runs before the method that fails. I've been stuck on this for several days so hopefully someone can tell me what I've got wrong in the code... Here is the component code....
import React from 'react'
import Stomp from 'stompjs';
import SockJS from 'sockjs-client';
import {useState, useEffect } from 'react';
function AuctionList({user, authCredentials, token}) {
const [auctionItems, setAuctionItems] = useState([]);
const [userData, setUserData] = useState({
email: user.email,
name: user.name,
message: ''
});
const [bid, setBid] = useState(0.00);
let stompClient;
let socket;
const connect = async () => {
socket = new SockJS('http://localhost:8080/ws')
stompClient = Stomp.over(socket)
stompClient.connect({}, onConnected, onError)
}
const onConnected = async () => {
stompClient.subscribe('/topic/bids', onMessageReceived)
stompClient.send("/app/socket.newUser",
{},
JSON.stringify({
sender: user.name,
type: 'NEW_USER',
time: Date.now()
})
)
}
const onError = async (err) => {
console.log(err);
}
const handleChange = async (e) =>{
setBid(e.target.value);
}
const submitBid = async (item) => {
let newMessage = {
type: "BID",
newBid: {
itemId: item.id,
email: user.email,
bidPrice: bid,
bidTime: new Date().getTime()
},
sender: userData.email,
time: new Date().getTime()
};
try {
stompClient.send("/socket.send", {}, JSON.stringify(newMessage));
} catch(err){
console.log(err); }
}
const onMessageReceived = async (payload)=>{
console.log("onMessageReceived")
console.log(payload)
}
const getAuctionList = async () => {
const url = "http://localhost:8080/auctionlist";
const init = {
method: "GET",
headers: {
'Content-type': 'application/json',
'Authorization': `Bearer ${token}`, // notice the Bearer before your token
},
};
fetch(url, init)
.then(response => response.json())
.then(response => {setAuctionItems(response)})
};
useEffect( () => {
getAuctionList();
connect();
}, []);
return (
<ul>
{auctionItems.map( item => {
return( <div key={item.id} className = "auctionItemComponent">
<h3>{item.name}</h3>
<span>{item.desc}</span>
<span>Current Bid: ${item.itemStartingPrice}</span>
<span>Minimum Bid: {item.itemMinBid}</span>
<span>Time left</span>
<input type="number" id="bidInput_" name="bidInput" onChange={handleChange} ></input>
<button type='submit' onClick={submitBid}>Submit bid</button>
</div>)
})}
</ul>
)
}
export default AuctionList
Also I realize I have a bunch of async functions that don't have any awaits. I tried adding those in, but it was no change.
The issue here is not with stompjs but with the scoping. You have stompClient inside React Component but the one from submitBid is different. You can do it in different ways.
Put stompjs in global stage as in example here: https://playcode.io/972045
You can use useRef to have the client inside the React Component and have React do the tracking of any modifications.
I personally think something like a "connection" should stay away from inside a React Component. You should have the connection configs in a different file and import an instance to the JSX file.

Next.js: Cookies are not properly stored from nested registration page

Customers are redirected to our Next.js app via a third-party checkout page. We need to track the account type and promotion used to purchase the program. We have created a dynamic registration page /sign-up/[type]/[promotion].tsx that customers are redirected to depending on their purchase so that we can save those values on account creation.
The issue we are having is cookies are returning undefined in our index page getServerSideProps so we are unable to fetch the user's data we also redirect to the /login page if the cookie is missing. I can see the cookie in the console. We are also not having this issue with the standard login and registration pages. I have gone so far as installing the nookies library to try to resolve the issue, but it has offered nothing new.
I have also tried ditching the dynamic route by building out all of the different registration routes, which are also failing.
const RegisterForm: React.FC<RegisterFormProps> = () => {
...
const router = useRouter()
const { type, promotion } = router.query
<Formik
initialValues={{ email: '', password: '' }}
validationSchema={Yup.object({
email: Yup.string().email('Invalid email address').required('Required'),
password: Yup.string()
.min(6, 'Password must be longer than 6 characters.')
.required('Required'),
})}
onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true)
try {
const response = await apolloClient.mutate({
mutation: CREATE_USER,
variables: {
email: values.email,
password: values.password,
isRootUser: true,
type,
promotion,
},
})
if (response?.data?.createUser?.error) {
setLoading(false)
return setError(response.data.createUser?.error)
}
setSubmitting(false)
const data = response.data.createUser
setAuthToken(data.authToken)
nookies.set({}, 'authToken', data.authToken, {
maxAge: 60 * 60 * 24,
})
setAuthenticated(true)
router.push('/')
} catch (error) {
setSubmitting(false)
alert(error)
}
}}
>
...
}
index.tsx
export const getServerSideProps: GetServerSideProps = async (context: GetServerSidePropsContext) => {
const cookies = nookies.get(context)
const authToken = cookies?.authToken
// if there is no authToken, redirect to login
if (!authToken)
return {
redirect: {
permanent: false,
destination: '/login',
},
props: {},
}
const apolloClient = initializeApollo()
const response = await apolloClient.query({
query: GET_USER_PROGRESS,
variables: {
authToken,
},
fetchPolicy: 'no-cache',
})
const progress = response?.data?.getUserProgress
return {
props: {
data: progress,
},
}
}

React creating a mock login service with async await

I am creating a mock login button which will return a user object after 5 seconds of clicking the login button.
I have the following event handler for the login button:
import UserService from "../services/user";
export default class Login extends Component {
constructor(props) {
super(props);
this.state = {
field: {
username: "",
password: "",
},
};
}
login = async (event) => {
event.preventDefault();
const res = await UserService.login({
username: this.state.field.username,
password: this.state.field.password,
});
console.log("RESPONSE DATA", res);
// Set user variable to the response
};
And this is the user service:
let user = null;
const login = async (credentials) => {
await setTimeout(() => {
user = {
name: "test user",
username: credentials.username,
password: credentials.password,
token: "test token",
};
console.log("login service finished");
}, 5000);
return user;
};
Basically, I want the user object to appear on the console.log("RESPONSE DATA", res) part. However, what I'm getting instead is null. How do I go around on doing this properly?
==================================
EDIT:
Decided to rewrite into function components. I'm still getting the same results
const Login = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const userContext = useContext(UserContext);
const login = async (event) => {
event.preventDefault();
console.log("logging in:", username, password);
try {
const user = await UserService.login({
username: username,
password: password,
});
console.log(user);
// userContext[1](user);
// console.log(userContext);
} catch (exception) {
console.log(exception);
}
};
The main issue here is that setTimeout return value is basically
a positive integer value which identifies the timer created by the call to setTimeout(). (which can be passed to clearTimeout() to cancel the timeout)
So in order to achive this you need to transform your function so it returns a promise. You can find some options here. In your case you could edit it slightly the answers so do something like:
let user = null;
const login = async (credentials) => {
return new Promise(resolve => setTimeout(() => {
user = {
name: "test user",
username: credentials?.username,
password: credentials?.password,
token: "test token",
};
console.log("login service finished");
return resolve(user);
}, 5000))
};

Facebook and Google requires two login click to login with Firebase Auth

I have React web application with firebase auth (mail, Facebook, Google).
Google and Facebook work only after 2 login clicks.
The code is equal, just the provider is different.
import React from 'react';
import firebase from "firebase/app";
import { app } from "../../../../config/firebase";
const signupWithGoogle = (user, userInfo)=>{
app.firestore().collection('users').doc(user.uid).set({
firstName: userInfo.profile.given_name,
lastName: userInfo.profile.family_name});
const batch = app.firestore().batch();
const initData = [
{ Applied: { positionIds: [], title: 'Applied' } },
{ Contract: { positionIds: [], title: 'Contract' } },
{ Denied: { positionIds: [], title: 'Denied' } },
{ InProgress: { positionIds: [], title: 'In Progress' } },
{ ReceivedTask: { positionIds: [], title: 'Received Task' } },
];
initData.forEach((doc) => {
const docRef = app
.firestore()
.collection('users')
.doc( user.uid)
.collection('columns')
.doc(Object.keys(doc)[0]);
batch.set(docRef, Object.values(doc)[0]);
});
const batchCommit= batch.commit();
return batchCommit;
}
export const googleLogin = async (
history
) => {
var provider = new firebase.auth.GoogleAuthProvider();
await firebase.auth()
.signInWithPopup(provider)
.then( resp => {
let {user, credential,additionalUserInfo: userInfo} = resp;
if (userInfo.isNewUser) signupWithGoogle(user, userInfo);
}).then(()=>
history.push('/')
)
.catch((error) => {
console.error(error.message);
});
};
I saw this question, but didn't help.(Firebase Authentication Requires Two 'Login' Calls)
I had the same problem with Firebase Authentication with Facebook, I had to register two times to make it works.
The problem was in my HTLM, I used a form.
I changed for a simpler code, and it worked.
While waiting for where you call your function from, as your issue would relate to improper state management, here are some edits you can make to the code you have shared so far to squash some problems that it has.
In your signupWithGoogle function, you create a floating promise that should be included in the write batch that you use to create the /users/{userId}/columns collection. Because you use Object.keys(doc)[0] and Object.values(doc)[0], you should consider using an array of [docId, docData] pairs or a JSON-like object structure like so:
// prepare data to add to the user's columns collection
const initColumnsData = {
Applied: { positionIds: [], title: 'Applied' },
Contract: { positionIds: [], title: 'Contract' },
Denied: { positionIds: [], title: 'Denied' },
InProgress: { positionIds: [], title: 'In Progress' },
ReceivedTask: { positionIds: [], title: 'Received Task' }
};
// queue columns data upload
Object.entries(initColumnsData)
.forEach(([docId, docData]) => {
const docRef = userDocRef
.collection('columns')
.doc(docId);
batch.set(docRef, docData);
});
As you mentioned that a lot of your code is shared aside from the provider implementation, you should consider extracting the common code from those functions:
const initUserData = (user, userDocData) => {
// init write batch
const batch = app.firestore().batch();
// init ref to user data
const userDocRef = app.firestore().collection('users').doc(user.uid);
// queue user data upload
batch.set(userDocRef, userDocData);
// prepare data to add to the user's columns collection
const initColumnsData = {
Applied: { positionIds: [], title: 'Applied' },
Contract: { positionIds: [], title: 'Contract' },
Denied: { positionIds: [], title: 'Denied' },
InProgress: { positionIds: [], title: 'In Progress' },
ReceivedTask: { positionIds: [], title: 'Received Task' }
};
// queue columns data upload
Object.entries(initColumnsData)
.forEach(([docId, docData]) => {
const docRef = userDocRef
.collection('columns')
.doc(docId);
batch.set(docRef, docData);
});
// make the changes
return batch.commit();
}
const initUserDataForGoogle(user, userInfo) {
return initUserData(user, {
firstName: userInfo.profile.given_name,
lastName: userInfo.profile.family_name
});
}
const initUserDataForFacebook(user, userInfo) {
return initUserData(user, {
firstName: /* ... */,
lastName: /* ... */
});
}
When exporting a function to be called elsewhere, avoid causing "side effects" (like navigating using the History API) and don't trap errors (using .catch() without rethrowing the error). The calling code should handle the result and any errors itself.
export const loginWithGoogle = async () => {
const provider = new firebase.auth.GoogleAuthProvider();
return firebase.auth()
.signInWithPopup(provider)
.then(async resp => {
const {user, credential, additionalUserInfo: userInfo} = resp;
if (userInfo.isNewUser)
await initUserDataForGoogle(user, userInfo);
return user;
});
};
Then in your components, you'd use:
setLoading(true);
/* await/return */ loginWithGoogle()
.then(() => {
history.push('/');
// or
// setLoading(false)
// then do something
})
.catch((err) => {
console.error("loginWithGoogle failed: ", err);
setLoading(false);
setError("Failed to log in with Google!"); // <- displayed in UI to user
});

Resources