In React there are lots of ways to write a form, some use libraries like Formik, Redux Form or some prefer the plain way of doing it writing everything from scratch. The advantage of using a form library is a lot of common form functionality is taken care of like validations, getting the whole form data in a single object and writing less code(this point is debatable :D)
One such form library in react is React Hook Form
Why I choose to use React Hook Form?
I have tried a couple of form libraries the most popular being Formik, but none of these is, as fast as React Hook Form. In my web apps, my form usually has around 60-70 fields, and for such a large amount of fields, no form library comes close to React Hook Form in terms of performance, not even Formik.
GOAL
In this article, we will cover how to create reusable form components like TextField, Select of Material UI, MultiSelect of react-select with React Hook Form. We will be using Yup for form validations and how it integrates with React Hook Form.
At the end of the article, I will share a git hub repo where I have included all the form components of Material UI with React Hook Form that one can easily reference or integrate into their projects
Table of Content
This article will be a long one. So I have divided my article into few sections
- Initial setup
- Basic form element binding with React Hook Form
- Validation with Yup
- Pre populating form field data
- Github repo
- References
Initial setup
We will use create-react-app
for this article. Follow the below steps to setup the basics
npx create-react-app hook-form-mui
cd hook-form-mui
npm install @material-ui/core @material-ui/icons react-hook-form yup @hookform/resolvers react-select styled-components @material-ui/pickers @date-io/moment@1.x moment
Once all the packages are installed, run the app once.
npm start
Basic form element binding with React Hook Form
1. Textfield
Create a folder in the src named controls. Inside controls folder create a folder input. Inside input folder create a file index.js (src -> controls -> input -> index.js)
index.js will have below code
import React from "react";
import { useFormContext, Controller } from "react-hook-form";
import TextField from "@material-ui/core/TextField";
function FormInput(props) {
const { control } = useFormContext();
const { name, label } = props;
return (
<Controller
as={TextField}
name={name}
control={control}
defaultValue=""
label={label}
fullWidth={true}
{...props}
/>
);
}
export default FormInput;
Lets deep dive into the above code.
When using React Hook Form, two primary concepts need to be kept in mind,
- We have to register each form field that we use. This helps in form submission and validation.
- Each form field should have a unique name associated with it.
In the above code, we are using a wrapper component called Controller
provided by react-hook-form
to register our form fields (in this case) TextField
component.
As you can see we can pass additional props of TextField
component and other props directly to the Controller
component
<Controller
as={TextField}
name={name}
control={control}
defaultValue=""
label={label}
fullWidth={true}
InputLabelProps={{
className: required ? "required-label" : "",
required: required || false,
}}
error={isError}
helperText={errorMessage}
{...props}
/>
control
object contains methods for registering a controlled component into React Hook Form. The control
object needs to be pass as a prop to the Controller
component.
control
object is declared as :
const { control } = useFormContext();
In App.js
, we will have the following code:
import React from "react";
import { useForm, FormProvider } from "react-hook-form";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
import FormInput from "./controls/input";
function App(props) {
const methods = useForm();
const { handleSubmit } = methods;
const onSubmit = (data) => {
console.log(data);
};
return (
<div style={{ padding: "10px" }}>
<Button
variant="contained"
color="primary"
onClick={handleSubmit(onSubmit)}
>
SUBMIT
</Button>
<div style={{ padding: "10px" }}>
<FormProvider {...methods}> // pass all methods into the context
<form>
<Grid container spacing={2}>
<Grid item xs={6}>
<FormInput name="name" label="Name" />
</Grid>
</Grid>
</form>
</FormProvider>
</div>
</div>
);
}
export default App;
Let's deep dive into the App.js
code.
The most important function is useForm()
which is a hook provided by react-hook-form
. useForm()
contains various methods which is required for form validation, submission and registration of the form fields.
const methods = useForm();
const { handleSubmit } = methods;
As in the above code useForm()
provides a method
object which contains handleSubmit
function which is used for form submission on button click. In this case SUBMIT
button.
<FormProvider {...methods}>
<form>
<Grid container spacing={2}>
<Grid item xs={6}>
<FormInput name="name" label="Name" />
</Grid>
</Grid>
</form>
</FormProvider>
In the above block of code, we declare a FormProvider
component under which our form and its respective fields will be declared. Also, we need to pass all the functions and objects of methods
to FormProvider
component. This is required as we are using a deep nested structured of custom form fields and the useFormContext()
used in the FormInput
component need to consume the functions and objects of methods
For the FormInput
component we just need to pass name
and label
props.
<FormInput name="name" label="Name" />
If you run the app, you should see:
Type any text in the Name field and click on SUBMIT button. In the dev-console check the output. The output will be an object with the field name and corresponding value.
Now let's move on to create other field components in a similar fashion.
2. Select
Create a new folder name styles under src. Create a new file index.js under styles folder (src -> styles -> index.js)
index.js will have following code
import styled from "styled-components";
import { InputLabel } from "@material-ui/core";
export const StyledInputLabel = styled(InputLabel)`
&& {
.req-label {
color: #f44336;
}
}
`;
I'm using styled-components
for my styling. StyledInputLabel
will be used below in the FormSelect
component. The main purpose of the above styling will be used during validation.
Create a new folder name select under controls, inside select folder create a index.js file (controls -> select -> index.js).
index.js will have following code
import React from "react";
import { useFormContext, Controller } from "react-hook-form";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import InputLabel from "@material-ui/core/InputLabel";
const MuiSelect = (props) => {
const { label, name, options } = props;
return (
<FormControl fullWidth={true}>
<InputLabel htmlFor={name}>{label}</InputLabel>
<Select id={name} {...props}>
<MenuItem value="">
<em>None</em>
</MenuItem>
{options.map((item) => (
<MenuItem key={item.id} value={item.id}>
{item.label}
</MenuItem>
))}
</Select>
</FormControl>
);
};
function FormSelect(props) {
const { control } = useFormContext();
const { name, label } = props;
return (
<React.Fragment>
<Controller
as={MuiSelect}
control={control}
name={name}
label={label}
defaultValue=""
{...props}
/>
</React.Fragment>
);
}
export default FormSelect;
Things to note in the above code
-
MuiSelect
function is a component which contains our UI for the rendering of the Select field. There are three main propsname
,label
andoptions
.options
is an array of objects that contains the data to be displayed in the dropdown. -
FormSelect
is similar toFormInput
component where again we are usinguseFormContext()
method,Controller
component andcontrol
object.
Let's see how we consume FormSelect
in the App.js. Below is the new code in App.js
import React from "react";
import { useForm, FormProvider } from "react-hook-form";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
import FormInput from "./controls/input";
import FormSelect from "./controls/select";
function App(props) {
const methods = useForm();
const { handleSubmit } = methods;
const onSubmit = (data) => {
console.log(data);
};
const numberData = [
{
id: "10",
label: "Ten",
},
{
id: "20",
label: "Twenty",
},
{
id: "30",
label: "Thirty",
},
];
return (
<div style={{ padding: "10px" }}>
<Button
variant="contained"
color="primary"
onClick={handleSubmit(onSubmit)}
>
SUBMIT
</Button>
<div style={{ padding: "10px" }}>
<FormProvider {...methods}>
<form>
<Grid container spacing={2}>
<Grid item xs={6}>
<FormInput name="name" label="Name" />
</Grid>
<Grid item xs={6}>
<FormSelect name="sel" label="Numbers" options={numberData} />
</Grid>
</Grid>
</form>
</FormProvider>
</div>
</div>
);
}
export default App;
What has changed in the App.js
- I have created data (array of objects) that we will pass to the
FormSelect
.
const numberData = [
{
id: "10",
label: "Ten",
},
{
id: "20",
label: "Twenty",
},
{
id: "30",
label: "Thirty",
},
];
- I have added following code to the render
<Grid item xs={6}>
<FormSelect name="sel" label="Numbers" options={noData} />
</Grid>
Now your web page will look like:
Fill the form data, and click on SUBMIT button. Check the output in the dev-console.
3. Multi-Select with Autocomplete (React-Select)
Here we will be using one of the most popular react libraries React-Select. Create a new folder name select-autocomplete under controls, inside select-autocomplete folder create two files index.js and index.css file
Now go to index.js under styles folder and add below code:
export const StyledFormControl = styled(FormControl)`
&& {
width: 100%;
display: block;
position: relative;
}
`;
export const StyledAutoSelectInputLabel = styled(InputLabel)`
&& {
position: relative;
.req-label {
color: #f44336;
}
transform: translate(0, 1.5px) scale(0.75);
transform-origin: top left;
}
`;
Now go to index.css under select-autocomplete folder and add below code:
.autoselect-options {
padding: 6px 16px;
line-height: 1.5;
width: auto;
min-height: auto;
font-size: 1rem;
letter-spacing: 0.00938em;
font-weight: 400;
cursor: pointer;
}
.autoselect-options:hover {
background-color: rgba(0, 0, 0, 0.14) !important;
}
I have done the styling changes for two purposes, firstly it will be used when we add validation for error handling and second to make the React-Select look and feel close to Material UI Select.
Now go to index.js under select-autocomplete folder and add below code:
import React, { useEffect, useState } from "react";
import { useFormContext, Controller } from "react-hook-form";
import Select, { createFilter } from "react-select";
import { StyledFormControl, StyledAutoSelectInputLabel } from "../../styles";
import "./index.css";
const stylesReactSelect = {
clearIndicator: (provided, state) => ({
...provided,
cursor: "pointer",
}),
indicatorSeparator: (provided, state) => ({
...provided,
margin: 0,
}),
dropdownIndicator: (provided, state) => ({
...provided,
cursor: "pointer",
}),
placeholder: (provided, state) => ({
...provided,
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
color: state.selectProps.error ? "#f44336" : "rgba(0, 0, 0, 0.54)",
}),
control: (provided, state) => ({
...provided,
borderRadius: 0,
border: 0,
borderBottom: state.selectProps.error
? "1px solid #f44336"
: "1px solid rgba(0,0,0,0.87)",
boxShadow: "none",
":hover": {
borderColor: state.selectProps.error ? "1px solid #f44336" : "inherit",
boxShadow: state.selectProps.error ? "1px solid #f44336" : "none",
},
}),
valueContainer: (provided, state) => ({
...provided,
paddingLeft: 0,
}),
};
const components = {
Option,
};
function Option(props) {
const { onMouseMove, onMouseOver, ...newInnerProps } = props.innerProps;
return (
<div {...newInnerProps} className="autoselect-options">
{props.children}
</div>
);
}
const ReactSelect = (props) => {
const { label, options, name } = props;
return (
<React.Fragment>
<StyledFormControl>
<StyledAutoSelectInputLabel>
<span>{label}</span>
</StyledAutoSelectInputLabel>
<Select
options={options}
placeholder="Please Select"
valueKey="id"
components={components}
isClearable={true}
styles={stylesReactSelect}
isSearchable={true}
filterOption={createFilter({ ignoreAccents: false })}
{...props}
/>
</StyledFormControl>
</React.Fragment>
);
};
function FormSelectAutoComplete(props) {
const { control } = useFormContext();
const { name, label, options } = props;
const [newData, setNewData] = useState([]);
useEffect(() => {
const newOptions = options.map((data, index) => ({
label: data.label,
value: data.id,
}));
setNewData(newOptions);
}, [options]);
return (
<React.Fragment>
<Controller
as={ReactSelect}
name={name}
control={control}
label={label}
{...props}
options={newData}
/>
</React.Fragment>
);
}
export default FormSelectAutoComplete;
Let's break down the code.
const stylesReactSelect = {
clearIndicator: (provided, state) => ({
...provided,
cursor: "pointer",
}),
indicatorSeparator: (provided, state) => ({
...provided,
margin: 0,
}),
dropdownIndicator: (provided, state) => ({
...provided,
cursor: "pointer",
}),
placeholder: (provided, state) => ({
...provided,
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
color: state.selectProps.error ? "#f44336" : "rgba(0, 0, 0, 0.54)",
}),
control: (provided, state) => ({
...provided,
borderRadius: 0,
border: 0,
borderBottom: state.selectProps.error
? "1px solid #f44336"
: "1px solid rgba(0,0,0,0.87)",
boxShadow: "none",
":hover": {
borderColor: state.selectProps.error ? "1px solid #f44336" : "inherit",
boxShadow: state.selectProps.error ? "1px solid #f44336" : "none",
},
}),
valueContainer: (provided, state) => ({
...provided,
paddingLeft: 0,
}),
};
- The above code is just styling change. As I have mentioned before I have done this to make the look and feel similar to Material UI Select to maintain the design consistency. You can refer to the complete style guide of react-select in this link
const components = {
Option,
};
function Option(props) {
const { onMouseMove, onMouseOver, ...newInnerProps } = props.innerProps;
return (
<div {...newInnerProps} className="autoselect-options">
{props.children}
</div>
);
}
- The above code improves performance if you have a large data(around 100+ data objects)
const ReactSelect = (props) => {
const { label, options, name } = props;
return (
<React.Fragment>
<StyledFormControl>
<StyledAutoSelectInputLabel>
<span>{label}</span>
</StyledAutoSelectInputLabel>
<Select
options={options}
placeholder="Please Select"
valueKey="id"
components={components}
isClearable={true}
styles={stylesReactSelect}
isSearchable={true}
filterOption={createFilter({ ignoreAccents: false })}
{...props}
/>
</StyledFormControl>
</React.Fragment>
);
};
- This is the UI part with label and react-select component. Similar to
FormSelect
, there are three main propsname
,label
andoptions
.options
is an array of objects that contains the data to be displayed in the react-select.
function FormSelectAutoComplete(props) {
const { control } = useFormContext();
const { name, label, options } = props;
const [newData, setNewData] = useState([]);
useEffect(() => {
const newOptions = options.map((data, index) => ({
label: data.label,
value: data.id,
}));
setNewData(newOptions);
}, [options]);
return (
<React.Fragment>
<Controller
as={ReactSelect}
name={name}
control={control}
label={label}
{...props}
options={newData}
/>
</React.Fragment>
);
}
-
FormSelectAutoComplete
is similar toFormSelect
component where again we are usinguseFormContext()
method,Controller
component andcontrol
object. One thing to note here is that the array of data objects that are passed to theSelect
component of react-select should havelabel
andvalue
key in the object. In the below code, I have purposefully passed data that doesn't have thislabel
andvalue
in the object, (which can be the case in the real world scenario) to show you what changes you need to do in order to satisfy this requirement.
useEffect(() => {
const newOptions = options.map((data, index) => ({
label: data.label,
value: data.id,
}));
setNewData(newOptions);
}, [options]);
You don't need to do this if your data object contains label
and value
as a key.
Let's see how we consume FormSelectAutoComplete
in the App.js. Below is the new code in App.js
import React from "react";
import { useForm, FormProvider } from "react-hook-form";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
import FormInput from "./controls/input";
import FormSelect from "./controls/select";
import FormSelectAutoComplete from "./controls/select-autocomplete";
function App(props) {
const methods = useForm();
const { handleSubmit } = methods;
const onSubmit = (data) => {
console.log(data);
};
const numberData = [
{
id: "10",
label: "Ten",
},
{
id: "20",
label: "Twenty",
},
{
id: "30",
label: "Thirty",
},
];
return (
<div style={{ padding: "10px" }}>
<Button
variant="contained"
color="primary"
onClick={handleSubmit(onSubmit)}
>
SUBMIT
</Button>
<div style={{ padding: "10px" }}>
<FormProvider {...methods}>
<form>
<Grid container spacing={2}>
<Grid item xs={6}>
<FormInput name="name" label="Name" />
</Grid>
<Grid item xs={6}>
<FormSelect name="sel" label="Numbers" options={numberData} />
</Grid>
<Grid item xs={6}>
<FormSelectAutoComplete
name="selAuto"
label="Auto Select Numbers"
options={numberData}
isMulti
/>
</Grid>
</Grid>
</form>
</FormProvider>
</div>
</div>
);
}
export default App;
What has changed in App.js
is the below piece of code
<Grid item xs={6}>
<FormSelectAutoComplete
name="selAuto"
label="Auto Select Numbers"
options={numberData}
isMulti
/>
</Grid>
Here we are using the same numberData
array of objects that we used in the FormSelect
because react-select takes array of objects as data which we have passed in the options
prop. isMulti
prop is used if we want to show multiple selected values.
Now your web page will look like:
Fill the form data, and click on SUBMIT button. Check the output in the dev-console.
Validation with Yup
If you have a form, the chance is that 99% of the time you will have some sort of the validation. React Hook Forms provide various ways to do the validation (Basic Validaiton & Schema Validation).
We are going to use Yup for our validations.
Let's modify our App.js
import React from "react";
import { useForm, FormProvider } from "react-hook-form";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
import FormInput from "./controls/input";
import FormSelect from "./controls/select";
import FormSelectAutoComplete from "./controls/select-autocomplete";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers";
const validationSchema = yup.object().shape({
nameV: yup.string().required("Name Validation Field is Required"),
selV: yup.string().required("Select Validation Field is Required"),
selAutoV: yup.array().required("Multi Select Validation Field required"),
});
function App(props) {
const methods = useForm({
resolver: yupResolver(validationSchema),
});
const { handleSubmit, errors } = methods;
const onSubmit = (data) => {
console.log(data);
};
const numberData = [
{
id: "10",
label: "Ten",
},
{
id: "20",
label: "Twenty",
},
{
id: "30",
label: "Thirty",
},
];
return (
<div style={{ padding: "10px" }}>
<Button
variant="contained"
color="primary"
onClick={handleSubmit(onSubmit)}
>
SUBMIT
</Button>
<div style={{ padding: "10px" }}>
<FormProvider {...methods}>
<form>
<Grid container spacing={2}>
<Grid item xs={6}>
<FormInput name="name" label="Name" />
</Grid>
<Grid item xs={6}>
<FormInput
name="nameV"
label="Name with Validation"
required={true}
errorobj={errors}
/>
</Grid>
<Grid item xs={6}>
<FormSelect name="sel" label="Numbers" options={numberData} />
</Grid>
<Grid item xs={6}>
<FormSelect
name="selV"
label="Numbers with Validation"
options={numberData}
required={true}
errorobj={errors}
/>
</Grid>
<Grid item xs={6}>
<FormSelectAutoComplete
name="selAuto"
label="Auto Select Numbers"
options={numberData}
isMulti
/>
</Grid>
<Grid item xs={6}>
<FormSelectAutoComplete
name="selAutoV"
label="Auto Select Numbers with Validation"
options={numberData}
isMulti
required={true}
errorobj={errors}
/>
</Grid>
</Grid>
</form>
</FormProvider>
</div>
</div>
);
}
export default App;
Let's dissect the new code changes:
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers";
- We have imported
yup
andyupResolver
const validationSchema = yup.object().shape({
nameV: yup.string().required("Name Validation Field is Required"),
selV: yup.string().required("Select Validation Field is Required"),
selAutoV: yup.array().required("Multi Select Validation Field required"),
});
- Create a
validationSchema
object as shown above.nameV
is the name of theFormInput
field to which the validation needs to be applied. The user input value will be of type "string" henceyup.string()
. Since it is a required fieldyup.string().required()
. The custom error message can be passed torequired
function as shown above. Similarly,selV
is the name ofFormSelect
field, where the value selected from the dropdown will be of type "string" henceyup.string().required()
. The custom error message can be passed torequired
function as shown above.selAutoV
is the name ofFormSelectAutoComplete
field, where the value selected will be in the form of array of objects. Henceyup.array().required()
. The custom error message can be passed torequired
function as shown above.
What if we don't pass a custom error message, it will not throw an error but it will display some other message(Try this out!)
const methods = useForm({
resolver: yupResolver(validationSchema),
});
const { handleSubmit, errors } = methods;
Pass the
validationSchema
object to theyupResolver
function as shown above. Also we will be usingerrors
object frommethods
object which will contain the field that has an error along with the error message.We have added three new components
FormInput
,FormSelect
&FormSelectAutoComplete
with two new propsrequired={true}
anderrorobj={errors}
<Grid item xs={6}>
<FormInput
name="nameV"
label="Name with Validation"
required={true}
errorobj={errors}
/>
</Grid>
<Grid item xs={6}>
<FormSelect
name="selV"
label="Numbers with Validation"
options={numberData}
required={true}
errorobj={errors}
/>
</Grid>
<Grid item xs={6}>
<FormSelectAutoComplete
name="selAutoV"
label="Auto Select Numbers with Validation"
options={numberData}
isMulti
required={true}
errorobj={errors}
/>
</Grid>
Now we need to modify our FormInput
, FormSelect
& FormSelectAutoComplete
component to highlight validation error and show respective error messages.
FormInput
- Create an index.css file in the input folder of controls (controls -> input -> index.css). index.css will have following code:
.required-label span {
color: #f44336;
}
import React from "react";
import { useFormContext, Controller } from "react-hook-form";
import TextField from "@material-ui/core/TextField";
import "./index.css";
function FormInput(props) {
const { control } = useFormContext();
const { name, label, required, errorobj } = props;
let isError = false;
let errorMessage = "";
if (errorobj && errorobj.hasOwnProperty(name)) {
isError = true;
errorMessage = errorobj[name].message;
}
return (
<Controller
as={TextField}
name={name}
control={control}
defaultValue=""
label={label}
fullWidth={true}
InputLabelProps={{
className: required ? "required-label" : "",
required: required || false,
}}
error={isError}
helperText={errorMessage}
{...props}
/>
);
}
export default FormInput;
We have made the following changes:
const { name, label, required, errorobj } = props;
let isError = false;
let errorMessage = "";
if (errorobj && errorobj.hasOwnProperty(name)) {
isError = true;
errorMessage = errorobj[name].message;
}
The required
and errorobj
which were passed as props to the FormInput
component in App.js
are being used above. errorObj
consist of name of the field and error message which we have passed in the validation schema. This object is created by react hook forms. The above piece of code will be similar across FormSelect
& FormSelectAutoComplete
form components which we have created.
Next change which we did was to the Controller
component
<Controller
as={TextField}
name={name}
control={control}
defaultValue=""
label={label}
fullWidth={true}
InputLabelProps={{
className: required ? "required-label" : "",
required: required || false,
}}
error={isError}
helperText={errorMessage}
{...props}
/>
We added the following new props to the Controller
component.
InputLabelProps={{
className: required ? "required-label" : "",
required: required || false,
}}
error={isError}
helperText={errorMessage}
InputLabelProps
, error
and helperText
props are specified by Material UI TextField
to control the styling of TextField
and how to show an error message.
Similar code changes will be done to FormSelect
and FormSelectAutoComplete
component.
FormSelect
import React from "react";
import { useFormContext, Controller } from "react-hook-form";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import { StyledInputLabel } from "../../styles";
import FormHelperText from "@material-ui/core/FormHelperText";
const MuiSelect = (props) => {
const { label, name, options, required, errorobj } = props;
let isError = false;
let errorMessage = "";
if (errorobj && errorobj.hasOwnProperty(name)) {
isError = true;
errorMessage = errorobj[name].message;
}
return (
<FormControl fullWidth={true} error={isError}>
<StyledInputLabel htmlFor={name}>
{label} {required ? <span className="req-label">*</span> : null}
</StyledInputLabel>
<Select id={name} {...props}>
<MenuItem value="">
<em>None</em>
</MenuItem>
{options.map((item) => (
<MenuItem key={item.id} value={item.id}>
{item.label}
</MenuItem>
))}
</Select>
<FormHelperText>{errorMessage}</FormHelperText>
</FormControl>
);
};
function FormSelect(props) {
const { control } = useFormContext();
const { name, label } = props;
return (
<React.Fragment>
<Controller
as={MuiSelect}
control={control}
name={name}
label={label}
defaultValue=""
{...props}
/>
</React.Fragment>
);
}
export default FormSelect;
FormSelectAutoComplete
import React, { useEffect, useState } from "react";
import { useFormContext, Controller } from "react-hook-form";
import Select, { createFilter } from "react-select";
import { StyledFormControl, StyledAutoSelectInputLabel } from "../../styles";
import FormHelperText from "@material-ui/core/FormHelperText";
import "./index.css";
const stylesReactSelect = {
clearIndicator: (provided, state) => ({
...provided,
cursor: "pointer",
}),
indicatorSeparator: (provided, state) => ({
...provided,
margin: 0,
}),
dropdownIndicator: (provided, state) => ({
...provided,
cursor: "pointer",
}),
placeholder: (provided, state) => ({
...provided,
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
color: state.selectProps.error ? "#f44336" : "rgba(0, 0, 0, 0.54)",
}),
control: (provided, state) => ({
...provided,
borderRadius: 0,
border: 0,
borderBottom: state.selectProps.error
? "1px solid #f44336"
: "1px solid rgba(0,0,0,0.87)",
boxShadow: "none",
":hover": {
borderColor: state.selectProps.error ? "1px solid #f44336" : "inherit",
boxShadow: state.selectProps.error ? "1px solid #f44336" : "none",
},
}),
valueContainer: (provided, state) => ({
...provided,
paddingLeft: 0,
}),
};
const components = {
Option,
};
function Option(props) {
const { onMouseMove, onMouseOver, ...newInnerProps } = props.innerProps;
return (
<div {...newInnerProps} className="autoselect-options">
{props.children}
</div>
);
}
const ReactSelect = (props) => {
const { label, options, required, errorobj, name } = props;
let isError = false;
let errorMessage = "";
if (errorobj && errorobj.hasOwnProperty(name)) {
isError = true;
errorMessage = errorobj[name].message;
}
return (
<React.Fragment>
<StyledFormControl>
<StyledAutoSelectInputLabel>
<span className={isError ? "req-label" : ""}>
{label} {required ? <span className="req-label">*</span> : null}
</span>
</StyledAutoSelectInputLabel>
<Select
options={options}
placeholder="Please Select"
valueKey="id"
components={components}
isClearable={true}
styles={stylesReactSelect}
isSearchable={true}
filterOption={createFilter({ ignoreAccents: false })}
error={isError}
{...props}
/>
{isError && (
<FormHelperText error={isError}>{errorMessage}</FormHelperText>
)}
</StyledFormControl>
</React.Fragment>
);
};
function FormSelectAutoComplete(props) {
const { control } = useFormContext();
const { name, label, options } = props;
const [newData, setNewData] = useState([]);
useEffect(() => {
const newOptions = options.map((data, index) => ({
label: data.label,
value: data.id,
}));
setNewData(newOptions);
}, [options]);
return (
<React.Fragment>
<Controller
as={ReactSelect}
name={name}
control={control}
label={label}
defaultValue={[]}
{...props}
options={newData}
/>
</React.Fragment>
);
}
export default FormSelectAutoComplete;
Save the code, run the app and click on SUBMIT button. Your webpage will look like
Pre populating form field data
There is always a scenario, where the form fields need to be pre-populated with some data e.g An Edit case of a web form.
React Hook Forms provide us with a method setValue
to do that.
setValue("name", "Ammar");
- Here
setValue
is the function which accepts two parameters.name
is the name of the field, "Ammar" is the value of the field to be set. -
setValue
function comes frommethod
object ofuseForm
function.
const methods = useForm();
const {setValue} = methods;
Github repo
I have created few more form components like Date Picker, Radio Buttons and Checkbox and have also shown validation for the date as well. Also, all of the code in this tutorial is present in the repo. You can use this repo as a reference or directly use the code in your project.
Repo
Top comments (10)
Thanks for sharing this detailed guide! I have just discovered react-hook-form. I haven't tried it out yet, but reading from the docs and tutorials, I like that it doesn't re-render as much as other libraries do. The re-rendering part is something I wished was addressed "out of the box" in other libraries.
You said that you have forms containing many fields. Do you also use 3rd-party UI integrations there?
Hi @leslie
Apologies for my very very late response.
Yes I'm using the 3rd party UI integrations. This tutorial is my actual workflow in the projects that I have developed and they are being used by my clients in production.
3rd party UI libraries that I have used is : Material UI, React-Select and Material-UI pickers dev. (Date and Time)
If you go to my github repo there you will find all the above mentioned integration.
Let me know if you need some more info/help in these.
I thank you very much for your time in going through my article.
Thank you for your post! I am having an issue with @hookform/resolvers in the src folder. I am getting the following error and would love advice on how to troubleshoot. I have installed @hookform/resolvers: Module not found: Can't resolve '@hookform/resolvers' in '/home/mandi/mandi/material-ui-reacthookform/src'
Should be
import { yupResolver } from "@hookform/resolvers/yup";
Thanks for this. How would you integrate the
register
method? I found, useinputRef={register}
in the TextField, no need forController
.Hi Ndrean
Thank you for your time in reading this. I apologize for my very very delayed response.
register
method can be used with any UI element that exposes aref
forregister
method. The reason I usedController
is because if you are using a Controlled component, then your workflow becomes very easy to integrate.The form controls that I have created you can easily swap it with any other 3rd party UI library instead of Material UI and the core logic with "React Hooks Form" library will remain the same.
With this view in mind I have used the
Controller
component.Check out this wicked implementation: github.com/redwoodjs/redwood/blob/...
Redwood forms guide:
redwoodjs.com/tutorial/everyone-s-...
Select How to use onChange?
const { label, name, options, required, errorobj, onChange } = props;
onChange={(e) => {
onChange(e)
}}
Hi Snruline
I'm very very sorry for the delayed response.
Can you please elaborate in detail where do you want to use
onChange
?