react-hook-form(V7): How to pass a function via onChange to a nested Material UI component - reactjs

I am have a Material UI nested component that look as follow:
imports . . .
const TxtInput = ({ name, value, label, required }) => {
const { control, ...rest } = useFormContext()
return (
<Controller
name={name}
defaultValue={value}
control={control}
render={({
field: { onChange, onBlur, value, name, ref }
}) =>
<TextField
required={required}
fullWidth
label={label}
id={name}
inputProps={{ 'aria-label': label }}
onBlur={onBlur}
onChange={onChange}
checked={value}
inputRef={ref}
{...rest}
/>}
/>
)
}
export default TxtInput
While in my app.js, <TxtInput /> look like this:
<FormProvider {...methods}>
<form onSubmit={handleSubmit(submit)}>
<TxtInput
name='fullName'
label='First and last name'
required
value=''
onChange={() => console.log('hello')}
</form>
</FormProvider>
And I am expecting to see 'Hello' with every keystroke in my console but, that is not the case.
Does anyone know why?

I think what you want is to pass the onChange event to the TxtInput instead of using its own Controller onChange
const TxtInput = ({ name, value, label, required, onChange }) => { // add onChange here
const { control, ...rest } = useFormContext()
return (
<Controller
name={name}
defaultValue={value}
control={control}
render={({
field: { onBlur, value, name, ref } // remove onChange here to allow pass though from parent onChange
}) =>
<TextField
required={required}
fullWidth
label={label}
id={name}
inputProps={{ 'aria-label': label }}
onBlur={onBlur}
onChange={onChange}
checked={value}
inputRef={ref}
{...rest}
/>}
/>
)
}
make a codesandbox to simulate your case as well. You can check it out
https://codesandbox.io/s/runtime-hill-9q2qu?file=/src/App.js

The last nested onChanged function should be returned as ()=>onChangeFn

Related

How to trigger re-render with `setValue` using react-hook-form?

I have a simple form with a select field, it's react-hook-form for validation and everything. There's a Controller which renders a Material UI Select. I would like to set the value of such select using setValue from outside the component (in the root of the form, where all controls reside).
This is the piece of code I'm using (slightly simplified not to waste too much of your time)
type Props = {
name: string;
control: Control<any>;
options: SelectOptions[];
};
const Select: FunctionComponent<Props> = ({
name,
control,
options,
}) => (
<Controller
control={control}
name={name}
render={({ field: { onChange, value } }) => {
return (
<FormControl>
<MuiSelect onChange={onChange}>
{options.map((o) => (
<MuiSelectItem key={o.key} value={o.value}>{o.label}</MuiSelectItem>
))}
</MuiSelect>
</FormControl>
)
}}
/>
);
As far as changing the value of the select, setValue works magically. When I feed a new value, it works as intended. The problem (I guess) is the component is not re-rendered, so the old value is still shown. I'm not sure how to fix this thing and docs did not help a lot. Any help is much appreciated, thanks!
As #knoefel said I tried setting the defaultValue="" but doesn't work for me(maybe because I am using FormProvider). So what I did is use watch instead of value
<Controller
name='name'
control={control}
render={({ field: { onChange } }) => (
<Select
dropdownValues={dropdownValues}
value={watch('name')}
onChange={onChange}
/>
)}
/>
I think you just forgot to set the value prop of <Controller /> to your <MuiSelect />. You also have to set a defaultValue for your <Controller /> field, either via the defaultValue prop or via useForm.
<Controller
control={control}
name={name}
defaultValue=""
render={({ field: { onChange, value } }) => {
return (
<FormControl>
<MuiSelect onChange={onChange} value={value}>
{options.map((o) => (
<MuiSelectItem key={o.key} value={o.value}>{o.label}</MuiSelectItem>
))}
</MuiSelect>
</FormControl>
)
}}
/>

How To implement ReactFlagsSelect with React Hook Form Controller

