lmcc
lmcc

Reputation: 73

useState variable not changing on keystroke

I have a login form made from a map of a FormInput component.

I have now created separate FormComponents depending on the question type as defined by a prop.

However, since making this change my 'credentials' state is not updating on a user keystroke.

I would like the CREDENTIALS state to be updated on keystroke. However, this is not happening currently.

LoginForm

import React from 'react';
import FormInput from '../../components/Form Input/FormInput';
import { loginInputs } from '../../formSource/formSourceData';
import './Login.scss';
import { useState, useContext } from 'react';
import { useNavigate, Link, useLocation } from 'react-router-dom';
import { useToastContext } from '../../context/toastContext';
import useFetch from '../../Hooks/UseFetch';
import { AuthContext } from '../../context/AuthContext'
import axios from 'axios'


const Login = () => {

  const [credentials, setCredentials] = useState({
    email: undefined,
    password: undefined,
  });

  const { user, loading, error, dispatch} = useContext(AuthContext)

  const { toastDispatch } = useToastContext();
  const navigate = useNavigate();
  const { state } = useLocation()

  const handleChange = (e) => {
    console.log(e.target.name)
    setCredentials({ ...credentials, [e.target.name]: e.target.value });
    console.log(credentials)
  };

  const handleSubmit = async (e) => {
    e.preventDefault()
    dispatch({type: "LOGIN_START"})
    try{
      const res = await axios.post("/api/auth/login", credentials)
      dispatch({type:"LOGIN_SUCCESS", payload: res.data})
      navigate(state?.path || '/auth/teacher/dashboard');
    } catch(err) {
      dispatch({type: "LOGIN_FAILURE", payload: err.response.data})
    }
  }

  return (
    <div className="container">
      <div className="formWrapper">
        <h1>Login</h1>
        <form className="loginForm" >
          {loginInputs.map((input) => (
            <FormInput
              key={input.id}
              {...input}
              handleChange={handleChange}
            />
          ))}
          <button type="submit" className="loginButton" onClick={handleSubmit}>
            Login
          </button>
        </form>
        <p className="forgotPassword">
          <Link to="/forgot-password">Forgot Password</Link>
        </p>
        <p className="accountText">
          Not signed up? <Link to="/register">Register</Link>
        </p>
      </div>
    </div>
  );
};

export default Login;

FormInputComponent

import { InputRounded } from '@mui/icons-material';
import React, { useState } from 'react';
import './formInput.scss';

const FormInput = (props) => {
  const { label, type, errorMessage, handleChange, id, value, ...inputProps } =
    props;

  const [focused, setFocused] = useState(false);
  const [passwordShown, setPasswordShown] = useState(false);

  const handleFocus = (e) => {
    setFocused(true);
  };

  const togglePassword = () => {
    setPasswordShown(!passwordShown);
  };

  const Dropdown = () => {
    return (
      <select
        className="formElementInput"
        value={value}
        name={inputProps.name}
        onChange={handleChange}
      >
        <option className="default" selected disabled>
          {inputProps.placeholder}
        </option>
        {inputProps.options.map((option) => (
          <option className="option" value={option}>
            {option}
          </option>
        ))}
      </select>
    );
  };

  const Input = () => {
    return (
      <div className="formGroup">
        <input
          className="formElementInput"
          value={value}
          name={props.name}
          placeholder={props.placeholder}
          type={passwordShown ? 'text' : type}
          onChange={props.handleChange}
          onBlur={handleFocus}
          focused={focused.toString()}
          onFocus={() =>
            inputProps.name === 'confirmPassword' && setFocused(true)
          }
        />
        <span className="icon" onClick={togglePassword}>
          {passwordShown ? inputProps.icon : inputProps.opposite}
        </span>
      </div>
    );
  };

  return (
    <div className="formElement">
      <label className="formElementLabel">{label}</label>
      {type === 'dropdown' ? (
        <Dropdown />
      ) : (
        <Input/>
      )}

      <span className="errorMessage">{errorMessage}</span>
    </div>
  );
};

export default FormInput;

LOGIN INPUT CODE

export const loginInputs = [
    {
        id: 1,
        name: "email",
        type: "email",
        placeholder: "Email",
        label: "Email",
        errorMessage: "Enter a valid email address",
        required: true

    },
    {
        id: 2,
        name: "password",
        type: "password",
        placeholder: "Password",
        label: "Password",
        errorMessage: "A password should be more than 8 characters.",
        required: true,
        icon: <Visibility/>,
        opposite: <VisibilityOff/>

    }
]

This is the original working code prior to separating.

import { InputRounded } from '@mui/icons-material';
import React, { useState } from 'react';
import './formInput.scss';

