I am trying to build a functionality where when a user navigates away from the form i.e when component unmounts it should trigger a save i.e post form data to server. This should happen only if there is any change in form data. Can anyone guide me as to why this is happening. I have tried class based approach which works but I do not want to refactor my production code.
import { useCallback, useEffect, useState } from "react";
import React from "react";
import * as _ from "lodash";
import { useFormik } from "formik";
// for now this is hardcoded here..but let's assume
// this server data will be loaded when component mounts
const serverData = {
choice: "yes",
comment: "some existing comment"
};
const availableChoices = ["yes", "no"];
const Form = () => {
const formik = useFormik({ initialValues: { ...serverData } });
const [isFormChanged, setIsFormChanged] = useState(false);
const valuesHaveChanged = React.memo(() => {
console.log("INIT VALUES= ", formik.initialValues);
console.log("FINAL VALUES = ", formik.values);
return !_.isEqual(formik.initialValues, formik.values);
}, [formik.initialValues, formik.values]);
const triggerSave = () => console.log("Save");
useEffect(() => {
// setForm({ ...serverData });
if (valuesHaveChanged) {
setIsFormChanged(true);
}
return () => {
// when this cleanup function runs
// i.e when this component unmounts,
// i need to check if there
// was any change in the form state
// if there was a change i need to trigger a save
// i.e post form data to server.
if (setIsFormChanged) {
triggerSave();
}
};
});
return (
<form>
<div className="form-group">
{availableChoices.map((choice) => (
<label key={choice}>
{choice}
<input
id="choice"
value={choice}
className="form-control"
type="radio"
name="choice"
checked={choice === formik.values.choice}
onChange={formik.handleChange}
/>
</label>
))}
</div>
<div className="form-group">
<textarea
rows="5"
cols="30"
id="comment"
name="comment"
value={formik.values.comment}
onChange={formik.handleChange}
className="form-control"
placeholder="some text..."
></textarea>
</div>
</form>
);
};
export default Form;
CodePudding user response:
The first problem i spotted is the dependency array.
useEffect(() => {
// the flag can be set anytime upon a field has changed
// maybe formik has a value like that, read doc
if (valuesHaveChanged) {
setIsFormChanged(true);
}
return () => {
if (setIsFormChanged) {
triggerSave();
}
}
// the dependency array is [], can't be missed
}, [])
Currently you are calling this effect and cleanup this effect in every update, ex. if any value changes in this component. But normally you only want to do it once upon dismount.
Even you do the above right, you still need to make sure your code contains no memory leak, because you are trying to do something upon the dismount. So it's better to pass the values:
triggerSave([...formik.values])
And make sure inside triggerSave
, you don't accidently call anything about formik
or setState
.
CodePudding user response:
Try to use useEffect
with dependencies
useEffect(() => {
return () => {
// when this cleanup function runs
// i.e when this component unmounts,
// i need to check if there
// was any change in the form state
// if there was a change i need to trigger a save
// i.e post form data to server.
if (!_.isEqual(formik.initialValues, formik.values)) {
triggerSave();
}
};
}, [formik.values]); // won't run on every render but just on formik.values update
Explanation:
useEffect
has dependencies as a second argument, if []
is passed - effect is triggered only on mount, if [...]
passed, will trigger on the first mount and on any of ...
update.
If you don't pass the second agrument, useEffect
works as a on-every-render effect.