0

I'm trying to validate a form before submting it,and I created an object of possible errors, but when try to change the value of each key it behaves weirdly...

const inialState = {
  name: "",
  email: "",
  message: "",
};
const errors = {
  name: false,
  email: false,
  message: false,
};
const Contact = () => {
  const [values, setValues] = useState(inialState);
  const [error, setError] = useState(errors);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!validateSubmit()) {
      return;
    }
  };
  const handleChange = (e) =>
    setValues({ ...values, [e.target.name]: e.target.value });

  function validateSubmit(e) {
    let response = true;
    if (!values.name) {
      setError({ ...error, name: true });
      response = false;
    }
    if (!values.email) {
      setError({ ...error, email: true });
      response = false;
    }
    if (!values.message) {
      setError({ ...error, [errors.message]: true });// I also tried this way =(
      response = false;
    }
    console.log(error);
    return response;
  }
...
return(
    <form onSubmit={handleSubmit}> //its a simple button type="submit"
...

The validateSubmit function is called by the Submit button.

This is what It's shown when I submit the form and its supossed to change the state values...

rafaelpadu
  • 1,134
  • 2
  • 6
  • 20
  • Should it be `[values.message]: true` instead of `[errors.message]: true`? – Rafael Tavares Jan 19 '22 at 19:42
  • I suggest you extract the error in a new object and later at the very end before the return sentence assign the error result object to the state like this: setError(errorResult) – Gabriel Martinez Bustos Jan 19 '22 at 19:44
  • @Rafael Tavares , no because the values is the object state of all of the form fields – rafaelpadu Jan 19 '22 at 19:48
  • Where does `values` come from? Where is `validateSubmit` called? Where are you doing that `console.log` whose output you've shown? The parts of the code you've shown so far seem more or less reasonable. – Bergi Jan 19 '22 at 20:07
  • It really should be `{ ...error, message: true }` not `{ ...error, [errors.message]: true }` to signify that `message` has an error. – Bergi Jan 19 '22 at 20:15
  • @Bergi I added almost the whole component, only left out the irrelevant components parts... – rafaelpadu Jan 19 '22 at 20:26

2 Answers2

1

The answer here is useReducer() to modify only portions of the state. https://reactjs.org/docs/hooks-reference.html#usereducer.

const errors = {
     name: false,
     email: false,
     message: false,
};

const reducer = (state, action) => {
    return {...state, ...action};
};

const [error, updateError] = useReducer(reducer,
    errors
);

function validateSubmit(e) {
    let response = true;
    if (!values.name) {
      updateError({name: true });
      response = false;
    }
    if (!values.email) {
      updateError({email: true });
      response = false;
    }
    if (!values.message) {
      updateError({message: true });
      response = false;
    }
    return response;
}
Ross
  • 1,026
  • 12
  • 10
  • Your `reducer` doesn't "*modify only portions of the state*", instead it completely throws away the previous state. It's not clear how that is better than using `useState` – Bergi Jan 19 '22 at 20:13
  • Why are you destructuring `{ data }`? You never pass an object with a `data` property as an action. – Bergi Jan 19 '22 at 20:13
  • Shouldn't have copy pasted old code, thanks @Bergi – Ross Jan 19 '22 at 21:13
0

The problem occurs when you call setError multiple times from validateSubmit. Only the last value will win - in your example that's the one that added "false": true (because errors.message that you used as a property name is false).

Notice that setError does not (synchronously, or at all) update the error constant, it only changes the component state and causes it to re-render with a new value. The {...errror, …} always did refer to the original value of error. To avoid this, you can

  • either aggregate the errors into a single value before calling setError once

    function validateSubmit(e) {
      let newError = error;
      if (!values.name) {
        newError = { ...newError, name: true };
      }
      if (!values.email) {
        newError = { ...newError, email: true };
      }
      if (!values.message) {
        newError = { ...newError, message: true };
      }
      console.log(error, newError);
      setError(newError);
      return newError != error;
    }
    
  • or use the callback version of setError that will execute the updates in a row and always pass the latest state in each callback as an argument:

    function validateSubmit(e) {
      let response = true;
      if (!values.name) {
        setError(oldError => ({ ...oldError, name: true }));
        response = false;
      }
      if (!values.email) {
        setError(oldError => ({ ...oldError, email: true }));
        response = false;
      }
      if (!values.message) {
        setError(oldError => ({ ...oldError, message: true }));
        response = false;
      }
      console.log(error);
      return response;
    }
    
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks man! I knew I was missing the callback of the same object. Just a note, theres a parentheses missing. setError((error) => ({ ...error, name: true })). – rafaelpadu Jan 19 '22 at 20:59
  • Quick question, why does my controled inputs works just fine (const handleChange = (e) => setValues({ ...values, [e.target.name]: e.target.value });), but these guys won't? I mean like, isn't it the same concept? – rafaelpadu Jan 19 '22 at 21:01
  • Because you call `setValues` only once, before the component is rerendered and a new `handleChange` function is created and attached as the handler, that will close over the new value of `const values`. – Bergi Jan 19 '22 at 21:03