React Hook Form是React生态系统中最流行的处理表单输入的库之一。如果你使用Material UI这样的组件库,让它正常工作可能会很棘手。
在本指南中,我们将演示如何使用Material UI与React Hook Form。如果你想集成一些其他的React UI库,比如Ant Design或Semantic UI,本教程也很有帮助。
要继续学习,你应该已经对Material UI和React Hook Form有了一些了解。我们不会太深入地研究如何使用这些库。相反,我们将专注于它们之间的整合。
为了展示如何使用Material UI和React Hook Form,我们将用Material UI提供的最常用的输入组件建立一个完整的表单,包括。
- 文本输入
- 单选输入
- 下拉菜单
- 日期
- 复选框
- 滑块
该表格还将有重置功能。它看起来就像这样。
如果你更喜欢视觉学习,可以看看附带的视频教程。
文本输入组件
让我们从一个简单的表单组件开始。这个组件将只有一个文本输入。
如果用传统的方法建立这个表单,而不使用任何库,我们需要分别处理输入的变化。我们还必须自己处理重置功能和验证。
它可能看起来像这样。
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>
);
};
输出将看起来像这样。
这里我们通过使用React本身提供的useState
Hook来存储数值。
const [textValue, setTextValue] = useState<string>("");
另外,我们正在设置我们的onTextChange
函数中输入的值。
const onTextChange = (e: any) => setTextValue(e.target.value);
如果我们看一下material-ui
提供的TextInput
组件,我们可以看到有两个重要的道具传递给它:value
和onChange
。value
负责输入的实际值,而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
对象导出了两样东西(除此之外):value
和onChange
。我们已经看到,我们需要这两样东西来控制几乎所有的输入。
重构我们的表单
所以让我们看看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
我们只是使用来自渲染函数onChange
和value
对象,并将其传递给RadioGroup
。
请注意,我们在这里没有使用label
。如果你想使用,你将需要添加Material UI的FormControl
和FormLabel
组件。
还要注意的是,使用了一个特殊的函数,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>
);
};
所以在这个组件中,我们在这里手动控制value
和onChange
两个项目。这就是为什么在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.