How to use Material UI Select with react-hook-form?
Asked Answered
A

11

44

I've built a form in React using Material UI and React Hook Form. I'm trying to create a custom TextField element that works as a Select Input. I would like it to be an uncontrolled component with a Ref prop. I've tried to pass the inputRef prop as the Material UI and React Hook Form docs recommend but with no success.

            <TextField
              id="id"
              name="name"
              select
              native="true"
              className={classes.textField}
              label="label"
              margin="normal"
              variant="outlined"
              inputRef={register({ required: "Choose one option" })}
              error={!!errors.name}
            >
              <MenuItem value="">Choose one option</MenuItem>
              <MenuItem value="3">03</MenuItem>
              <MenuItem value="6">06</MenuItem>
              <MenuItem value="9">09</MenuItem>
              <MenuItem value="12">12</MenuItem>
              <MenuItem value="16">16</MenuItem>
              <MenuItem value="18">18</MenuItem>
            </TextField>

One thing that I've found is that if I use the native select with ref, it works just fine.

Besides, I tried to change the inputRef prop to a SelectProps one but it didn't work too.

Alonaalone answered 3/8, 2020 at 21:6 Comment(1)
Take a look at Controller: react-hook-form.com/api#ControllerPeterus
T
38

Using Select component from Material UI with react hook form need you to implement custom logic with a Controller https://react-hook-form.com/api#Controller

Here is a reusable component that will hopefully simplify the code to use that Select component in your app:

import React from "react";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Select from "@material-ui/core/Select";
import { Controller } from "react-hook-form";

const ReactHookFormSelect = ({
  name,
  label,
  control,
  defaultValue,
  children,
  ...props
}) => {
  const labelId = `${name}-label`;
  return (
    <FormControl {...props}>
      <InputLabel id={labelId}>{label}</InputLabel>
      <Controller
        as={
          <Select labelId={labelId} label={label}>
            {children}
          </Select>
        }
        name={name}
        control={control}
        defaultValue={defaultValue}
      />
    </FormControl>
  );
};
export default ReactHookFormSelect;

You can use it in your app like this:

           <ReactHookFormSelect
              id="numero_prestacao"
              name="numero_prestacao"
              className={classes.textField}
              label="Em quantas parcelas?"
              control={control}
              defaultValue={numero_prestacao || ""}
              variant="outlined"
              margin="normal"
            >
              <MenuItem value="">Escolha uma opção</MenuItem>
              <MenuItem value="3">03 parcelas</MenuItem>
              <MenuItem value="6">06 parcelas</MenuItem>
              <MenuItem value="9">09 parcelas</MenuItem>
              <MenuItem value="12">12 parcelas</MenuItem>
              <MenuItem value="16">16 parcelas</MenuItem>
              <MenuItem value="18">18 parcelas</MenuItem>
            </ReactHookFormSelect>

Here is your codeSandBox updated with this component for the selects in the Information form:

https://codesandbox.io/s/unit-multi-step-form-kgic4?file=/src/Register/Information.jsx:4406-5238

Tangle answered 4/8, 2020 at 12:21 Comment(5)
How do you handle the onChange event?Dunham
In this case we don't have onChange bc the input is handled by the DOM, i.e. an uncontrolled component.Alonaalone
Yes, as @Alonaalone said, react-hook-form works with uncontrolled inputs. If you need the value of an input in your form you can use watch: react-hook-form.com/api#watchTangle
To avoid the following error, you need to make sure you pass a valid defaultValue: Material-UI: A component is changing the uncontrolled value state of Select to be controlled. Elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled Select element for the lifetime of the component.Schlegel
I'm trying to use this with mui v5 + typescript: Type '{ as: Element; name: string; control: any; defaultValue: any; }' is not assignable to type 'IntrinsicAttributes & { render: ({ field, fieldState, formState, }: { field: ControllerRenderProps<FieldValues, string>; fieldState: ControllerFieldState; formState: UseFormStateReturn<...>; }) => ReactElement<...>; } & UseControllerProps<...>'.Importation
B
27

RHF v7 update

Below is a minimal code example of Material UI Select in a RHF form:

const { formState, getValues, watch, register, handleSubmit } = useForm();
const { errors } = formState;
<TextField
  select
  fullWidth
  label="Select"
  defaultValue=''
  inputProps={register('currency', {
    required: 'Please enter currency',
  })}
  error={errors.currency}
  helperText={errors.currency?.message}
>
  {currencies.map((option) => (
    <MenuItem key={option.value} value={option.value}>
      {option.label}
    </MenuItem>
  ))}
</TextField>

Codesandbox Demo

