React Hook Form & Material UI: errors are undefined - reactjs

I'm using React Hook Form v7 with Material UI v5. I have a simple form component as shown below. I have marked the TextField as required, however, the error state and helper text are never shown when the value of the field is empty. The value of errors.title is always undefined.
What am I doing wrong here? How can I get the React Hook Form validation working, i.e. errors?
import React from 'react';
import Grid from '#mui/material/Grid';
import TextField from '#mui/material/TextField';
import Button from '#mui/material/Button';
import { useForm, Controller } from 'react-hook-form';
import backEndApi from '../api/backEndApi';
const UserForm = function () {
const { control, handleSubmit, formState: { errors }, getValues } = useForm();
const onFormSubmit = (event) => {
event.preventDefault();
backEndApi.post(getValues());
};
return (
<Grid container alignItems="center" justify="center" direction="column" paddingTop="10%">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Grid container alignItems="center" justify="center" direction="column" spacing={5}>
<Grid item>
<Controller
name="title"
control={control}
rules={{ required: true }}
render={({ field }) => {
return (
<TextField
{...field}
id="title"
label="Title"
required
error={errors.title}
helperText={errors.title && 'Title is required'}
/>
);
}}
/>
</Grid>
<Grid item>
<Button id="job-submit" color="primary" type="submit" variant="contained">
Create Job
</Button>
</Grid>
</Grid>
</form>
</Grid>
);
};
export default UserForm;

I think rules conflicts with required of TextField.
I confirmed it works. (https://codesandbox.io/s/practical-chebyshev-9twrle?file=/src/userForm.js)
<Controller
name="title"
control={control}
rules={{ required: true }}
render={({ field }) => {
return (
<TextField
{...field}
id="title"
label="Title"
error={Boolean(errors.title)}
helperText={errors.title && "Title is required"}
/>
);
}}
/>
Also you don't have to wrap with Controller for such a simple case.
Example: https://codesandbox.io/s/practical-chebyshev-9twrle?file=/src/userFormWithoutController.js

Related

How can I use useForm's register with dynamic parameter?

I want to create a form by using react-hook-form, but I am having trouble with it. As I found, there was a big change at v7 so the code I used as a reference is not working. I tried to modify it, and I figured out the problem is with the registering, but I cannot pass that name parameter dynamically.
My main component.
import React from 'react';
import i18next from 'i18next';
import { Button, Grid, Typography } from '#material-ui/core';
import { useForm, FormProvider } from 'react-hook-form';
import { Link } from 'react-router-dom';
import FormInput from './CustomTextField'
const AddressForm = ({ next }) => {
const methods = useForm();
return (
<>
<Typography variant="h6" gutterBottom>
{i18next.t('shipping_address')}
</Typography>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit((data) => {
console.log(data);
next({ ...data })})}>
<Grid container spacing={3}>
<FormInput required register={methods.register} name='lastName' label={i18next.t('last_name')} />
<FormInput required register={methods.register} name='firstName' label={i18next.t('first_name')} />
<FormInput required register={methods.register} name='email' label={i18next.t('mail')} />
<FormInput required register={methods.register} name='zip' label={i18next.t('zip_code')} />
<FormInput required register={methods.register} name='city' label={i18next.t('city')} />
<FormInput required register={methods.register} name='address1' label={i18next.t('address_1')} />
</Grid>
<br />
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button component={Link} to="/cart" variant="outlined">
{i18next.t('back_to_cart')}
</Button>
<Button type="submit" variant="contained" color="primary">
{i18next.t('next_step')}
</Button>
</div>
</form>
</FormProvider>
</>
)
}
export default AddressForm
CustomTextField component:
import React from 'react';
import { TextField, Grid } from '#material-ui/core';
import { useFormContext, Controller } from 'react-hook-form';
const CustomTextField = ({ name, label, register, required}) => {
const { control } = useFormContext();
return (
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={name}
render = {(field) => (
<TextField
{...register({name})} // <--- It is not working like this
label={label}
required={required}
/>
)}
/>
</Grid>
)
}
export default CustomTextField
By the doc: https://react-hook-form.com/api/useform/register
it takes the input field's name as a parameter, if I pass it in as a string, it works fine. How can I pass the name's value as a parameter to the register() function?
The problem is you're mixing RHF's <Controller /> and register. As Mui's <TextField /> is an external controlled component you should use <Controller />. Check the docs here for more info.
const CustomTextField = ({ name, label, register, required}) => {
const { control } = useFormContext();
return (
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={name}
render={({ field: { ref, ...field } }) => (
<TextField
{...field}
inputRef={ref}
label={label}
required={required}
/>
)}
/>
</Grid>
)
}
If you really want to use register here, you have to remove the wrapping <Controller /> and pass a name as a string instead as an object like you are doing right now. But i would recommend to use <Controller /> as with register you are losing the functionality of setting up the correct ref for your <TextField /> input element as it is linked via the inputRef prop instead of using ref which RHF register uses.
const CustomTextField = ({ name, label, register, required}) => {
const { control } = useFormContext();
return (
<Grid item xs={12} sm={6}>
<TextField
{...register(name)}
label={label}
required={required}
/>
</Grid>
)
}

