Validation error in my login form shows only upon clicking submit button a 2nd time with incorrect login credentials. Im noticing that even though the state is updating, error message is not being rendered the first time itself. It requires a 2nd submit click to be able to see the error message Snackbar.
I have defined a separate auth-context which has all the logic for making a post request for login and performing error handling, validations as well as setting user data to localStorage. It also has context providers to supply current state value. Login.js is using this auth-context to make a post request and based on the state either logs in or (is supposed to) displays validation errors.
Here's part of my Login.js page Component:
import { useAuth } from '../../context/auth-context';
export default function Login() {
const classes = useStyles();
const { handleChange, handleSubmit, formValues, formErrors } = useForm(submit, validate);
const [errorAlertMsg, setErrorAlertMsg] = useState('');
const authState = useAuth();
function submit() {
authState.login(formValues);
if(authState.error){
setErrorAlertMsg(authState.error);
}
/*authState initially shows error as null instead of the validation error. After submit is clicked it stays that way upon reaching here. On 2nd submit click it shows*/
console.log(authState);
}
function closeAlertHandler() {
setErrorAlertMsg('');
}
function Alert(props) {
return <MuiAlert elevation={6} variant="filled" {...props} />;
}
return (
<div>
<Grid container>
<Grid
item xs={4}
direction='row'
>
<UnauthenticatedSidebar/>
</Grid>
<Grid
item xs={8}
direction='row' >
<Container className={classes.mainContainer} component="main" maxWidth="xs">
<CssBaseline />
<Grid container justify='flex-end'>
<Grid item>
<Typography classes = {{ body2: classes.body2Style }} component="p" variant="body2">
Don't have an account?
<Link className={classes.routerLink} to='/signup'>
<Button color='primary' classes = {{ root: classes.buttonMarginRight }} variant="outlined" size="large">
Create Account
</Button>
</Link>
</Typography>
</Grid>
</Grid>
<div className={classes.paper}>
<Typography className={classes.welcomeHeading} component="h2" variant="h4">
Welcome Back!
</Typography>
<form onSubmit= { handleSubmit } className={classes.form} noValidate>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
InputProps = {{
classes: {root: classes.textFieldSelectLabel}
}}
InputLabelProps = {{
classes: {root: classes.textFieldSelectLabel}
}}
value={ formValues.email }
onChange = { handleChange }
error = { formErrors.email }
helperText = { formErrors.email || null }
/>
</Grid>
<Grid item xs={12}>
<TextField
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
InputProps = {{
endAdornment: <InputAdornment position='end'>
<Typography className={classes.adornmentStyle}>
Forgot?
</Typography>
</InputAdornment>,
classes: {root: classes.textFieldSelectLabel}
}}
InputLabelProps = {{
classes: {root: classes.textFieldSelectLabel}
}}
value={formValues.password}
onChange= { handleChange }
error = { formErrors.password }
helperText= { formErrors.password || null}
/>
</Grid>
</Grid>
<Button
type="submit"
variant="contained"
color="primary"
className={classes.submit}
>
Login
</Button>
</form>
<Snackbar open = {errorAlertMsg.length !== 0} autoHideDuration={5000} onClose = { closeAlertHandler }>
<Alert onClose={closeAlertHandler} severity="error">
{errorAlertMsg}
</Alert>
</Snackbar>
</div>
</Container>
</Grid>
</Grid>
</div>
);
}
Here's the auth-context.js file
import React, { useState } from 'react';
import axios from 'axios';
const AuthContext = React.createContext([{}, () => {}]);
function AuthProvider({children}) {
const [state, setState] = React.useState({
status: 'logged out',
error: null,
user: null
});
const [token, setToken] = useState(localStorage.getItem('authToken') ? localStorage.getItem('authToken') : null);
const [email, setEmail] = useState(localStorage.getItem('email') ? localStorage.getItem('email'): null);
//checking for token and email and then accordingly updating the state
const getUser = () => {
if(token) {
setState({status: 'success', error: null, user: email})
}
}
const logout = () => {
localStorage.removeItem('authToken');
setToken(null);
localStorage.removeItem('email');
setEmail(null);
setState({status: 'logged out', error: null, user: null})
}
const login = async(formValues) => {
try {
const res = await axios.post('http://localhost:3001/user/login', formValues);
if(res.data.token) {
setState({status:'success', error:null, user: formValues.email});
localStorage.setItem('authToken', res.data.token);
setToken(res.data.token);
localStorage.setItem('email', res.data.email);
setEmail(res.data.email);
}
}
catch(err) {
const validationError = err.response.data.validationError || null;
const missingDataError = err.response.data.missingData || null;
if(validationError){
setState({status: 'error', error: validationError, user: null})
}
else if(missingDataError) {
setState({status: 'error', error: missingDataError, user: null})
}
else {
console.error(err);
}
}
}
React.useEffect(() => {
getUser();
}, [token, email]);
let authState = {...state, logout, login}
/**
* Provider component is the place where you'd pass a prop called value to,
* which you can subsequently consume within the Consumer component
*/
return (
<AuthContext.Provider value={authState}>
{state.status === 'pending' ? (
'Loading...'
) : state.status === 'logged out' ? (
children
) : (
children
)}
</AuthContext.Provider>
)
}
//this seems simpler method to pass functions from context to consumers
function useAuth() {
const context = React.useContext(AuthContext)
if (context === undefined) {
throw new Error(`useAuth must be used within a AuthProvider`)
}
return context;
}
export {AuthProvider, useAuth};