Bryantbryanty answered 24/10, 2021 at 6:15 Comment(5)
You demo have an error A component is changing an uncontrolled input to be controlledRidicule
@Ridicule this is a terrible warning from MUI because RHF uses uncontrolled mode by default, in uncontrolled mode, value is undefined and the defaultValue is optional, but in this MUI component, you need to provide one explicitly for some reason. Anyway I fixed my answer, it should be working properly now.Bryantbryanty
What is the difference between this approach and the Controller component (accepted answer) approach ?Ungovernable
@Bryantbryanty Use Controller from react-hook-formImbibe
This answer does not use Select. It uses TextField. Why are you mentioning Select in your answer?Apocynthion
W
11

Accepted version is correct but outdated.

At least in the version that I'm using: "react-hook-form": "^7.30.0" you should use the render parameter.

Here is the "updated" version that perfectly works for me:

        <FormControl>
          <InputLabel id="level-label">Level</InputLabel>
          <Controller
            name="level"
            id="level"
            defaultValue={level}
            control={control}
            render={({ field }) => (
              <Select labelId="level-label" {...field}>
                <MenuItem value={0}>0</MenuItem>
                <MenuItem value={1}>1</MenuItem>
              </Select>
            )}
          />
          <FormHelperText error={true}>{errors.level?.message}</FormHelperText>
        </FormControl>

The important here is to propagate the field properties down to the child element (Select in our case)

PS. I don't think you need a separate component for it, it is pretty straight forward.

[Updated] Here is a full code of one of my dialog. As per request from Deshan.

import {
  Box, Chip, FormControl, Input, Stack,
} from '@mui/material';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import debounce from '../@utils/debounce';
import useRawParams from '../@utils/useRawParams';
import { useBrandsSearchQuery } from '../data/products';
import { SearchRoute } from '../SBRoutes';
import LoadingDiv from './LoadingDiv';
import SBDialog from './SBDialog';
import { useSearchBarContext } from '../contexts/SearchBarContext';

const context = { suspense: false };
/**
 * Show the modal dialog with the list of brands, and search box for it
 * Eeach brand will be as a link, for the SEO purposes
 */
export default function AllBrandsDialog({ open, setOpen }) {
  const [t] = useTranslation();
  const [query, setQuery] = useState('');
  const [brands, setBrands] = useState([]);
  const params = useRawParams(true);
  const paramsBrands = params.brands?.split(',') || [];
  const { setFilterActive } = useSearchBarContext();

  const variables = useMemo(() => (query.length ? {
    filterText: query,
  } : null), [query]);

  const [{ data, fetching: loading }] = useBrandsSearchQuery({ variables, pause: Boolean(!variables), context });
  const debounceSetQuery = useCallback(debounce(200, (text) => {
    setQuery(text);
  }));

  useEffect(() => {
    if (!data || !open) return;
    setBrands(data.brands || []);
  }, [data, open]);

  return (
    <SBDialog open={open} setOpen={setOpen} title={t('Search and select a brand')}>
      <Stack direction="column" spacing={2}>
        <FormControl>
          <Input
            id="tagSearch"
            placeholder={t('Start typing to see the brands')}
            onChange={(e) => debounceSetQuery(e.target.value)}
            autoFocus={true}
          />
        </FormControl>
        <Box display="grid" width={220} height={300} overflow="auto" gap={1} position="relative">
          {brands?.map((brand) => (
            <Chip
              component={Link}
              key={brand.id}
              disabled={paramsBrands.indexOf(brand.url) > -1}
              to={SearchRoute.generatePath({
                ...params,
                brands: [...paramsBrands, brand.url],
                page: undefined,
              })}
              size="small"
              label={brand.nicename}
              variant="outlined"
              onClick={() => {
                setOpen(false);
                setFilterActive(false);
              }}
              clickable={true}
            />
          ))}
          {loading && <LoadingDiv modal={true} />}
        </Box>
      </Stack>
    </SBDialog>
  );
}

AllBrandsDialog.propTypes = {
  open: PropTypes.bool.isRequired,
  setOpen: PropTypes.func.isRequired,
};
Winegrower answered 20/6, 2022 at 14:53 Comment(6)
Hi @Kostanos, Can you please share a working code sample of this?? TIAHammack
Hey @Deshan-Charuka, I just updated my answer with a full code example from one of my dialog that I have. I hope it helps you.Winegrower
@Winegrower your "full code example" doesn't even include the use of react-hook-form. Why did you add it your post, it adds nothing to your answer, its totally off-topic. Pus your initial answer uses "Controller" component and "control" object, without showing where these would have come from - ok the Controller is just an import, but what about control. Now THAT would have been a great "full code example" ...Patriciate
@AndyLorenz it is true, in my "full code" example I don't have a component with a render. But you can add this component to that example from my previous code sample. The goal of this code was to show how I realize form in generalWinegrower
Worked for us on "react-hook-form": "^7.50.1" and "@mui/material": "^5.14.17", thanks.Boast
How can I specify the value prop in Select this way? I tried using value={field.value} but it didn't work.Apocynthion
A
6

