So I try make a form with a conditional checkbox using react hook form but react always give me the previous state of the checkbox value when i save my data in the context. i see some people say you have to add a useEffect cause useState is asynchronous but nothing change for me.
my code:
const ContactFormMain: FunctionComponent<any> = ({ nextFormStep }) => {
const { saveForm, infos } = useContext(ContactFormContext) as ContactFormDataType;
const { register } = useForm({
mode: "onBlur",
})
const [formUserData, setFormUserData] = useState<IContactFormData | {}>(infos[0]);
const [val, setVal] = useState(false)
const [sharingChoice, setSharingChoice] = useState<string>(infos[0].sharePatientInfo)
useEffect(() => {
console.log(sharingChoice)
console.log(val)
}, [sharingChoice, val])
const updateContext = (e: FormEvent<HTMLButtonElement | HTMLTextAreaElement | HTMLInputElement>): void => {
console.log('afterSetState', formUserData);
setFormUserData({
...formUserData,
[e.currentTarget.id]: e.currentTarget.value
})
}
const handleCheck = (e: ChangeEvent<HTMLInputElement>): void => {
console.log(e.target.checked)
setVal(!val)
if(val === true) {
setSharingChoice("Des informations concernant le patient sont transmises dans ce ticket.")
} else {
setSharingChoice("Il n'y pas d'informations patient partagées dans ce ticket.")
}
console.log('checkedValue', val)
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const updateContextWithCheckBox = (e: ChangeEvent<HTMLInputElement> ) => {
handleCheck(e);
updateContext(e);
}
const handleSaveUserInfo = (e: FormEvent, formData: IContactFormData | any) => {
e.preventDefault();
saveForm(formData)
console.log('context', formData)
nextFormStep();
}
return (
<>
<form onSubmit={(e) => handleSaveUserInfo(e, formUserData)}>
<label htmlFor='subjects'>
Raisons
</label>
<div id="subjects">
<button type="button" id="subject" value='Détection de pathologie subtile' onClick={updateContext}> Détection de pathologie subtile </button>
<button type="button" id="subject" value="Gain de temps" onClick={updateContext}> Gain de temps </button>
<button type="button" id="subject" value="impact sur la prise en charge" onClick={updateContext}> impact sur la prise en charge </button>
<textarea {...register("description")} id="description" name="description" defaultValue={infos[0].description} onChange={updateContext} />
<label htmlFor='sharePatientInfo'>
Ne contient pas d'info patient
</label>
<input type="checkbox" placeholder="infoPatient " {...register("sharePatientInfo ")} id="sharePatientInfo" checked={val} value={sharingChoice} name="sharePatientInfo" onChange={updateContextWithCheckBox} />
<button type='submit'>Suivant</button>
</div>
</form>
</>
)
}
export default ContactFormMain;
any idea?
CodePudding user response:
Short Answer
You have added a useEffect
but you aren't putting it to any use. The purpose of the useEffect
is to synchronize your state. Feel free to read more about the purpose in these React docs: https://beta.reactjs.org/learn/synchronizing-with-effects
There are 2 moments where you assume that the value will be immediately updated in the state (even though the update is asynchronous), so let's focus on each.
Fix Part 1
const handleCheck = (e: ChangeEvent<HTMLInputElement>): void => {
console.log(e.target.checked)
setVal(!val) // <-- here we tell React to update the "val" state
if(val === true) { // <-- but it is probably not updated yet when we get here
// ...so we probably end up doing the wrong thing in this "if" block
setSharingChoice("Des informations concernant le patient sont transmises dans ce ticket.")
} else {
setSharingChoice("Il n'y pas d'informations patient partagées dans ce ticket.")
}
console.log('checkedValue', val)
}
Let's fix it. It's easy enough. All we have to do is use a variable from our local scope that we know will not change, such as the previous val
or the e.target.checked
value.
const handleCheck = (e: ChangeEvent<HTMLInputElement>): void => {
console.log(e.target.checked);
const prevVal = val; // <-- we save this for later
setVal(!prevVal); // <-- tell React to update this value **asynchronously**
if(!prevVal === true) { // <-- the new value of "val" will be "!prevVal" so use that instead
// ...and now we are ending up in the right part of the "if" block
setSharingChoice("Des informations concernant le patient sont transmises dans ce ticket.")
} else {
setSharingChoice("Il n'y pas d'informations patient partagées dans ce ticket.")
}
console.log('checkedValue', val)
}
Fix Part 2
Now the other part.
const updateContext = (e: FormEvent<HTMLButtonElement | HTMLTextAreaElement | HTMLInputElement>): void => {
console.log('afterSetState', formUserData);
setFormUserData({
...formUserData,
[e.currentTarget.id]: e.currentTarget.value
// ^^^^^^^^^^^^^^^^^^^^^
// this is actually the "sharingChoice" state value, which isn't updated yet
// since that update happens asynchronously
})
}
There are a few ways to fix this one.
Quick tangent/suggestion: One method I'd recommend learning more about is using useRef
when you have a value you want to track that changes over time in the lifecycle of the component, but shouldn't trigger a re-render when its value changes. From the code you provided, it looks like the formUserData
and sharingChoice
could be refs instead of state, since nothing in the UI needs to update when they change, but in the greater context of your component, that may not be the case.
But for the quick fix here, just move the work done by the updateContext
call from updateContextWithCheckBox
method to the useEffect
block.
const updateContextWithCheckBox = (e: ChangeEvent<HTMLInputElement> ) => {
handleCheck(e);
//updateContext(e); // <-- don't do it here
}
useEffect(() => {
setFormUserData((prev) => ({
...prev,
"sharePatientInfo": sharingChoice // <-- now we actually get the latest sharingChoice value
}));
}, [sharingChoice, setFormUserData]);