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>
);
}
Related
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>
)
}
I am trying to implement custom input element for TextField component from Material UI
example :
export const InputsPage: React.FC = () => {
const [value, setValue] = useState('');
return (
<Paper>
<Box p={2}>
<TextField
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
color='primary'
label='FROM'
placeholder='Placeholder'
InputProps={{
inputComponent: ({ inputRef, ...rest }) => <input ref={inputRef} {...rest} type='text' />,
}}
/>
</Box>
</Paper>
);
};
because i am using controlled input with my own state the input is not working properly ... each time ill trying to type some thing the input will loss focus so i need to type each char/number and make a click on the input again to make a focus so i can continue typing
if ill use uncontrolled input it will work properly
here is an example what is happening : codeSandbox
The problem is that you are defining inline the component type for the inputComponent prop. This means that with each re-render it will be considered by React to be a new component type, so instead of just re-rendering, the element will be remounted (removed completely from the DOM and re-added) which results in focus being lost.
You can fix this by defining the component type (CustomInputComponent in the example) at the top-level as shown in the example below:
import React, { useState } from "react";
import "./styles.css";
import { TextField } from "#material-ui/core";
const CustomInputComponent = ({ inputRef, ...rest }) => (
<input ref={inputRef} {...rest} type="text" />
);
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<TextField
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
color="primary"
label="FROM"
placeholder="Placeholder"
InputProps={{
inputComponent: CustomInputComponent
}}
/>
</div>
);
}
If you want to use masked input with material ui and form library you could do this:
import React, { memo } from "react";
// MASKED INPUT
import MaskedInput from "react-text-mask";
// MATERIAL UI
import { Phone } from "#mui/icons-material";
import { InputAdornment, TextField } from "#mui/material";
interface MaskedPhoneInputProps {
fieldRef: (ref: HTMLElement | null) => void;
field: any;
errors: any;
}
const MaskedPhoneInput = ({
fieldRef,
field,
errors,
}: MaskedPhoneInputProps) => {
return (
<MaskedInput
{...field}
ref={(ref) => {
fieldRef(ref ? ref.inputElement : null);
}}
mask={[
"0",
"(",
/[1-9]/,
/\d/,
/\d/,
")",
" ",
/\d/,
/\d/,
/\d/,
"-",
/\d/,
/\d/,
/\d/,
/\d/,
]}
guide={false}
keepCharPositions
render={(ref, props) => (
<TextField
inputRef={ref}
{...props}
variant="outlined"
label="Telefon Numarası"
fullWidth
size="small"
placeholder="Telefon numaranızı giriniz"
error={!!errors?.phone}
helperText={errors?.phone?.message}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Phone sx={{ color: "text.disabled" }} />
</InputAdornment>
),
}}
/>
)}
/>
);
};
export default memo(MaskedPhoneInput);
Then you could use it in any controller (formik, react hook form) like this:
<Controller
name="phone"
control={control}
rules={{ required: true }}
render={({ field: { ref, ...rest } }) => (
<MaskedPhoneInput fieldRef={ref} field={rest} errors={errors} />
)}
/>
WARNING
ref={(ref) => {
fieldRef(ref ? ref.inputElement : null);
}}
Defining ref like it is important, if you don't define you may take error about focus like elm.focus is not a function in react hook form.
I am trying to use antd form with react-hook-form but couldn't get it to work.
Basically I am using a theme with antd and want to integrate react-hook-form for better validation and other things. So far this is the code
import { Button, Form, Input, Message, Row, Tooltip } from "antd";
import { useForm, Controller } from "react-hook-form";
import {
EyeTwoTone,
MailTwoTone,
PlaySquareTwoTone,
QuestionCircleTwoTone,
SkinTwoTone,
} from "#ant-design/icons";
import Link from "next/link";
import Router from "next/router";
import styled from "styled-components";
const FormItem = Form.Item;
const Content = styled.div`
max-width: 400px;
z-index: 2;
min-width: 300px;
`;
const Register = (props) => {
const defaultValues = {
name: null,
phone: null,
email: null,
password: null,
confirmPassword: null,
checked: false,
};
const { handleSubmit, reset, watch, control, errors, getValues } = useForm({
defaultValues,
});
// const { register, errors, handleSubmit, control } = useForm({
// mode: 'onChange',
// });
const onSubmit = async (data) => {
try {
console.log("data", data);
} catch (err) {
console.log("err", err);
}
};
return (
<Row
type="flex"
align="middle"
justify="center"
className="px-3 bg-white"
style={{ minHeight: "100vh" }}
>
<Content>
<div className="text-center mb-5">
<Link href="/signup">
<a className="brand mr-0">
<PlaySquareTwoTone style={{ fontSize: "32px" }} />
</a>
</Link>
<h5 className="mb-0 mt-3">Sign up</h5>
<p className="text-muted">create a new account</p>
</div>
<Form layout="vertical" onSubmit={handleSubmit(onSubmit)}>
<Controller
as={
<FormItem
label="Email"
name="email"
rules={[
{
type: "email",
message: "The input is not valid E-mail!",
},
{
required: true,
message: "Please input your E-mail!",
},
]}
>
<Input
prefix={<MailTwoTone style={{ fontSize: "16px" }} />}
type="email"
placeholder="Email"
/>
</FormItem>
}
control={control}
name="select"
/>
{/* <Input inputRef={register} name="input" /> */}
<button type="button" onClick={() => reset({ defaultValues })}>
Reset
</button>
<input type="submit" />
</Form>
</Content>
</Row>
);
};
export default Register;
Now, as you can see I am having regular form tag and inside that the input field. But using the regular form, I am not getting the layout props provided by the antd forms. Also I am not able to get the values during submit.
So my question is that how can I use AntD form component with react hook form so i can use the benefits of react-hook0form as well a Antd styling.
You can simply use the built-in useForm method of ant design, no need to pull in a thirdparty. It looks like:
const [form] = Form.useForm();
Also Form has onFinish method not onSubmit in ant, at least in version 4.
Use controller wrapper from react-hook-form, documentation link
Import Controller
import { useForm, Controller } from 'react-hook-form';
Call control from useForm
const { handleSubmit, control } = useForm();
Your Input
<Form.Item label="Email">
<Controller
name="email"
defaultValue=""
control={control}
render={({ onChange, value }) => (
<Input onChange={onChange} value={value} />
)}/>
</Form.Item>
I am using react-hook-form for form state management in my application. When I am using <Input /> as a control, it works as expected, however with <TextField /> it shows a warning saying "A component is changing an uncontrolled input of type text to be controlled."
What's going wrong here? Is there any alternative for this component?
Here is my react code:
import React from "react";
import "./styles.css";
import { useForm, Controller } from "react-hook-form";
import Joi from "#hapi/joi";
import { TextField, createMuiTheme, ThemeProvider } from "#material-ui/core";
const validationSchema = Joi.object({
username: Joi.string()
.alphanum()
.min(3)
.max(30)
.required()
});
const theme = createMuiTheme({
palette: {
type: "dark"
}
});
const resolver = (data, validationContext) => {
const { error, value: values } = validationSchema.validate(data, {
abortEarly: false
});
return {
values: error ? {} : values,
errors: error
? error.details.reduce((previous, currentError) => {
return {
...previous,
[currentError.path[0]]: currentError
};
}, {})
: {}
};
};
export default function App() {
const { register, handleSubmit, errors, control } = useForm({
validationResolver: resolver,
validationContext: { test: "test" }
});
console.log("error", errors);
return (
<ThemeProvider theme={theme}>
<div className="App">
<h1>Hello CodeSandbox</h1>
<form onSubmit={handleSubmit(d => console.log(d))}>
<label>Username</label>
<Controller as={<input />} name="username" control={control} />
<Controller
as={<TextField />}
name="firstName"
label="First Name"
control={control}
/>
<input type="submit" />
</form>
</div>
</ThemeProvider>
);
}
and here is a link to it in a sandbox: https://codesandbox.io/s/react-hook-form-validationresolver-7k33n
You can fix the warning by supplying default values to your input elements to prevent them from being undefined initially:
<Controller
as={<input />}
name="username"
control={control}
defaultValue=""
/>
<Controller
as={<TextField />}
name="firstName"
label="First Name"
control={control}
defaultValue=""
/>
I'm trying to use react-hook-form together with the antd <Input /> component
I'm not getting reset to work with <Controller />
Here is my code:
const NormalLoginForm = () =>{
const {reset, handleSubmit, control} = useForm();
const onSubmit = handleSubmit(async ({username, password}) => {
console.log(username, password);
reset();
});
return (
<form onSubmit={onSubmit} className="login-form">
<Form.Item>
<Controller as={<Input
prefix={<Icon type="user" style={{color: 'rgba(0,0,0,.25)'}}/>}
autoFocus={true}
placeholder="Benutzername"
/>} name={'username'} control={control}/>
</Form.Item>
<Form.Item>
<Controller as={<Input
prefix={<Icon type="lock" style={{color: 'rgba(0,0,0,.25)'}}/>}
type="password"
placeholder="Passwort"
/>} name={'password'} control={control}/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" className="login-form-button">
Log in
</Button>
</Form.Item>
</form>
);
}
I'm expecting that the two input fields are getting cleared when the form is submitted. But that doesn't work.
Am I missing something here?
Example on Stackblitz
https://stackblitz.com/edit/react-y94jpf?file=index.js
Edit:
The RHFInput mentioned here React Hook Form with AntD Styling is now part of react-hook-form and has been renamed to Controller. I'm already using it.
I've figured out that chaning
reset();
to
reset({
username:'',
password:''
});
solves the problem.
However - I wanted to reset the whole form without explicitly assigning new values.
Edit 2:
Bill has pointed out in the comments that it's almost impossible to detect the default values for external controlled inputs. Therefore we're forced to pass the default values to the reset method. That makes totally sense to me.
You must wrapper the components for antd and create a render component, it is very similar if you use Material UI, So the code can be like:
import { Input, Button } from 'antd';
import React from 'react';
import 'antd/dist/antd.css';
import {useForm, Controller} from 'react-hook-form';
const RenderInput = ({
field: {
onChange,
value
},
prefix,
autoFocus,
placeholder
}) => {
return (
<Input
prefix={prefix}
autoFocus={autoFocus}
placeholder={placeholder}
onChange={onChange}
value={value}
/>
);
}
export const NormalLoginForm = () =>{
const {reset, handleSubmit, control} = useForm();
const onSubmit = ({username, password}) => {
console.log(username, password);
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="login-form">
<Controller
control={control}
name={'username'}
defaultValue=""
render={ ({field}) => (
<RenderInput
field={field}
autoFocus={true}
placeholder="Benutzername"
/>
)}
/>
<Controller
render={ ({field}) => (
<RenderInput
field={field}
type="password"
placeholder="Passwort"
/>
)}
defaultValue=""
name={'password'}
control={control}
/>
<Button
type="primary"
htmlType="submit"
className="login-form-button"
>
Log in
</Button>
</form>
);
}
export default NormalLoginForm;
You can notice that I did't put the Icon ,it was because I tested using the version 4 for antd and something change in how to use the icons.