I tried to implement with React Hook From using <Controller />. While I am submitting the form country field return undefined.
<Controller
name="country"
control={control}
render={({ field: { onChange, value } }) => (
<ReactFlagsSelect
selected={selected}
onSelect={code => handleChange(code)}
value={value}
onChange={onChange}
/>
)}
/>
The problem is you pass RHF's onChange handler to the wrong prop. <ReactFlagsSelect /> doesn't have a onChange prop, instead you should pass it to the onSelect prop.
<Controller
name="country"
control={control}
render={({ field: { onChange, value } }) => (
<ReactFlagsSelect
selected={selected}
onSelect={onChange}
value={value}
/>
)}
/>
Side-note: RHF's will update value changes to your registered fields in it's internal form state, so there is no need to use extra state management for these values via useState or something similar. If you really need to call your handleChange callback, you can do both in the onSelect callback.
<Controller
name="country"
control={control}
render={({ field: { onChange, value } }) => (
<ReactFlagsSelect
selected={selected}
onSelect={code => {
onChange(code);
handleChange(code);
}}
value={value}
/>
)}
/>

mui autocomplete with react-hook-form: defaultValue in formData

I'm setting the defaultValue of the Autocopmlete component as below:
<Controller
control={control}
name={name}
render={({field: {onChange, value}}) => (
<Autocomplete
freeSolo={freeSolo}
options={options}
renderInput={params => {
return <TextField {...params} label={label} margin="normal" variant="outlined" onChange={onChange} />
}}
onChange={(event, values, reason) => onChange(values)}
defaultValue={defaultValue}
/>
)}
/>
the value is well displayed based on the defaultValue.
However when I click on the submit button, the value of the Autocomplete field is always undefined if I don't use the autocomplete component.
Here are how I register the hook and component (simplified code)
const customerSchema = yup.object().shape({
route: yup.string().nullable()
})
const {control, handleSubmit} = useForm({
resolver: yupResolver(customerSchema)
})
const onSubmit = formData => {
console.log(formData) // autocomplete is undefined if no action on the autocomplete
}
<form noValidate autoComplete="off" onSubmit={handleSubmit(onSubmit)}>
<AutoCompleteInput
control={control}
name="route"
label="route"
options={customers_routes.map(option => option.route_name)}
defaultValue={customer.route}
freeSolo={false}
/>
<Button type="submit">
Update
</Button>
</form>
What should I do to have the route field always available in the form data when I click on submit?
Why don't you use defaultValues option with useForm:
const {control, handleSubmit} = useForm({
resolver: yupResolver(customerSchema),
defaultValues: { route: customer.route },
});
and instead of sending defaultValue prop to AutoComplete you can use value prop, like this:
<Controller
control={control}
name={name}
render={({ field: { onChange, value } }) => (
<Autocomplete
freeSolo={freeSolo}
options={options}
renderInput={(params) => {
return (
<TextField
{...params}
label={label}
margin="normal"
variant="outlined"
onChange={onChange}
/>
);
}}
onChange={(event, values, reason) => onChange(values)}
value={value}
/>
)}
/>
Here is a simplest way to use an Autocomplete with react hook from , render your Autocomplete component inside Controller from react hook from, use onChange and value from the render function to control the value
<Controller
control={control}
name="type"
rules={{
required: 'Veuillez choisir une réponse',
}}
render={({ field: { onChange, value } }) => (
<Autocomplete
freeSolo
options={['field', 'select', 'multiple', 'date']}
onChange={(event, values) => onChange(values)}
value={value}
renderInput={(params) => (
<TextField
{...params}
label="type"
variant="outlined"
onChange={onChange}
/>
)}
/>
)}

react-hook-form not triggering onSubmit when using Controller

