sam
sam

Reputation: 63

React useState hook: object property remains unchanged

I have recently started learning react and I have a component that has a login form:

import Joi from "joi";
import { useState } from "react";

const Login = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const loginSchema = Joi.object().keys({
    username: Joi.string().required(),
    password: Joi.string().required(),
  });
  const [errors, setErrors] = useState({ username: null, password: null });

  const handleSubmit = (form) => {
    form.preventDefault();
    validate();
  };

  const validate = () => {
    const { error } = loginSchema.validate(
      { username, password },
      { abortEarly: false }
    );
    if (!error) return null;
    for (const item of error.details) {
      let key = item.path[0];
      let value = item.message;
      setErrors({ ...errors, [key]: value });
    }
    return errors;
  };

  return (
    <div className="container">
      <div>
        <h1>Login</h1>
      </div>
      <div className="card">
        <div className="card-body">
          <form onSubmit={handleSubmit}>
            <div className="form-group">
              <label htmlFor="username">Username</label>
              <input
                name="username"
                type="text"
                className="form-control"
                id="username"
                placeholder="Enter username"
                value={username}
                onChange={(username) => setUsername(username.target.value)}
              />
              {errors.username && (
                <div className="alert alert-danger">{errors.username}</div>
              )}
            </div>
            <div className="form-group">
              <label htmlFor="password">Password</label>
              <input
                name="password"
                type="password"
                className="form-control"
                id="password"
                placeholder="Password"
                value={password}
                onChange={(password) => setPassword(password.target.value)}
              />
              {errors.password && (
                <div className="alert alert-danger">{errors.password}</div>
              )}
            </div>
            <button type="submit" className="btn btn-primary">
              Submit
            </button>
          </form>
          {/* Delete later */}
          <h4>{JSON.stringify(errors)}</h4>
        </div>
      </div>
    </div>
  );
};

export default Login;

Here, I am trying to set the values of username and password in the errors object, but for some reason, only the password property gets set, the username property remains null. I have used object destructuring but it still doesn't set the value, please help.

Upvotes: 0

Views: 513

Answers (2)

Shubham Khatri
Shubham Khatri

Reputation: 281734

State updates are not reflected immediately in React, and since you loop over your data to set the errors object multiple times, it only sets in the last key due to batching in event handlers and the fact that state updates are affected by closures.

You should instead create an updated objected and set it in state once

   let newErrors = {};
   for (const item of error.details) {
      let key = item.path[0];
      let value = item.message;
      newErrors = { ...newErrors, [key]: value };
    }
  setErrors(prev => ({...prev, ...newErrors}));

Upvotes: 1

Olivier Boiss&#233;
Olivier Boiss&#233;

Reputation: 18113

I think the problem comes from this code

for (const item of error.details) {
  let key = item.path[0];
  let value = item.message;
  setErrors({ ...errors, [key]: value });
}

Here you are calling setErrors in a for loop with the same errors object so only the last field is taken into account.

Here is a possible solution :

if (!error) return null;

const newErrors = error.details.reduce((acc, item) => {
  const key = item.path[0];
  const value = item.message;
  acc[key] = value;
  return acc;
}, {...errors});

setErrors(newErrors);

Upvotes: 1

Related Questions