0

I have just started using react and created a basic form which consists 3 fields (username, password and a submit button). I am only using reducers and state within this component and while testing out my validation I picked up that the button enabled property as a reverse reaction if I enter a correct email, backspace the '@' character and enter the '@' character back. What should happen is if the entered email doesn't include an '@' I set formIsValid property to false and if it does then I set it to true which enable the login button. I have a hunch that maybe the component isn't re-rendering but I have no idea on how to prove this nor fix this.

Screenshot of validation fails but button is still enabled

button enabled

adding the '@' in email now disables button

button disabled

This only occurs if I type something and then start backspacing and re-entering into the field, If I go in the order of filling in the email and then filling in the password as needed then is works as it should

Component :

import React,
// eslint-disable-next-line 
{ useEffect, useState, useReducer } from 'react';

import Card from '../UI/Card/Card';
import classes from './Login.module.css';
import Button from '../UI/Button/Button';

const emailReducer = (state, action) => {
  if(action.type === 'USER_INPUT') {
    return {value: action.val, isValid: action.val.includes('@') };
  }
  if (action.type === 'INPUT_BLUR') {
    return {value: state.value, isValid: state.value.includes('@') };
  }
  return { value: '', isValid: false }
};

const passwordReducer = (state, action) => {
  if(action.type === 'USER_INPUT') {
    return {value: action.val, isValid: action.val.trim().length > 6}
  }
  if(action.type === 'INPUT_BLUR') {
    return {value: state.value, isValid: state.value.trim().length > 6}
  }
};

const Login = (props) => {
  // const [enteredEmail, setEnteredEmail] = useState('');
  // const [emailIsValid, setEmailIsValid] = useState();
  const [formIsValid, setFormIsValid] = useState(false);


  const [emailState, dispatchEmail] = useReducer(emailReducer, {value: '', isValid: null});
  const [passwordState, dispatchPassword] = useReducer(passwordReducer, {value: '', isValid: null})

  const emailChangeHandler = (event) => {
    dispatchEmail({type: 'USER_INPUT', val: event.target.value});

    setFormIsValid(
      emailState.isValid && passwordState.isValid
    );
  };

  // useEffect(() => {
  //   const identifier = setTimeout(() => {
  //     setFormIsValid(
  //       enteredEmail.includes('@') && enteredPassword.trim().length > 6
  //     );
  //   }, 500);

  //   return () => {
  //     console.log('CLEANUP');
  //     clearTimeout(identifier);
  //   };
  // }, [enteredEmail, enteredPassword]);

  const passwordChangeHandler = (event) => {
    dispatchPassword({type: 'USER_INPUT', val: event.target.value});

    setFormIsValid(
      passwordState.isValid && emailState.isValid
    );
  };

  const validateEmailHandler = () => {
    dispatchEmail({type: 'INPUT_BLUR'});
  };

  const validatePasswordHandler = () => {
    dispatchPassword({type: 'INPUT_BLUR'})
  };

  const submitHandler = (event) => {
    event.preventDefault();
    props.onLogin(emailState.value, passwordState.value);
  };

  return (
    <Card className={classes.login}>
      <form onSubmit={submitHandler}>
        <div
          className={`${classes.control} ${emailState.isValid === false ? classes.invalid : ''
            }`}
        >
          <label htmlFor="email">E-Mail</label>
          <input
            type="email"
            id="email"
            value={emailState.value}
            onChange={emailChangeHandler}
            onBlur={validateEmailHandler}
          />
        </div>
        <div
          className={`${classes.control} ${passwordState.isValid === false ? classes.invalid : ''
            }`}
        >
          <label htmlFor="password">Password</label>
          <input
            type="password"
            id="password"
            value={passwordState.value}
            onChange={passwordChangeHandler}
            onBlur={validatePasswordHandler}
          />
        </div>
        <div className={classes.actions}>
          <Button type="submit" className={classes.btn} disabled={!formIsValid}>
            Login
          </Button>
        </div>
      </form>
    </Card>
  );
};

export default Login;

The above code has 2 reducers, 1 for email state and 1 for password state

stateObject:

{
   value: '',
   isValid: false
}

I am using the useState hook to only manage if the entire form is valid which is basically determined by emailState.IsValid && passwordState.IsValid

Also I am listening for onchange events on both input fields which executes my reducer functions for their respective state

I tried removing the expression that uses the reducer state and went with

event.target.value.includes('@') && passwordState.isValid thinking that maybe the reducer state is not updating in time before my component renders but that did not work. Any help pointing me to where the problem is and what is causing this would be really appreciated, thanks in advance

Nate1zn
  • 152
  • 7
  • 1
    Though this code uses `useReducer` for some state, the asynchronous nature of state updates doesn't change. Each of those handler functions is queueing a state update, but then immediately making other state decisions based on the not-yet-updated state. The calls to `setFormIsValid` should probably be in a `useEffect`. – David Dec 12 '22 at 18:32
  • Ah! that was it, thank you so much! Now I understand why the tutorials say that you shouldn't update state based on the state of something else. Going to add the side effect code here shortly that fixes this behavior. EDIT : nevermind, I just linked it to the suggested question – Nate1zn Dec 12 '22 at 18:44

0 Answers0