在React Hook Form中使用Material UI

2,459 阅读9分钟

React Hook Form是React生态系统中最流行的处理表单输入的库之一。如果你使用Material UI这样的组件库,让它正常工作可能会很棘手。

在本指南中,我们将演示如何使用Material UI与React Hook Form。如果你想集成一些其他的React UI库,比如Ant Design或Semantic UI,本教程也很有帮助。

要继续学习,你应该已经对Material UIReact Hook Form有了一些了解。我们不会太深入地研究如何使用这些库。相反,我们将专注于它们之间的整合。

为了展示如何使用Material UI和React Hook Form,我们将用Material UI提供的最常用的输入组件建立一个完整的表单,包括。

  • 文本输入
  • 单选输入
  • 下拉菜单
  • 日期
  • 复选框
  • 滑块

该表格还将有重置功能。它看起来就像这样。

Material UI and React Hook Form Example

如果你更喜欢视觉学习,可以看看附带的视频教程。

文本输入组件

让我们从一个简单的表单组件开始。这个组件将只有一个文本输入。

如果用传统的方法建立这个表单,而不使用任何库,我们需要分别处理输入的变化。我们还必须自己处理重置功能和验证。

它可能看起来像这样。

import TextField from "@material-ui/core/TextField";
import React, { useState } from "react";
import { Button, Paper } from "@material-ui/core";

export const FormWithoutHookForm = () => {
  const [textValue, setTextValue] = useState<string>("");

  const onTextChange = (e: any) => setTextValue(e.target.value);
  const handleSubmit = () => console.log(textValue);
  const handleReset = () => setTextValue("");

  return (
    <Paper>
      <h2>Form Demo</h2>

      <TextField
        onChange={onTextChange}
        value={textValue}
        label={"Text Value"} //optional
      />

      <Button onClick={handleSubmit}>Submit</Button>
      <Button onClick={handleReset}>Reset</Button>
    </Paper>
  );
};

输出将看起来像这样。

Material UI and React Hook Form Text Input Component Example

这里我们通过使用React本身提供的useState Hook来存储数值。

const [textValue, setTextValue] = useState<string>("");

另外,我们正在设置我们的onTextChange 函数中输入的值。

const onTextChange = (e: any) => setTextValue(e.target.value);

如果我们看一下material-ui 提供的TextInput 组件,我们可以看到有两个重要的道具传递给它:valueonChangevalue 负责输入的实际值,而onChange 决定当输入变化时发生什么。无论我们如何使用这个表单,我们都需要照顾到这两件事。

设置React Hooks表单

React Hook Form从著名的useForm Hook导出一些实用程序,然后在你的输入组件里面使用。

首先,导入useForm Hook。

import { useForm } from "react-hook-form";

然后,在组件内使用Hook。

const { register } = useForm();

一个典型的输入可能看起来像这样。

<input type="text" ref={register} name="firstName" />

仔细看这里:我们把register 作为一个值传递给实际输入组件的ref 。所有的魔法都发生在幕后。

reactstrap提供了一个类似的道具,名为innerRef ,它可以用来传递我们的register ,以便与react-hook-form 无缝整合。

不幸的是,当我们使用Material UI时就不是这样了;该库还没有提供任何类似的道具来将register 作为值传递给ref 道具。

Controller 组件

React Hook Form包括一个名为Controller 的包装组件,用于与不能直接访问ref 的组件库合作。

根据React文档,这是一个渲染道具--一个返回React元素的函数,并提供将事件和价值附加到组件中的能力。

这个特殊的Controller 组件的骨架如下。

<Controller
  control={control}
  name="test"
  render={({
    field: { onChange, onBlur, value, name, ref },
    fieldState: { invalid, isTouched, isDirty, error },
    formState,
  }) => ( WHATEVER_INPUT_WE_WANT )}
/>

让我们来分析一下这里发生了什么。

  • control 是一个道具,我们从useForm 钩子那里拿回来,并传递到输入中。
  • name 是React Hook Form内部跟踪输入值的方式
  • render 是最重要的道具;我们在这里传递一个渲染函数