const FormInput = (props) => {
  const { label, type, errorMessage, handleChange, id, value, ...inputProps } =
    props;

  const [focused, setFocused] = useState(false);
  const [passwordShown, setPasswordShown] = useState(false);

  const handleFocus = (e) => {
    setFocused(true);
  };

  const togglePassword = () => {
    setPasswordShown(!passwordShown);
  };

  const Dropdown = () => {
    return (
      <select
        className="formElementInput"
        value={value}
        name={inputProps.name}
        onChange={handleChange}
      >
        <option className="default" selected disabled>
          {inputProps.placeholder}
        </option>
        {inputProps.options.map((option) => (
          <option className="option" value={option}>
            {option}
          </option>
        ))}
      </select>
    );
  };

  const Input = () => {
    return (
      <div className="formGroup">
        <input
          className="formElementInput"
          value={value}
          name={props.name}
          placeholder={props.placeholder}
          type={passwordShown ? 'text' : type}
          onChange={props.handleChange}
          onBlur={handleFocus}
          focused={focused.toString()}
          onFocus={() =>
            inputProps.name === 'confirmPassword' && setFocused(true)
          }
        />
        <span className="icon" onClick={togglePassword}>
          {passwordShown ? inputProps.icon : inputProps.opposite}
        </span>
      </div>
    );
  };

  return (
    <div className="formElement">
      <label className="formElementLabel">{label}</label>
      {type === 'dropdown' ? (

        <select
          className="formElementInput"
          value={value}
          name={inputProps.name}
          onChange={handleChange}
        >
          <option className="default" selected disabled>
            {inputProps.placeholder}
          </option>
          {inputProps.options.map((option) => (
            <option className="option" value={option} >
              {option}
            </option>
          ))}
        </select>
      ) : (
        <div className="formGroup">
          <input
            className="formElementInput"
            value={value}
            name={inputProps.name}
            placeholder={inputProps.placeholder}
            type={passwordShown ? "text" : type}
            onChange={handleChange}
            onBlur={handleFocus}
            focused={focused.toString()}
            onFocus={() =>
              inputProps.name === 'confirmPassword' && setFocused(true)
            }
          />
          <span className="icon" onClick={togglePassword}>
              {passwordShown ? inputProps.icon : inputProps.opposite}
          </span> 
          </div>
      )}

      <span className="errorMessage">{errorMessage}</span>
    </div>
  );
};

export default FormInput;

Edit thirsty-feather-9fwwwy

Here is a link to the code sandbox which exhibits the same behaviour I have explained above.

https://codesandbox.io/s/thirsty-feather-9fwwwy?file=/src/App.js

Upvotes: 0

Views: 69

Answers (1)

Shivam Jha
Shivam Jha

Reputation: 4572

The reason is that your <Input /> component inside FormComponent.jsx is controlled.. so it looks for the value attribute in rendered input field, but could not find any:

return (
      <div className="formGroup">
        <input
          className="formElementInput"
          name={props.name}
          value={props.value}
          placeholder={props.placeholder}
          type={passwordShown ? "text" : props.type}
          onChange={props.handleChange}
          onBlur={handleFocus}
          focused={focused.toString()}
          onFocus={() => props.name === "confirmPassword" && setFocused(true)}
        />
        // ...

Because.. there is not any:

// Notice no value `key` on any of these objects:
export const loginInputs = [
  {
    id: 1,
    name: "email",
    type: "email",
    placeholder: "Email",
    label: "Email",
    errorMessage: "Enter a valid email address",
    required: true
  },
  {
    id: 2,
    name: "password",
    type: "password",
    placeholder: "Password",
    label: "Password",
    errorMessage: "A password should be more than 8 characters.",
    required: true,
    icon: "Icon",
    opposite: "Icon2"
  }
];

to fix that, you can provide a value attribute, depending upon your credentials state:

{loginInputs.map((input) => (
  <FormInput
    key={input.id}
    value={credentials[input.name] ?? ""} // provide value prop
    {...input}
    handleChange={handleChange}
  />
))}

Bonus: I noticed that the input lost it's focus every time you typed there(maybe this was not the problem in your code, just in Sandbox you provided).

This was because you declared <Input /> inside the <FormInput /> so everytime it changed, it basically created that <Input /> component from scratch. These are many ways to do it, but the easiest is to just use it inline in ternary:

{props.type === "dropdown" ? (
        <Dropdown /> // Also do the same for `Dropdown` if  face the similar isssue
      ) : (
        // used inline
        <div className="formGroup">
          <input
            className="formElementInput"
            name={props.name}
            value={props.value}
            placeholder={props.placeholder}
            type={passwordShown ? "text" : props.type}
            onChange={props.handleChange}
            onBlur={handleFocus}
            focused={focused.toString()}
            onFocus={() => props.name === "confirmPassword" && setFocused(true)}
          />
          <span className="icon" onClick={togglePassword}>
            {passwordShown ? props.icon : props.opposite}
          </span>
        </div>
      )}

Here's the link to updated Sandbox:

https://codesandbox.io/s/epic-poitras-84z4do?file=/Login.jsx:943-995

Upvotes: 1

Related Questions