How to display custom <Errormessage> in formik field component and disable the default message

I am making a web project using React, Material, Formik, formik-material-ui.
I have made a Formik form using yup for validation.
const schema = yup.object({
name: yup.string().trim().lowercase().required("Name is required."),
});
<Formik
initialValues={{
name: "",
}}
validationSchema={schema}
>
{({ submitForm, isSubmitting, handleSubmit, handleChange, values }) => (
<Form noValidate onSubmit={handleSubmit}>
<Grid container direction="row" spacing={2}>
<Grid container item xs={12} spacing={4}>
<Grid item xs={4}>
<InputLabel>Patient Name *</InputLabel>
<TextField fullWidth name="name" type="text" />
<InputLabel>Patient ID: P0006</InputLabel>
</Grid>
</Grid>
</Grid>
</Form>
)}
</Formik>
The TextField is a custom component as follows
import React, { Fragment } from "react";
import { Field, ErrorMessage } from "formik";
import { TextField } from "libs/formik-material-ui/src";
const TextFieldStyle = {
padding: 8,
fontSize: "0.75rem",
};
export default React.memo((props: any) => {
return (
<Fragment>
<Field
component={TextField}
inputProps={{
style: TextFieldStyle,
}}
size="small"
margin="none"
variant="outlined"
{...props} // add props at the key to override any user defined similar props
>
{props.children}
</Field>
<ErrorMessage name={props.name}>{(msg) => <div style={{ color: "red", textAlign: "left" }}>{msg}</div>}</ErrorMessage>
</Fragment>
);
});
Since I want to display ErrorMessage of a different style and not the default one, I have added below the field.
But with this approach, the error message is being printed twice.
How can I disable the default message from being printed ?
You can use helperText="" to disabled default message
<Field
component={TextField}
inputProps={{
style: TextFieldStyle,
}}
size="small"
margin="none"
variant="outlined"
helperText=""
{...props} // add props at the key to override any user defined similar props
>
You would need to remove your {msg} and let formik handle the error message for you
And if you want to style the error, use className from formik:
Formik Props Type
export interface ErrorMessageProps {
name: string;
className?: string;
component?: string | React.ComponentType;
children?: ((errorMessage: string) => React.ReactNode);
render?: ((errorMessage: string) => React.ReactNode);
}
So you would need to use it like so
<ErrorMessage name={props.name} className="your-class" />;

Fetching the default value of Material UI Date Picker when used with React Hook Forms