Therender prop

render 属性的Controller 是最重要的道具,需要理解。该函数有三个键:field,fieldState, 和formState 。我们现在要关注的是field

field 对象导出了两样东西(除此之外):valueonChange 。我们已经看到,我们需要这两样东西来控制几乎所有的输入。

重构我们的表单

所以让我们看看Controller 组件是否真的能解决我们的问题。我们将使用Controller 组件,并在渲染函数里面传递TextInput

让我们首先从useForm Hook中提取出我们需要的东西。

const { handleSubmit, reset, control } = useForm();

然后,像这样在表单中使用Controller 组件。

import TextField from "@material-ui/core/TextField";
import React, { useState } from "react";
import { Button, Paper } from "@material-ui/core";
import { Controller, useForm } from "react-hook-form";

export const FormWithHookForm = () => {
  const { handleSubmit, reset, control } = useForm();
  const onSubmit = (data: any) => console.log(data);

  return (
    <form>
      <Controller
        name={"textValue"}
        control={control}
        render={({ field: { onChange, value } }) => (
          <TextField onChange={onChange} value={value} label={"Text Value"} />
        )}
      />
      <Button onClick={handleSubmit(onSubmit)}>Submit</Button>
      <Button onClick={() => reset()} variant={"outlined"}>Reset</Button>
    </form>
  );
};

这个表单的工作原理和之前的一样。魔术的发生要归功于Controller 所提供的渲染函数的field 属性。

提取一个组件,使其可重复使用

所以我们现在知道如何使用React Hook Form的Controller 组件来让表单在没有任何ref 。现在让我们把输入组件提取到一个单独的组件中,这样我们就可以到处使用它。

这个普通的组件将需要来自其父级的三个prop。

  • name ,输入的密钥
  • control ,用来访问React Hook Form的功能。
  • label, 输入的标签(可选)。
import TextField from "@material-ui/core/TextField";
import { Controller } from "react-hook-form";
import React from "react";

export const FormInputText = ({ name, control, label }) => {
  return (
     (
        
      )}
    />
  );
};

我们将在我们的表单中像这样使用这个组件。

import { FormInputText } from "./FormInputTextGood";

export const FormWithHookForm = () => {
  // rest are same as before
  return (
    <form>
      <FormInputText
        name={"textInput"}
        control={control}
        label={"Text Input"}
      />
    </form>
  );
};

现在这个组件更容易理解和重用了。让我们也来处理一些其他的输入。

Radio 输入组件

第二种最常见的输入组件是Radio 。这里有一个重要的概念要记住。

如果你使用过Material UI中的Radio ,你已经知道你需要RadioGroup 组件作为父级,而里面的一堆选项,如单独的Radio 按钮,作为子级。

import React from "react";
import {
  FormControl,
  FormControlLabel,
  FormLabel,
  Radio,
  RadioGroup,
} from "@material-ui/core";
import { Controller, useFormContext } from "react-hook-form";
import { FormInputProps } from "./FormInputProps";

const options = [
  {
    label: "Radio Option 1",
    value: "1",
  },
  {
    label: "Radio Option 2",
    value: "2",
  },
];

export const FormInputRadio: React.FC<FormInputProps> = ({ name,control,label }) => {
  const generateRadioOptions = () => {
    return options.map((singleOption) => (
      <FormControlLabel
        value={singleOption.value}
        label={singleOption.label}
        control={<Radio />}
      />
    ));
  };  

  return <Controller
      name={name}
      control={control}
      render={({field: { onChange, value }}) => (
        <RadioGroup value={value} onChange={onChange}>
          {generateRadioOptions()}
        </RadioGroup>
      )}
    />
};

这里的主要概念是一样的。field 我们只是使用来自渲染函数onChangevalue 对象,并将其传递给RadioGroup

请注意,我们在这里没有使用label 。如果你想使用,你将需要添加Material UI的FormControlFormLabel 组件。

还要注意的是,使用了一个特殊的函数,generateRadioOptions ,来生成单个的单选输入。我们把options 作为一个常量添加到组件里面。你可以把它们作为道具或任何其他你认为合适的方式。

