React Hook Form V7 - Material UI 5 Autocomplete: Lazy loaded values not validated - reactjs

I use react-hook-form V7 with an Material UI 5 Autocomplete input. My lazy loaded values in the Autocomplete are not validated on first submit click (but on second). What's wrong?
useEffect(() => {
setItemList([
{ id: 1, name: "item1" },
{ id: 2, name: "item2" }
]);
setItemIdSelected(2);
}, []);
[...]
<Autocomplete
options={itemList}
getOptionLabel={(item) => (item.name ? item.name : "")}
getOptionSelected={(option, value) =>
value === undefined || value === "" || option.id === value.id
}
value={
itemIdSelected
? itemList.find((item) => item.id === itemIdSelected)
: ""
}
onChange={(event, items) => {
if (items !== null) {
setItemIdSelected(items.id);
}
}}
renderInput={(params) => (
<TextField
{...params}
{...itemsIdFormHookRest}
label="items"
margin="normal"
variant="outlined"
inputRef={itemsIdFormHookRef}
error={errors.itemsId ? true : false}
helperText={errors.itemsId && "item required"}
required
/>
)}
/>
[...]
Please have a look to the code on code sandbox:

You should wrap the Material UI Autocomplete with the Controller Component provided by React Hook Form. See this section in the documentation for further information.
I would also suggest to remove the useState saving the id, as you could also access the value via React Hook Form either through getValues or after the handleSubmit callback is called.
Here is the updated CodeSandbox.

How can i implement datepickers?
This works only with modal, but when typing yourself is not working.
<LocalizationProvider dateAdapter={AdapterDateFns} locale={ruLocale}>
<Controller
render={({ field }) => (
<DatePicker
value={field.value}
onChange={(value) => field.onChange(value)}
renderInput={(params) => (
<TextField {...params} />
)}
/>
)}
name={name}
control={control}
/>
</LocalizationProvider>

I handle react-hook-form with mui Autocomplete in the following way
"devDependencies": {
"#mui/material": "^5.8.3",
"react-hook-form": "^7.33.1",
}
const Country = [{ label: 'United Kingdem', value: 'uk' }];
<Controller
name={"country"}
control={control}
rules={{
required: true,
}}
render={({ field: { onChange, ..._field } }) => (
<Autocomplete
options={Country}
onChange={(_, data) => onChange(data.value)}
renderInput={(params) => {
return (
<TextField
{...params}
label="disableCloseOnSelect"
variant="standard"
/>
);
}}
{..._field}
/>
)}
/>

Related

React hook form Material Autocomplete don't match option to value on fetching data

I have a problem that after fetching my categories and business entity my autocomplete don't match value preview for Material Autocomplete.
I am using react hook forms and in Autoselect I am trying to save only id instead of the whole object.
How can I customize isOptionEqualToValue to show preview that
my id ("ect345-8") equals to one of the option {id: "ect345-8", title: "main"}
<Controller
control={control}
name="category_id"
render={({ field: { ref, onChange } }: any) => (
<Autocomplete
options={categories}
isOptionEqualToValue={(option: any, value: any) =>
option.id === value.id
}
onChange={(_, data: any) => onChange(data.id)}
getOptionLabel={(option) => `${option.title}`}
renderInput={(params) => (
<TextField
{...params}
placeholder="Choose category"
inputRef={ref}
variant="outlined"
name="category_id"
/>
)}
/>
)}
/>

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}
/>
)}
/>
)}

Set current location as default value in TextField Autocomplete Material UI

