There are a few problems, but they all come from the same root – state updates to not take place immediately.
Problem one is that you cannot use the updated state right after calling setState
. This is causing your validation logic to execute using the form values from the previous render.
Problem two is that if you call a state updater function twice in a row, it will only use the last value. So in your validation logic, you call setFormError
once based on the value of login
, and once based on the value of password
. This means the value determined by the login
logic will never be used.
I’ve written an example of a few ways you can write this correctly.
Example one as a function called on each input change:
const { useState } = React;
const MyInput = (props) => {
return (
<input {...props} style={{ border: "1px solid red", outline: "none" }} />
);
};
const Example = () => {
const [form, setForm] = useState({
login: "",
password: ""
});
const [formError, setFormError] = useState({
isValidLogin: "FAIL",
isValidPassword: "FAIL"
});
const handleValidateForm = (newForm) => {
let newValid = {...formError}; // Use a new object so we only update state once
if (newForm.login.length > 3) {
newValid.isValidLogin = "GOOD"
} else {
newValid.isValidLogin = "FAIL"
}
if (newForm.password.length > 3) {
newValid.isValidPassword = "GOOD"
} else {
newValid.isValidPassword = "FAIL"
}
setFormError(newValid);
};
const onChange = (e) => {
e.preventDefault();
const { name, value } = e.target;
const newForm = {...form, [name]: value}; // Set to new object
setForm(newForm);
handleValidateForm(newForm); // Send the new object so its up-to-date
};
return (
<div>
<div>
<MyInput type="text" name="login" onChange={onChange} />{" "}
{formError.isValidLogin}
<br />
<MyInput type="text" name="password" onChange={onChange} />{" "}
{formError.isValidPassword}
</div>
</div>
);
};
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
The other approach would be to move the validation into a useEffect
:
const { useState, useEffect } = React;
const MyInput = (props) => {
return (
<input {...props} style={{ border: "1px solid red", outline: "none" }} />
);
};
const Example = () => {
const [form, setForm] = useState({
login: "",
password: ""
});
const [formError, setFormError] = useState({
isValidLogin: "FAIL",
isValidPassword: "FAIL"
});
useEffect(() => {
let newValid = {...formError}; // Use a new object so we only update state once
if (form.login.length > 3) {
newValid.isValidLogin = "GOOD"
} else {
newValid.isValidLogin = "FAIL"
}
if (form.password.length > 3) {
newValid.isValidPassword = "GOOD"
} else {
newValid.isValidPassword = "FAIL"
}
setFormError(newValid);
}, [form]); // Runs every time the form changes
const onChange = (e) => {
e.preventDefault();
const { name, value } = e.target;
setForm({...form, [name]: value});
};
return (
<div>
<div>
<MyInput type="text" name="login" onChange={onChange} />{" "}
{formError.isValidLogin}
<br />
<MyInput type="text" name="password" onChange={onChange} />{" "}
{formError.isValidPassword}
</div>
</div>
);
};
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
It has the same effect, so its up to you which pattern you prefer.
CLICK HERE to find out more related problems solutions.