下拉菜单

几乎所有的表单都需要某种下拉菜单。Dropdown 组件的代码如下。

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

const options = [
  {
    label: "Dropdown Option 1",
    value: "1",
  },
  {
    label: "Dropdown Option 2",
    value: "2",
  },
];

export const FormInputDropdown= ({name,control, label}) => {

  const generateSelectOptions = () => {
    return options.map((option) => {
      return (
        <MenuItem key={option.value} value={option.value}>
          {option.label}
        </MenuItem>
      );
    });
  };

  return <Controller
      control={control}
      name={name}
      render={({ field: { onChange, value } }) => (
        <Select onChange={onChange} value={value}>
          {generateSelectOptions()}
        </Select>
      )}
    />
};

日期输入

这是一个常见而又特殊的组件。在Material UI中,我们没有任何Date 组件可以开箱即用。我们需要利用一些辅助库。

首先,安装这些依赖项。

yarn add @date-io/date-fns@1.3.13 @material-ui/pickers@3.3.10 date-fns@2.22.1

要注意版本问题。否则,你可能会遇到一些奇怪的问题。

我们还需要用一个特殊的包装器来包装我们的数据输入组件,MuiPickersUtilsProvider 。这将为我们注入日期选择器的功能。

记住,这不是React Hook Form的要求;这是Material UI的要求。因此,如果你使用任何其他设计库,比如Ant Design或Semantic UI,你不需要担心这个问题。

import React from "react";
import DateFnsUtils from "@date-io/date-fns";
import { KeyboardDatePicker, MuiPickersUtilsProvider} from "@material-ui/pickers";
import { Controller } from "react-hook-form";
const DATE_FORMAT = "dd-MMM-yy";

export const FormInputDate = ({ name, control, label }) => {
  return (
    <MuiPickersUtilsProvider utils={DateFnsUtils}>
      <Controller
        name={name}
        control={control}
        render={({ field : {onChange , value } }) => (
          <KeyboardDatePicker
            onChange={onChange}
            value={value}
            label={label}
          />
        )}
      />
    </MuiPickersUtilsProvider>
  );
};

复选框组

如果你想使用一个像开关一样工作的简单的复选框,那么它很容易使用。你只需要像以前的组件那样使用它,所以我不会再展示一次同样的东西。

然而,当你想创建一组复选框并将所选值设置为一个数组时,就会出现复杂的情况。这里的主要挑战是,Material UI没有提供一个多选复选框组件。

没有明确的例子说明如何用React Hook Form使用这个组件。为了实现这一功能,我们必须维护所选项目的本地状态。

import React, { useEffect, useState } from "react";
import {Checkbox,FormControl,FormControlLabel,FormLabel} from "@material-ui/core";
import { Controller } from "react-hook-form";

const options = [
  {
    label: "Checkbox Option 1",
    value: "1",
  },
  {
    label: "Checkbox Option 2",
    value: "2",
  },
];

export const FormInputMultiCheckbox= ({name,control,setValue,label}) => {
  const [selectedItems, setSelectedItems] = useState<any>([]);

  // we are handling the selection manually here
  const handleSelect = (value: any) => {
    const isPresent = selectedItems.indexOf(value);
    if (isPresent !== -1) {
      const remaining = selectedItems.filter((item: any) => item !== value);
      setSelectedItems(remaining);
    } else {
      setSelectedItems((prevItems: any) => [...prevItems, value]);
    }
  };

  // we are setting form value manually here
  useEffect(() => {
    setValue(name, selectedItems); 
  }, [selectedItems]);

  return (
    <FormControl size={"small"} variant={"outlined"}>
      <FormLabel component="legend">{label}</FormLabel>

      <div>
        {options.map((option: any) => {
          return (
            <FormControlLabel
              control={
                <Controller
                  name={name}
                  render={({}) => {
                    return (
                      <Checkbox
                        checked={selectedItems.includes(option.value)}
                        onChange={() => handleSelect(option.value)}
                      />
                    );
                  }}
                  control={control}
                />
              }
              label={option.label}
              key={option.value}
            />
          );
        })}
      </div>
    </FormControl>
  );
};