How to set defaultValue to current location into TexField inside AutoComplete form from Material UI.
I have fetched current location using Geolocaction
navigator.geolocation.getCurrentPosition(function(position) {
console.log("position",position);
console.log("Latitude is :", position.coords.latitude);
console.log("Longitude is :", position.coords.longitude);
// Get address from latidude & longitude.
Geocode.fromLatLng(position.coords.latitude, position.coords.longitude).then(
response => {
setAddress(response.results[0].formatted_address);
},
error => {
console.error(error);
}
);
});
I am using autocomplete form from MATERIAL UI. I have tried to write defaultValue={addresss} but nothing happens.
<Autocomplete
id="google-map-demo"
getOptionLabel={(option) =>
typeof option === "string" ? option : option.description
}
filterOptions={(x) => x}
options={options}
onChange={(e, v) => {
user.setFulladdress(v.description);
getPlace(v.place_id);
}}
renderInput={(params) => (
<TextField
{...params}
placeholder="Address"
defaultValue={address}
InputProps={{
...params.InputProps,
classes: {
input: classes.resize,
},
disableUnderline: true,
}}
variant="standard"
fullWidth
onChange={(e, v) => {
setInputValue(e.target.value);
}}
/>
Anyone idea how to put default value as current location of user?

A component is changing an uncontrolled Autocomplete to be controlled

Can you tell me that why I'm getting error "A component is changing an uncontrolled Autocomplete to be controlled.
Elements should not switch from uncontrolled to controlled (or vice versa).
Decide between using a controlled or uncontrolled Autocomplete element for the lifetime of the component."
component :
function AutoComplete(props) {
const defaultProps = {
options: props.options,
getOptionLabel: option => option.name,
};
const handleChange = (e, value) => {
props.onChange(value);
};
return (
<Autocomplete
{...defaultProps}
renderInput={params => (
<TextField {...params} label={props.label} margin="normal" />
)}
onChange={handleChange}
value={props.value}
/>
);
}
calling autocomplte:
<Controller
control={control}
name = 'country'
as = {
<AutoComplete
options={countryOptions}
onChange={selectCountryHandler}
label="Country"
value={selectedCountry || ''}
/>
} />
how can I solve this error?
You ensured that the value property never had been undefined, but you had to do same for inputValue.
the "value" state with the value/onChange props combination. This state represents the value selected by the user, for instance when pressing Enter.
the "input value" state with the inputValue/onInputChange props combination. This state represents the value displayed in the textbox.
⚠️ These two state are isolated, they should be controlled independently.
Component becomes uncontrolled when inputValue property is undefined, and vice versa.
If in the following example you delete an empty string from
React.useState('') you'll get the same error message because inputValue during first render is undefined.
import React from 'react'
import TextField from '#material-ui/core/TextField'
import Autocomplete from '#material-ui/lab/Autocomplete'
const options = ['Option 1', 'Option 2']
export default function AutocompleteLab() {
const [value, setValue] = React.useState(options[0])
const [inputValue, setInputValue] = React.useState('')
return (
<div>
<div>{`value: ${value !== null ? `'${value}'` : 'null'}`}</div>
<div>{`inputValue: '${inputValue}'`}</div>
<br />
<Autocomplete
value={value}
onChange={(_, newValue) => {
setValue(newValue)
}}
inputValue={inputValue}
onInputChange={(_, newInputValue) => {
setInputValue(newInputValue)
}}
options={options}
style={{ width: 300 }}
renderInput={(params) => <TextField {...params} label="Name" variant="outlined" />}
/>
</div>
)
}
When no value is selected, you need to add || null to prevent the Autocomplete going into uncontrolled mode:
<Autocomplete {...props} value={props.value || null} />
If you pass value={undefined} to the Autocomplete component, it will start in "uncontrolled" mode, meaning it keeps its own internal state. Then if you later supply a value it raises the "A component is changing" error. But if you pass value={null}instead of value={undefined} that causes the Autocomplete to start in controlled mode. The Autocomplete will assume you will be providing the state, not keep its own, and the error goes away.
I solved this by removing the default value.
<Autocomplete
multiple
id="multiple-limit-tags"
options={(option) => option.label}
getOptionLabel={(option) => option}
// defaultValue={options || []}
renderInput={(params) => <TextField {...params} label="My Label" />}
/>
It wasn't obvious how to solve this, and the documentation doesn't help much either. I find it curious that a copy-pasted example from the documentation results in this error. I guess the example works because the choices are hard-coded.
Previous answer was absolutely correct, BUT I spend 20 minutes while I figure out that inputValue it should be value
So working example from me:
export default function AddModal(): ReactElement {
const [resource, setResource] = useState('one');
<Autocomplete
id="autocomplete"
options={['one', 'two']}
defaultValue={resource}
value={resource}
PopperComponent={StyledPopper}
onChange={(event, newInputValue) => setResource(newInputValue)}
renderInput={(params) => <TextField {...params} />}
/>
I had the same issue today, but I was able to solve it by providing a default value of null as well as providing a null value in case of it not existing. I'll leave the code that worked for me:
<Autocomplete
value={filters.tag || null}
defaultValue={null}
options={tags || []}
getOptionLabel={(option) => option}
renderInput={(params) => (
<TextField {...params} label="Search" variant='outlined' size='small' />
)}
fullWidth
onChange={(event, value) => {
if (value) {
setFilters({ ...filters, tag: value });
} else {
setFilters({ ...filters, tag: '' });
}
}}
/>
For me I fixed this issue by updated the onChange function and add || null rather then removing the default value since I still need it here is the code
<Box mt={2}>
<Controller
control={control}
name="thematic"
rules={{
required: 'Veuillez choisir une réponse',
}}
render={({ field: { onChange } }) => (
<Autocomplete
defaultValue={
questionData?.thematic ? questionData?.thematic : null
}
options={thematics}
getOptionLabel={(option) => option.name}
onChange={(event, values) => {
onChange(values || null)
}}
renderInput={(params) => (
<TextField
{...params}
label="Thématique"
placeholder="Thématique"
helperText={errors.thematic?.message}
error={!!errors.thematic}
/>
)}
/>
)}
/>
</Box>
For the autocomplete with options which is a simple array without object you can simply do it that way and u want get any issue
<Box mt={2}>
<Controller
control={control}
name="type"
rules={{
required: 'Veuillez choisir une réponse',
}}
render={({ field: { onChange, value } }) => (
<Autocomplete
freeSolo
options={['Champ', 'Sélection', 'Choix multiple']}
onChange={(event, values) => onChange(values)}
value={value}
renderInput={(params) => (
<TextField
{...params}
label="Type de la question"
variant="outlined"
onChange={onChange}
helperText={errors.type?.message}
error={!!errors.type}
/>
)}
/>
)}
/>
</Box>
the default value you can set it within useForm if you are using react-hook-form
const {
handleSubmit,
control,
watch,
register,
formState: { errors },
} = useForm({
defaultValues: {
...
type: questionData?.type ? mapperQuestionType[questionData?.type] : '',
},
})

Proper way to use react-hook-form Controller with Material-UI Autocomplete

I am trying to use a custom Material-UI Autocomplete component and connect it to react-hook-form.
TLDR: Need to use MUI Autocomplete with react-hook-form Controller without defaultValue
My custom Autocomplete component takes an object with the structure {_id:'', name: ''} it displays the name and returns the _id when an option is selected. The Autocomplete works just fine.
<Autocomplete
options={options}
getOptionLabel={option => option.name}
getOptionSelected={(option, value) => option._id === value._id}
onChange={(event, newValue, reason) => {
handler(name, reason === 'clear' ? null : newValue._id);
}}
renderInput={params => <TextField {...params} {...inputProps} />}
/>
In order to make it work with react-hook-form I've set the setValues to be the handler for onChange in the Autocomplete and manually register the component in an useEffect as follows
useEffect(() => {
register({ name: "country1" });
},[]);
This works fine but I would like to not have the useEffect hook and just make use of the register somehow directly.
Next I tried to use the Controller component from react-hook-form to proper register the field in the form and not to use the useEffect hook
<Controller
name="country2"
as={
<Autocomplete
options={options}
getOptionLabel={option => option.name}
getOptionSelected={(option, value) => option._id === value._id}
onChange={(event, newValue, reason) =>
reason === "clear" ? null : newValue._id
}
renderInput={params => (
<TextField {...params} label="Country" />
)}
/>
}
control={control}
/>
I've changed the onChange in the Autocomplete component to return the value directly but it doesn't seem to work.
Using inputRef={register} on the <TextField/> would not cut it for me because I want to save the _id and not the name
HERE is a working sandbox with the two cases. The first with useEffect and setValue in the Autocomplete that works. The second my attempt in using Controller component
Any help is appreciated.
LE
After the comment from Bill with the working sandbox of MUI Autocomplete, I Managed to get a functional result
<Controller
name="country"
as={
<Autocomplete
options={options}
getOptionLabel={option => option.name}
getOptionSelected={(option, value) => option._id === value._id}
renderInput={params => <TextField {...params} label="Country" />}
/>
}
onChange={([, { _id }]) => _id}
control={control}
/>
The only problem is that I get an MUI Error in the console
Material-UI: A component is changing the uncontrolled value state of Autocomplete to be controlled.
I've tried to set an defaultValue for it but it still behaves like that. Also I would not want to set a default value from the options array due to the fact that these fields in the form are not required.
The updated sandbox HERE
Any help is still very much appreciated
The accepted answer (probably) works for the bugged version of Autocomplete. I think the bug was fixed some time after that, so that the solution can be slightly simplified.
This is very useful reference/codesandbox when working with react-hook-form and material-ui: https://codesandbox.io/s/react-hook-form-controller-601-j2df5?
From the above link, I modified the Autocomplete example:
import TextField from '#material-ui/core/TextField';
import Autocomplete from '#material-ui/lab/Autocomplete';
const ControlledAutocomplete = ({ options = [], renderInput, getOptionLabel, onChange: ignored, control, defaultValue, name, renderOption }) => {
return (
<Controller
render={({ onChange, ...props }) => (
<Autocomplete
options={options}
getOptionLabel={getOptionLabel}
renderOption={renderOption}
renderInput={renderInput}
onChange={(e, data) => onChange(data)}
{...props}
/>
)}
onChange={([, data]) => data}
defaultValue={defaultValue}
name={name}
control={control}
/>
);
}
With the usage:
<ControlledAutocomplete
control={control}
name="inputName"
options={[{ name: 'test' }]}
getOptionLabel={(option) => `Option: ${option.name}`}
renderInput={(params) => <TextField {...params} label="My label" margin="normal" />}
defaultValue={null}
/>
control is from the return value of useForm(}
Note that I'm passing null as defaultValue as in my case this input is not required. If you'll leave defaultValue you might get some errors from material-ui library.
UPDATE:
Per Steve question in the comments, this is how I'm rendering the input, so that it checks for errors:
renderInput={(params) => (
<TextField
{...params}
label="Field Label"
margin="normal"
error={errors[fieldName]}
/>
)}
Where errors is an object from react-hook-form's formMethods:
const { control, watch, errors, handleSubmit } = formMethods
So, I fixed this. But it revealed what I believe to be an error in Autocomplete.
First... specifically to your issue, you can eliminate the MUI Error by adding a defaultValue to the <Controller>. But that was only the beginning of another round or problems.
The problem is that functions for getOptionLabel, getOptionSelected, and onChange are sometimes passed the value (i.e. the _id in this case) and sometimes passed the option structure - as you would expect.
Here's the code I finally came up with:
import React from "react";
import { useForm, Controller } from "react-hook-form";
import { TextField } from "#material-ui/core";
import { Autocomplete } from "#material-ui/lab";
import { Button } from "#material-ui/core";
export default function FormTwo({ options }) {
const { register, handleSubmit, control } = useForm();
const getOpObj = option => {
if (!option._id) option = options.find(op => op._id === option);
return option;
};
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<Controller
name="country"
as={
<Autocomplete
options={options}
getOptionLabel={option => getOpObj(option).name}
getOptionSelected={(option, value) => {
return option._id === getOpObj(value)._id;
}}
renderInput={params => <TextField {...params} label="Country" />}
/>
}
onChange={([, obj]) => getOpObj(obj)._id}
control={control}
defaultValue={options[0]}
/>
<Button type="submit">Submit</Button>
</form>
);
}
import { Button } from "#material-ui/core";
import Autocomplete from "#material-ui/core/Autocomplete";
import { red } from "#material-ui/core/colors";
import Container from "#material-ui/core/Container";
import CssBaseline from "#material-ui/core/CssBaseline";
import { makeStyles } from "#material-ui/core/styles";
import TextField from "#material-ui/core/TextField";
import AdapterDateFns from "#material-ui/lab/AdapterDateFns";
import LocalizationProvider from "#material-ui/lab/LocalizationProvider";
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
export default function App() {
const [itemList, setItemList] = useState([]);
// const classes = useStyles();
const {
control,
handleSubmit,
setValue,
formState: { errors }
} = useForm({
mode: "onChange",
defaultValues: { item: null }
});
const onSubmit = (formInputs) => {
console.log("formInputs", formInputs);
};
useEffect(() => {
setItemList([
{ id: 1, name: "item1" },
{ id: 2, name: "item2" }
]);
setValue("item", { id: 3, name: "item3" });
}, [setValue]);
return (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<Container component="main" maxWidth="xs">
<CssBaseline />
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<Controller
control={control}
name="item"
rules={{ required: true }}
render={({ field: { onChange, value } }) => (
<Autocomplete
onChange={(event, item) => {
onChange(item);
}}
value={value}
options={itemList}
getOptionLabel={(item) => (item.name ? item.name : "")}
getOptionSelected={(option, value) =>
value === undefined || value === "" || option.id === value.id
}
renderInput={(params) => (
<TextField
{...params}
label="items"
margin="normal"
variant="outlined"
error={!!errors.item}
helperText={errors.item && "item required"}
required
/>
)}
/>
)}
/>
<button
onClick={() => {
setValue("item", { id: 1, name: "item1" });
}}
>
setValue
</button>
<Button
type="submit"
fullWidth
size="large"
variant="contained"
color="primary"
// className={classes.submit}
>
submit
</Button>
</form>
</Container>
</LocalizationProvider>
);
}
I do not know why the above answers did not work for me, here is the simplest code that worked for me, I used render function of Controller with onChange to change the value according to the selected one.
<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}
/>
)}
/>
)}
Thanks to all the other answers, as of April 15 2022, I was able to figure out how to get this working and render the label in the TextField component:
const ControlledAutocomplete = ({
options,
name,
control,
defaultValue,
error,
rules,
helperText,
}) => (
<Controller
name={name}
control={control}
defaultValue={defaultValue}
rules={rules}
render={({ field }) => (
<Autocomplete
disablePortal
options={options}
getOptionLabel={(option) =>
option?.label ??
options.find(({ code }) => code === option)?.label ??
''
}
{...field}
renderInput={(params) => (
<TextField
{...params}
error={Boolean(error)}
helperText={helperText}
/>
)}
onChange={(_event, data) => field.onChange(data?.code ?? '')}
/>
)}
/>
);
ControlledAutocomplete.propTypes = {
options: PropTypes.arrayOf({
label: PropTypes.string,
code: PropTypes.string,
}),
name: PropTypes.string,
control: PropTypes.func,
defaultValue: PropTypes.string,
error: PropTypes.object,
rules: PropTypes.object,
helperText: PropTypes.string,
};
In my case, options is an array of {code: 'US', label: 'United States'} objects. The biggest difference is the getOptionLabel, which I guess needs to account for if both when you have the list open (and option is an object) and when the option is rendered in the TextField (when option is a string) as well as when nothing is selected.
I have made it work pretty well including multiple tags selector as follow bellow. It will work fine with mui5 and react-hook-form 7
import { useForm, Controller } from 'react-hook-form';
import Autocomplete from '#mui/material/Autocomplete';
//setup your form and control
<Controller
control={control}
name="yourFiledSubmitName"
rules={{
required: 'required field',
}}
render={({ field: { onChange } }) => (
<Autocomplete
multiple
options={yourDataArray}
getOptionLabel={(option) => option.label}
onChange={(event, item) => {
onChange(item);
}}
renderInput={(params) => (
<TextField {...params} label="Your label" placeholder="Your placeholder"
/>
)}
)}
/>
Instead of using controller, with the help of register, setValue of useForm and value, onChange of Autocomplete we can achieve the same result.
const [selectedCaste, setSelectedCaste] = useState([]);
const {register, errors, setValue} = useForm();
useEffect(() => {
register("caste");
}, [register]);
return (
<Autocomplete
multiple
options={casteList}
disableCloseOnSelect
value={selectedCaste}
onChange={(_, values) => {
setSelectedCaste([...values]);
setValue("caste", [...values]);
}}
getOptionLabel={(option) => option}
renderOption={(option, { selected }) => (
<React.Fragment>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
{option}
</React.Fragment>
)}
style={{ width: "100%" }}
renderInput={(params) => (
<TextField
{...params}
id="caste"
error={!!errors.caste}
helperText={errors.caste?.message}
variant="outlined"
label="Select caste"
placeholder="Caste"
/>
)}
/>
);

Resources