Hook managed state on Form does not update in parent component - reactjs

I have a form:
<FieldLabel label='Label'>
<Textfield
value={formState.name}
onChange={name => setFormState({ ...formState, name })}
name="someName"/>
</FieldLabel>
<FieldLabel label='Description'>
<Textarea
value={formState.description}
onChange={description => setFormState({ ...formState, description}}
name="someDesc"/>
</FieldLabel>
This form is 'dumb' meaning it gets its formState from a parent and dispatches setFormState when input is changed.
In the parent component it looks like:
<MyForm
formState={formState}
setFormState={setFormState} />
where formState and setFormState are hooks:
const initialFormState: FormState = {
name: '',
description: '',
}
const [formState, setFormState] = useState<FormState>({ ...initialFormState });
However, the state does not change, nor am I able to write anything into the inputs on the Form. When I click submit the initial state is logged, and all the properties are empty strings, just like in the initialFormState.
If anyone needs more info I'd be happy to provide it. Thanks.
Adding a codesandbox link: https://codesandbox.io/s/admiring-haze-gsy59?file=/src/App.js

onChange on input does not emit the value, it emits an event.
Replace with onChange={e => setFormState({ ...formState, name: e.target.value })}

Related

Validating a child input type file with react-hook-form and Yup

I'm creating a form with a file upload with help of react-hook-form and Yup. I am trying to use the register method in my child component. When passing register as a prop (destructured in curly braces) the validation and submiting doesn't work. You can always submit the form and the submitted file object is empty.
Here's a sandbox link.
There are several of problems with your code.
1- register method returns an object with these properties:
{
onChange: function(){},
onBlur:function{},
ref: function(){}
}
when you define your input like this:
<input
{...register('photo')}
...
onChange={(event) => /*something*/}
/>
actually you are overrding the onChange method which has returned from register method and react-hook-form couldn't recognize the field change event. The solution to have your own onChange alongside with react-hook-form's onChange could be something like this:
const MyComp = ()=> {
const {onChange, ...registerParams} = register('photo');
...
return (
...
<input
{...params}
...
onChange={(event) => {
// do whatever you want
onChange(event)
}}
/>
);
}
2- When you delete the photo, you are just updating your local state, and you don't update photo field, so react-hook-form doesn't realize any change in your local state.
the problems in your ImageOne component could be solved by change it like this:
function ImageOne({ register, errors }) {
const [selectedImage, setSelectedImage] = useState(null);
const { onChange, ...params } = register("photo");
return (
...
<Button
className="delete"
onClick={() => {
setSelectedImage(null);
//Make react-hook-form aware of changing photo state
onChange({ target: { name: "photo", value: [] } });
}}
>
...
<input
//name="photo"
{...params}
type="file"
accept="image/*"
id="single"
onChange={(event) => {
setSelectedImage(event.target.files[0]);
onChange(event); // calling onChange returned from register
}}
/>
...
);
}
3- Since your input type is file so, the value of your photo field has length property that you can use it to handle your validation like this:
const schema = yup.object().shape({
photo: yup
.mixed()
.test("required", "photo is required", value => value.length > 0)
.test("fileSize", "File Size is too large", (value) => {
return value.length && value[0].size <= 5242880;
})
.test("fileType", "Unsupported File Format", (value) =>{
return value.length && ["image/jpeg", "image/png", "image/jpg"].includes(value[0].type)
}
)
});
Here is the full edited version of your file.

Is there a way to stop useMutation from setting `data` to `undefined` before updating it to a new value?

I have a mock mutation like so:
interface Person {
firstName: string;
lastName: string;
}
async function sendPersonApi({ firstName, lastName }: Person) {
await new Promise((res) => setTimeout(res, 1000));
return {
firstName,
lastName,
status: "success"
};
}
I have two components: <Form /> and <Output />. I basically want the mutation to run on form submit of the <Form />, and then show the result of that mutation in the <Output />.
I have a CodeSandbox with this behavior mostly working: https://codesandbox.io/s/long-worker-k350q?file=/src/App.tsx
Form
const personAtom = atom<Person>({
firstName: "",
lastName: ""
});
function Form() {
const [formState, setFormState] = useAtom(personAtom);
const handleSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(event) => {
event.preventDefault();
const formElement = event.currentTarget;
const formData = new FormData(formElement);
const firstName = formData.get("firstName") as string;
const lastName = formData.get("lastName") as string;
setFormState({ firstName, lastName }); // This does not update the `data`/`isLoading`/`isError` in the <Output /> component
},
[setFormState]
);
return (
<form id="name-form" onSubmit={handleSubmit}>
<input name="firstName" /> <br />
<input name="lastName" /> <br />
<button>Submit</button>
</form>
);
}
Output
function Output() {
const [person] = useAtom(personAtom);
const { mutate, data, isLoading, isError } = useMutation(sendPersonApi, {
mutationKey: "sendPerson"
});
useEffect(() => {
mutate(person);
}, [person]);
return (
<output name="name-output" form="name-form">
<p>data: {JSON.stringify(data)}</p>
<p>isLoading: {JSON.stringify(isLoading)}</p>
<p>isError: {JSON.stringify(isError)}</p>
</output>
);
}
Initially, I had wanted to implement this by doing the mutation itself inside the <Form /> submit handler, but it seems like I was not be able to access the data/isLoading/isError in a different component even if I used the same mutationKey in useMutation.
So I ended up changing the implementation so that the submit handler just updates some global state (using jotai atoms), and then in the Output I have an effect that listens for changes to this global state and then calls mutate().
The problem now though is that the data always gets reset to undefined in every subsequent submit before getting a new value. Is there a way to stop useMutation from setting data to undefined before updating it to a new value?
Also, is there a cleaner way of using the result of a mutation in another component? I'd prefer to have the mutate() done inside the Form and then somehow use the data/isLoading/isError in a different component without having to rely on useEffect.
I think I'm going to just create a separate atom to hold the global state for the current data. Then I'll use the onSuccess of the useMutation to update this global state from within <Form /> and then just use that global state inside <Output />.