I am building a form using Material UI which contains 3 fields
(1) Date (2) Hours (3) Comments. By default, the MUI Date picker field is defaulting to today's date. so when I populate the remaining fields (i.e. hours and comment) and submit the form, I am not seeing the Date value in my DATA object. instead, I have to explicitly select a calendar date and submit then the DATA object is populating with the date picker value.
this is causing an issue for me while submitting the data to database over a API call. anyone faced this type of issue..? how can i capture the date value properly in this scenario.
below is the code
import React, { useState } from "react";
import Grid from "#material-ui/core/Grid";
import TextField from "#material-ui/core/TextField";
import Button from "#material-ui/core/Button";
import DateFnsUtils from "#date-io/date-fns";
import {
MuiPickersUtilsProvider,
KeyboardTimePicker,
KeyboardDatePicker,
} from "#material-ui/pickers";
import { useForm, Controller } from "react-hook-form";
import AddTime from "./AddTime";
export default function Form({ user }) {
const { handleSubmit, control, reset } = useForm({});
const onSubmit = (data) => console.log(data);
return (
<React.Fragment>
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<Controller
name="date"
control={control}
render={({ field: { ref, ...rest } }) => (
<KeyboardDatePicker
margin="none"
id="date-picker-dialog"
label="Select date"
format="MM/dd/yyyy"
KeyboardButtonProps={{
"aria-label": "change date",
}}
{...rest}
/>
)}
/>
</MuiPickersUtilsProvider>
</Grid>
<Grid item xs={12} sm={6}>
<Controller
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label="Enter Hours"
variant="standard"
size="small"
error={!!error}
helperText={error ? error.message : null}
/>
)}
name="hours"
control={control}
rules={{ required: "Enter Hours" }}
/>
</Grid>
<Grid item xs={12}>
<Controller
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label="Enter Comments if any"
variant="standard"
size="small"
multiline
fullWidth
error={!!error}
helperText={error ? error.message : null}
/>
)}
name="comment"
rules={{ required: "Enter Comments" }}
control={control}
/>
</Grid>
<Grid item xs={12}>
<Button
variant="contained"
color="primary"
type="submit"
size="small"
>
submit
</Button>
<br />
<br />
<Button
variant="contained"
color="primary"
size="small"
onClick={() => {
reset({
date: new Date(),
hours: "",
comment: "",
});
}}
>
Reset Form
</Button>
</Grid>
</Grid>{" "}
</form>
</React.Fragment>
);
}
Below is the screenshot of the console log. when I click on submit, date is showing as undefined
when I manually change the date and submit, then it's populating fine.
Thanks In Advance -
Venkata Penumatsa
The problem is you're not setting a defaultValue for your <Controller /> components. This is required for external controlled components like Material UI components.
You need to either provide defaultValue at the field-level or useForm with defaultValues.
Here is the relevant section in the documentation.

Formik form does not recognize Material ui Button