所以在这个组件中,我们在这里手动控制valueonChange 两个项目。这就是为什么在render 函数中,我们不再使用field 这个道具了。为了设置这个值,我们在这里使用另一个新的道具,名为setValue 。这个函数是react-hook-form 的一个特殊函数,用于手动设置值。

你可以问,如果我们手动处理输入,为什么还要这样做?答案是当你使用react-hook-form ,你希望所有的输入都在一个地方。所以我们在这里给这个MultiSelectCheckbox 组件一个特殊的处理,这样它就能很容易地与其他组件一起工作。

滑块

我们的最后一个组件是一个Slider 组件,这是一个相当常见的组件。

代码简单易懂,但有一个问题:Material UI提供的onChange 函数不能与React Hook Form的onChange ,因为签名不同。

因此,当我们试图在React Hook Form的Controller 组件内使用Slider 组件时,会抛出错误。再一次,我们必须保持一个本地状态来控制onChange ,并手动设置值。

这个组件的完整代码如下。

import React, { useEffect } from "react";
import { Slider } from "@material-ui/core";
import { Controller } from "react-hook-form";

export const FormInputSlider = ({name,control,setValue,label}) => {
  const [sliderValue, setSliderValue] = React.useState(0);

  useEffect(() => {
    if (sliderValue) setValue(name, sliderValue);
  }, [sliderValue]);

  const handleChange = (event: any, newValue: number | number[]) => {
    setSliderValue(newValue as number);
  };

  return <Controller
      name={name}
      control={control}
      render={({ field, fieldState, formState }) => (
        <Slider
          value={sliderValue}
          onChange={handleChange}
        />
      )}
    />
};

把这一切放在一起

现在让我们在我们的表单中使用所有这些组件。我们的表单将利用我们刚刚制作的所有可重复使用的组件。

import { Button, Paper, Typography } from "@material-ui/core";
import { FormProvider, useForm } from "react-hook-form";
import { FormInputText } from "./form-components/FormInputText";
import { FormInputMultiCheckbox } from "./form-components/FormInputMultiCheckbox";
import { FormInputDropdown } from "./form-components/FormInputDropdown";
import { FormInputDate } from "./form-components/FormInputDate";
import { FormInputSlider } from "./form-components/FormInputSlider";
import { FormInputRadio } from "./form-components/FormInputRadio";

const defaultValues = {
  textValue: "",
  radioValue: "",
  checkboxValue: [],
  dateValue: new Date(),
  dropdownValue: "",
  sliderValue: 0,
};

export const FormDemo = () => {
  const methods = useForm({ defaultValues: defaultValues });
  const { handleSubmit, reset, control, setValue } = methods;
  const onSubmit = (data: IFormInput) => console.log(data);

  return (
    <Paper>
      <Typography variant="h6"> Form Demo </Typography>

      <FormInputText name="textValue" control={control} label="Text Input" />
      <FormInputRadio name={"radioValue"} control={control} label={"Radio Input"} />
      <FormInputDropdownname="dropdownValue"control={control}label="Dropdown Input"/>
      <FormInputDate name="dateValue" control={control} label="Date Input" />

      <FormInputMultiCheckbox
        control={control}
        setValue={setValue}
        name={"checkboxValue"}
        label={"Checkbox Input"}
      />

      <FormInputSlider
        name={"sliderValue"}
        control={control}
        setValue={setValue}
        label={"Slider Input"}
      />

      <Button onClick={handleSubmit(onSubmit)} variant={"contained"}>
        Submit
      </Button>
      <Button onClick={() => reset()} variant={"outlined"}>
        Reset
      </Button>
    </Paper>
  );
};

结论

现在,我们的表单更加简洁,性能更强。从这里,我们可以非常容易地添加我们的表单验证逻辑和错误处理

要想自己玩这个例子,请在GitHub上查看完整的代码。

The postUsing Material UI with React Hook Formappeared first onLogRocket Blog.