I have the following form:
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="voucherPrice"
control={control}
defaultValue={false}
rules={{ required: true }}
render={({ field: { onChange, value, ref } }) => (
<Input {...field} onChange={(ev) => handlePriceInputChange(ev)} value={price} type="number" innerRef={ref} />
)}
/>
<p>{errors.voucherPrice?.message}</p>
<Button
variant="contained"
sx={{ mt: 1, mr: 1 }}
type="submit"
>
{"Continue"}
</Button>
</form>
and with this configuration:
function PriceSelection(props) {
const {
register,
handleSubmit,
control,
formState: { errors },
} = useForm({
resolver: yupResolver(schema),
});
const onSubmit = (data) => {
console.log("does not work?", data);
};
const classes = useStylesPriceSelection();
const [selected, setSelected] = useState(false);
const [price, setPrice] = useState("");
const handlePriceInputChange = (ev) => {
console.log("change", price);
setPrice(parseInt(ev.target.value));
};
The function onSubmit does not trigger when I press the submit button. Also I would like the input field to be filled by default by the state price and its value to be sent with the parameter data on the function onSubmit when I push the submit button.
You are mixing useState with react-hook-form and are not updating react-hook-form's internal form state. You don't need to declare a useState for your field.
In your example you are destructering onChange from the field object of <Controller /> but you are never using it for your <Input /> component. Therefore react-hook-form can't update it's form state. As you set your field to be required the onSubmit callback won't get triggered because react-hook-form will never receive an update or value for it.
The correct way would be:
<Controller
name="voucherPrice"
control={control}
rules={{ required: true }}
render={({ field: { ref, onChange, value, ...field } }) => (
<Input {...field} onChange={onChange} value={value} type="number" innerRef={ref} />
)}
/>
Or even shorter:
<Controller
name="voucherPrice"
control={control}
rules={{ required: true }}
render={({ field: { ref, ...field } }) => (
<Input {...field} type="number" innerRef={ref} />
)}
/>
UPDATE
If you need to have access to the value outside of the <Controller />, you should use react-hook-form's watch method. This will allow you to subscribe to the latest value of the voucherPrice field and use it inside your component -> Docs
If you want to set or update the value programmatically you can use the setValue method from react-hook-form -> Docs
const { control, handleSubmit, watch, setValue } = useForm();
const voucherPrice = watch("voucherPrice");
const onButtonClick = () => {
setValue("voucherPrice", <newValue>);
}
If you really need to have a separate useState for your value and want to update it additionally to your react-hook-form field update, you could do the following:
<Controller
name="voucherPrice"
control={control}
rules={{ required: true }}
render={({ field: { ref, onChange, value, ...field } }) => (
<Input
{...field}
onChange={(v) => {
onChange(v);
handlePriceInputChange(v);
}}
value={value}
type="number"
innerRef={ref}
/>
)}
/>
But i would suggest to use the react-hook-form only solution, as it has all the functionality you need to manage your form state.

Register third party- custom component with react-hook-form

I am using react-hook-form and using third party DatePicker. Since it's a custom component using it as a controlled component to register it. This works fine
<Controller
control={control}
name="reviewStartDate"
render={({ field: { onChange, onBlur, value } }) => (
<DatePicker
className={`form-control ${errors.reviewStartDate ? 'is-invalid' : ''}`}
customInput={<input />}
wrapperClassName="datePicker"
onChange={onChange}
onBlur={onBlur}
selected={value ? new Date(value) : ''}
dateFormat='dd-MMM-yyyy'
/>
)}
/>
Similarly/however, I am using thirdparty Multiselect. Here the value is not being registered. It does show the selected value but when I submit the form the value is not present in data.
<Controller
control={control}
name="rootCauseAnalysisCategory"
render={({ field: { value } }) => (
<Multiselect
options={rootCauseAnalysisCategorys}
isObject={false}
showCheckbox={true}
hidePlaceholder={true}
closeOnSelect={false}
selectedValues={value}
/>
)}
/>
Similarly
The <MultiSelect /> component has onSelect and onRemove props, so you can just pass onChange to them. This will work because they both have the signature that the first argument is an array containing the current selected values.
<Controller
control={control}
name="rootCauseAnalysisCategory"
defaultValue={[]}
render={({ field: { value, onChange } }) => (
<Multiselect
options={rootCauseAnalysisCategorys}
isObject={false}
showCheckbox={true}
hidePlaceholder={true}
closeOnSelect={false}
onSelect={onChange}
onRemove={onChange}
selectedValues={value}
/>
)}
/>
UPDATE
If you want to access the current value for rootCauseAnalysisCategory, you have to use watch. Please note, that it is also important to either provide a defaultValue at the <Controller /> field level or call useForm with defaultValues. In the example i passed the defaultValue at the field level.
function App() {
const { control, handleSubmit, watch } = useForm();
const onSubmit = (data) => {
console.log(data);
};
const rootCauseAnalysisCategorys = ["Category 1", "Category 2"];
const rootCauseAnalysisCategory = watch("rootCauseAnalysisCategory");
return (
<div className="App">
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="rootCauseAnalysisCategory"
defaultValue={[]}
render={({ field: { value, onChange } }) => (
<Multiselect
options={rootCauseAnalysisCategorys}
isObject={false}
showCheckbox={true}
hidePlaceholder={true}
closeOnSelect={false}
onSelect={onChange}
onRemove={onChange}
selectedValues={value}
/>
)}
/>
{rootCauseAnalysisCategory?.includes("Category 1") && <p>Category 1</p>}
<input type="submit" />
</form>
</div>
);
}

Resources