I am building a login form with Formik with Materail UI, but Formik doesn't recognize Button of Material UI. If I replace the Button with html button everything works. Could anybody explain why it's not working with the Button component. Below is my code:
import React, { ReactElement } from "react";
import TextField from "#material-ui/core/TextField";
import FormControl from "#material-ui/core/FormControl";
import { Formik, Form } from "formik";
import { makeStyles } from "#material-ui/core/styles";
import Button from "#material-ui/core/Button";
import Box from "#material-ui/core/Box";
const useStyles = makeStyles((theme) => ({
root: {
margin: theme.spacing(1),
width: 200,
display: "flex",
},
input: {
marginTop: 5,
marginBottom: 5,
},
}));
interface Props {}
export default function loginForm({}: Props): ReactElement {
const classes = useStyles();
return (
<Box className={classes.root}>
<Formik
initialValues={{ username: "", password: "" }}
validate={(values) => {
const errors: { username?: string; password?: string } = {};
if (!values.username) {
errors.username = "Required";
} else if (!values.password) {
errors.password = "Required";
}
return errors;
}}
onSubmit={(values, { setSubmitting }) => {
console.log("values: ", values);
setSubmitting(false);
}}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
}) => (
<form onSubmit={handleSubmit}>
<FormControl>
<TextField
className={classes.input}
error={errors.username ? true : false}
type="username"
name="username"
onChange={handleChange}
onBlur={handleBlur}
value={values.username}
label="Username"
variant="outlined"
helperText={
errors.username && touched.username && errors.username
}
/>
<TextField
className={classes.input}
error={errors.password ? true : false}
type="password"
name="password"
onChange={handleChange}
onBlur={handleBlur}
value={values.password}
label="Password"
variant="outlined"
helperText={
errors.password && touched.password && errors.password
}
/>
</FormControl>
<Box display="flex" justifyContent="space-between">
<Button
variant="contained"
color="primary"
disabled={isSubmitting}
>
Submit
</Button>
<Button
variant="contained"
color="secondary"
disabled={isSubmitting}
>
Cancel
</Button>
</Box>
</form>
)}
</Formik>
</Box>
);
}
please note the code above doesn't work, but if you replace Button with button, the form works.
In most browsers, a HTML button by default has the type=submit which means that Formik's submit handler will be called. A Material-UI button does not have this default so the submit handler will never be called. Try adding type=submit to your <Button> props.
(Also, check out Formik's Material-UI integration examples)
You should add the id to the form.
<form onSubmit={handleSubmit} id="myForm">
And bind the form id with submit button
<Button
type="submit"
form="myForm"
>

ForwardRef warning React-hook-forms with Material UI TextField

I am trying to build a form with react-hook-forms with Material UI's inputs (my custom variant of TextField in this case). Although the form seems to work completely fine, it triggers a warning message in the console when rendering the form.
Warning: Function components cannot be given refs. Attempts to
access this ref will fail. Did you mean to use React.forwardRef()?
I am using react-hook-form's Controller to wrap my TextField (as suggested by the docs)
Any suggestions or solutions are very welcome!
Below both the TextField component and the form where this issue occurs:
Component TextField
const TextField = props => {
const {
icon,
disabled,
errors,
helperText,
id,
label,
value,
name,
required,
...rest
} = props;
const classes = useFieldStyles();
return (
<MuiTextField
{...rest}
name={name}
label={label}
value={value || ''}
required={required}
disabled={disabled}
helperText={helperText}
error={errors}
variant="outlined"
margin="normal"
color="primary"
InputProps={{
startAdornment: icon,
classes: {
notchedOutline: classes.outline,
},
}}
InputLabelProps={{
className: classes.inputLabel,
}}
/>
)
};
TextField.propTypes = {
icon: PropTypes.node,
disabled: PropTypes.bool,
label: PropTypes.string,
id: PropTypes.string,
value: PropTypes.any,
required: PropTypes.bool,
helperText: PropTypes.string,
};
export default TextField;
Component LoginForm
const LoginForm = () => {
const { handleSubmit, errors, control } = useForm();
const onSubmit = values => console.log(values);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Typography variant="h5" color="primary" gutterBottom>
Login
</Typography>
<Box py={3} height="100%" display="flex" flexDirection="column">
<Controller
as={TextField}
label="Username"
name="username"
control={control}
errors={errors}
required
/>
<Controller
as={TextField}
label="Password"
type="password"
name="password"
control={control}
errors={errors}
required
/>
<Link>
Forgot your password?
</Link>
</Box>
<Button variant="contained" color="primary" fullWidth type="submit">
Submit
</Button>
</form>
)
};
Try to use Controller's render prop instead of as, because TextField's exposed ref is actually called inputRef, while Controller is trying to access ref.
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { useForm, Controller } from "react-hook-form";
import Header from "./Header";
import { TextField, ThemeProvider, createMuiTheme } from "#material-ui/core";
import "react-datepicker/dist/react-datepicker.css";
import "./styles.css";
import ButtonsResult from "./ButtonsResult";
let renderCount = 0;
const theme = createMuiTheme({
palette: {
type: "dark"
}
});
const defaultValues = {
TextField: "",
TextField1: ""
};
function App() {
const { handleSubmit, reset, control } = useForm({ defaultValues });
const [data, setData] = useState(null);
renderCount++;
return (
<ThemeProvider theme={theme}>
<form onSubmit={handleSubmit((data) => setData(data))} className="form">
<Header renderCount={renderCount} />
<section>
<label>MUI TextField</label>
<Controller
render={(props) => (
<TextField
value={props.value}
onChange={props.onChange}
inputRef={props.ref}
/>
)}
name="TextField"
control={control}
rules={{ required: true }}
/>
</section>
<section>
<label>MUI TextField</label>
<Controller
render={(props) => (
<TextField
value={props.value}
onChange={props.onChange}
inputRef={props.ref}
/>
)}
name="TextField1"
control={control}
rules={{ required: true }}
/>
</section>
<ButtonsResult {...{ data, reset, defaultValues }} />
</form>
</ThemeProvider>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
you can click the following link for actual behavior, now with ref assigned properly with Controller, we can successfully focus on the field when there is an error for better accessibility.
https://codesandbox.io/s/react-hook-form-focus-74ecu
The warning is completely right as suggested by the official docs it think you did not reach to the functional components part. Link to the offical docs
You cannot give ref to functional components as they do not have instances
If you want to allow people to take a ref to your function component, you can use forwardRef (possibly in conjunction with useImperativeHandle), or you can convert the component to a class.
You can, however, use the ref attribute inside a function component as long as you refer to a DOM element or a class component like this:
function CustomTextInput(props) {
// textInput must be declared here so the ref can refer to it
const textInput = useRef(null);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}

Resources