Here my code that working, hope it can help, need to use setValue

  <TextField
    fullWidth
    inputRef={register({
      name: 'name',
    })}
    select
    onChange={e => setValue('name', e.target.value, true)}
    label={label}
    defaultValue={defaultValue}
  >
    {options.map((option) => (
      <MenuItem key={option.label} value={option.value}>
        {option.label}
      </MenuItem>
    ))}
  </TextField>

Here using native select, do not need setValue, but value alway string

<TextField
    fullWidth
    select
    SelectProps={{
      native: true,
      inputProps: { ref: register, name: 'name' }
    }}
    label={label}
    defaultValue={defaultValue}
  >
    {options.map((option) => (
      <option key={option.label} value={option.value}>
        {option.label}
      </option>
    ))}
  </TextField>
Avocation answered 1/2, 2021 at 13:43 Comment(0)
N
1

This is an example that uses Material-UI with React hook form. You need to add the validation in 'inputRef' prop of TextField. Also you need to add 'onChange' function to keep the state updated. 'shouldValidate' will trigger the validation.

  <TextField
    select
    name='city'
    inputRef={register({ required: true })}
    onChange={e => setValue('city', e.target.value, { shouldValidate: true })}
    label="City"
    defaultValue="">
    {cityList.map((option, index) => (
      <MenuItem key={index} value={option}>
        {option}
      </MenuItem>
    ))}
  </TextField>

  {errors.city && <ErrorText>City is required</ErrorText>}
Nephograph answered 1/3, 2021 at 9:47 Comment(0)
P
0

✔ I came across this same issue, and this is how i solved mine:

<Select ... onChange={e => register({ name: 'academicLevel', value: e.target.value })}/>

more info

Phocine answered 3/2, 2021 at 22:16 Comment(0)
Q
0

When you using react-hook-form with material UI, you don´t need to use onChange and setState. Only use inputRef and all works!

Quassia answered 3/3, 2021 at 19:16 Comment(1)
the question is about using the selectTangle
C
0

Just need to pass the register to the Input Ref

 <Select
   variant="outlined"
   name="reason"
   inputRef={register({ required: true })}
  >
Collocation answered 11/4, 2021 at 3:46 Comment(0)
P
0

just use mui-react-hook-form-plus

Here is an example:

import { HookSelect, useHookForm } from 'mui-react-hook-form-plus';

const defaultValues = {
        person: {
            firstName: 'Atif',
            lastName: 'Aslam',
            sex: '',
        },
};

const App = () => {
    const { registerState, handleSubmit } = useHookForm({
        defaultValues,
    });

    const onSubmit = (_data: typeof defaultValues) => {
        alert(jsonStringify(_data));
    };

    return (
        <HookSelect
            {...registerState('person.sex')}
            label='SEX'
            items={[
                { label: 'MALE', value: 'male' },
                { label: 'FEMALE', value: 'female' },
                { label: 'OTHERS', value: 'others' },
            ]}
        />
    )
}

Repo: https://github.com/adiathasan/mui-react-hook-form-plus

Demo: https://mui-react-hook-form-plus.vercel.app/?path=/docs/

Platte answered 11/10, 2022 at 2:16 Comment(0)
I
0

save select state to useState and pass state to react-hook-form register

import React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { Select, MenuItem } from '@mui/material';

const schema = yup.object().shape({
  age: yup.number().required('Age is required'),
});

function App() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: yupResolver(schema),
  });

  const [age, setAge] = React.useState('');

  const handleChange = (event) => {
    setAge(event.target.value);
  };

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Select
        labelId="demo-simple-select-label"
        id="demo-simple-select"
        {...register('age')}
        value={age}
        label="Age"
        onChange={handleChange}
      >
        <MenuItem value={10}>Ten</MenuItem>
        <MenuItem value={20}>Twenty</MenuItem>
        <MenuItem value={30}>Thirty</MenuItem>
      </Select>

      {errors.age && <p>{errors.age.message}</p>}

      <button type="submit">Submit</button>
    </form>
  );
}

export default App;
Ichang answered 13/5, 2023 at 11:33 Comment(0)
W
0

I adjusted @Olivier's answer to get to a working solution in my case (RHF v7+, MUI v5+)

  <TextField
    select
    {...form.register(name)}
    value={form.watch(name)}
    error={!!form.formState.errors[name]}
    helperText={(form.formState.errors[name]?.message as React.ReactNode) ?? ''}
    {...props}
  >
    {options.map(({ value, label }) => (
      <MenuItem key={value} value={value}>
        {label}
      </MenuItem>
    ))}
  </TextField>
Wilds answered 23/4, 2024 at 0:57 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.