React useState hook - when to use previous state when updating state?

In the code below, the form is not updated correctly unless I pass in previous state to the state setting call ('setForm').
To see it in action, run the code as-is, and the console will print 'true', indicating it worked. That's using 'validate1' function. If you replace that with 'validate2' (which doesn't use previous state), it fails to print 'true'.
It seems like in validate2, the second setForm call overwrites the form.long state of the first setForm call, due to the async nature of these calls. But why does validate1 work? Why does using previous state cause it to work? Any documentation on this behavior would be really helpful.
(I can make validate2 work by having one setForm that sets both fields, but the code below is contrived to show a difference in the two ways setForm can be called, with and without previous state.)
CodeSandbox
const {useState, useEffect} = React;
function LoginForm() {
const [form, setForm] = useState({
username: "",
password: "",
long: null
});
useEffect(() => {
console.log(form.long);
}, [form.long]);
const validate1 = (e) => {
e.preventDefault();
setForm((prevState) => ({
...prevState,
long: form.password.length >=3 ? true : false
}));
setForm((prevState) => ({
...prevState,
username: "*****"
}));
};
const validate2 = (e) => {
e.preventDefault();
setForm({
...form,
long: form.password.length >=3 ? true : false
});
setForm({
...form,
username: "*****"
});
};
const updateField = (e) => {
setForm({
...form,
[e.target.name]: e.target.value
});
};
return (
<form onSubmit={validate1}>
<label>
Username:
<input value={form.username} name="username" onChange={updateField} />
</label>
<br />
<label>
Password:
<input
value={form.password}
name="password"
type="password"
onChange={updateField}
/>
</label>
<br />
<button>Submit</button>
</form>
);
}
// Render it
ReactDOM.render(
<LoginForm/>,
document.getElementById("react")
);
<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="react"></div>
In the React documentation, they mention the following,
If the next state depends on the current state, we recommend using the
updater function form, instead:
this.setState((state) => {
return {quantity: state.quantity + 1};
});
That's the reason validate1 works, because the second call to setForm depends on the previous setForm call state, and the state will be up to date if the updater function form is used.
Why does validate1 work though? Why does using previous state cause it to work?
setForm({
...form,
long: form.password.length >=3 ? true : false
});
setForm({
...form,
username: "*****"
});
The value of form in the second setForm is still the old value until the next re-render. It doesn't reflect the updated form from the previous setForm.
React setState and useState does not make changes directly to the state object.
setState and useState create queues for React core to update the state object of a React component.
So the process to update React state is asynchronous for performance reasons. That’s why changes don’t feel immediate.
https://linguinecode.com/post/why-react-setstate-usestate-does-not-update-immediately

How to store value in one variable outside the component using reactjs

I have went through this link "How to get only one value by selecting and switching to any of the option using reactjs?". I have similar component where i want to store either radio button value or Checkbox values in one variable which is in the App component. But i didn't find any solution to it. Can anyone help me in this? So that it would be useful for me and for the person who posted.
In App component:
const [value, setValue] = useState({
radioValue: '',
checkBoxValue: '',
});
const handleChangeInput = e => {
setValue({
...value,
[e.target.name]: e.target.value,
});
};
In Child component:
<input
type="radio"
name="radioValue"
value={value}
onChange={e => handleInputChange(e)}
/>

handle onChange using react custom component with formik

I use custom react component with formik which is handled by sending the value where I use it and setting its own state from its parent, so onChange I am always set its react state besides setFieldvalue to set it in formik as I dont use handleChange from formik props.
<Field
render={(fields: { field: object; form: formProps }) => {
return (
<TextField
name="title"
error={errors.title && touched.title}
value={title}
onKeyUp={() => null}
onBlur={handleBlur}
onChange={(e: { target: { value: string } }) =>
this.props.onChange('title', e, fields.form)
}
placeholder="e.g Introduction to UX Design"
/>
);
}}
/>
onChange = (
stateField: string,
e: { target: { value: string } },
form: { setFieldValue: (field: string, value: string) => void }
) => {
// the field is controlled twice here
form.setFieldValue(stateField, e.target.value);
this.setState({ [stateField]: e.target.value });
};
it is working correct but it is a hassle for me to handle the two cases in each field and I am not feeling it is the best way to do so, any help ?
Why do you need to control the value twice? Can you not lift it out of the form state when required, rather than storing in the component state? If you explain the use case more, I think someone will suggest a better way of tackling the problem.

Resources