[React-Context]: Form validation error fails to show up the first time

Viewed 670

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};
1 Answers

There are two issues in here

  • login is an asynchronous function
  • State updates are bound by closure in functional component and do not reflect in the same render cycle.

Since you are using authState.error immediately after calling authState.login, you don't see the updated value.

The solution is to make use of useEffect and wait for the authState to change

function submit() {
   authState.login(formValues);
}

useEffect(() => {
    if(authState.error){
      setErrorAlertMsg(authState.error);
    }
    console.log(authState);
}, [authState]);
Related