如何在react-hook-form中使用Material UI Select?

41
我在React中使用Material UI和React Hook Form构建了一个表单。我正在尝试创建一个自定义的TextField元素,使其作为Select输入工作。我希望它是一个未受控制的组件,并具有Ref属性。我已经尝试按照Material UI和React Hook Form文档推荐的方法将inputRef属性传递给它,但没有成功。
            <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>

我发现的一件事是,如果我使用本地的selectref,它可以正常工作。

此外,我尝试将inputRef属性更改为SelectProps,但也没有起作用。


请查看控制器:https://react-hook-form.com/api#Controller - Bill
10个回答

36

在使用 react hook form 的 Material UI Select 组件时,需要通过 Controller 实现自定义逻辑https://react-hook-form.com/api#Controller

以下是一个可重用组件,希望能够简化您在应用中使用 Select 组件的代码:

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;

您可以像这样在您的应用程序中使用它:

           <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>

这是您的codeSandBox,已更新此组件以用于信息表单中的选择:

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


你如何处理onChange事件? - Ricardo Huertas
2
在这种情况下,我们没有onChange,因为输入由DOM处理,即一个不受控制的组件。 - YungHK
是的,正如@YungHK所说,react-hook-form适用于不受控制的输入。如果您需要表单中输入的值,可以使用watch:https://react-hook-form.com/api#watch - abumalick
4
为避免以下错误,请确保传递有效的defaultValue值:Material-UI: 组件正在将Select的未受控制的值状态更改为受控制的。 元素不应该从未受控制切换到受控制(反之亦然)。 在组件的整个生命周期中决定使用受控制的或未受控制的Select元素。 - Paku
是的,Paku,这个有效的默认值可以工作,谢谢大家。 - Abdelhadi Abdo
1
我正在尝试在mui v5 + typescript中使用它:类型'{as: Element; name: string; control: any; defaultValue: any;}'无法分配给类型'IntrinsicAttributes&{render:({field,fieldState,formState,}:{field:ControllerRenderProps <FieldValues,string>; fieldState:ControllerFieldState; formState:UseFormStateReturn <...>; })=> ReactElement <...>; }&UseControllerProps <...> '。 - Remy

24

RHF v7 更新

以下是在 RHF 表单中使用 Material UI Select 的最简代码示例:

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


你的演示出现了错误“组件正在将一个不受控制的输入更改为受控制的” - Randall
1
@Randall,这是MUI的一个可怕警告,因为RHF默认使用未受控模式,在未受控模式中,“value”是未定义的,而“defaultValue”是可选的,但在这个MUI组件中,由于某种原因,您需要明确提供其中一个。无论如何,我已经修复了我的答案,现在应该可以正常工作了。 - NearHuscarl
这种方法与控制器组件(接受的答案)方法有什么区别? - gowthz
@NearHuscarl 使用 react-hook-form 中的控制器。 - Aman
@NearHuscarl,到目前为止我找到的最清晰的例子,非常感谢。 - Yohav Rn
谢谢。这正是我需要的。这是一个使用MUI v5和react-hook-form的可行示例。 - Voon Tao Tan

10

已接受的版本是正确的,但已经过时。

至少在我使用的版本中:"react-hook-form": "^7.30.0",您应该使用render参数。

这是适用于我完美工作的“更新”版本:

        <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>


重要的是将字段属性传递到子元素(在我们的情况下是Select
PS. 我不认为你需要一个单独的组件,它非常直截了当。
[更新] 这是我其中一个对话框的完整代码。根据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,
};

嗨@Kostanos,你能分享一份可用的代码示例吗?谢谢。 - Deshan-Charuka
1
嘿@Deshan-Charuka,我刚刚更新了我的答案,并附上了一个完整的代码示例,该示例来自我其中一个对话框。希望能对你有所帮助。 - Kostanos
1
@Kostanos,你的“完整代码示例”甚至没有包括使用react-hook-form。为什么要在你的帖子中添加它?它对你的答案毫无帮助,完全离题了。此外,你的初始答案使用了“Controller”组件和“control”对象,但没有显示这些是从哪里来的 - 好吧,“Controller”只是一个导入,但“control”呢?那才是一个很棒的“完整代码示例”... - Andy Lorenz
@AndyLorenz,没错,在我的“完整代码”示例中,我没有一个带有渲染的组件。但是你可以从我之前的代码示例中将这个组件添加到那个示例中。这段代码的目的是展示我如何实现表单的一般方法。 - Kostanos

5

这是我运行正常的代码,希望能够帮到你,需要使用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>

在这里使用本地选择,不需要setValue,但是值始终为字符串

<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>

1
这是一个使用React hook form和Material-UI的示例。您需要在TextField的“inputRef”属性中添加验证。同时,您需要添加“onChange”函数以保持状态更新。“shouldValidate”将触发验证。
  <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>}

0

✔ 我也遇到了同样的问题,这是我解决的方法:

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

更多信息


0

只需使用 mui-react-hook-form-plus

这里是一个例子:

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' },
            ]}
        />
    )
}

代码库: https://github.com/adiathasan/mui-react-hook-form-plus

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


0
当您使用react-hook-form与Material UI时,您不需要使用onChange和setState。只需使用inputRef即可使所有内容正常工作!

问题是关于使用select的。 - abumalick

0

将选择状态保存到 useState 中,并将状态传递给 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;

0

只需要将注册表传递给输入引用即可

 <Select
   variant="outlined"
   name="reason"
   inputRef={register({ required: true })}
  >

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接