Search code examples
reactjsreact-hooksreact-propsreact-hook-form

Ref issue using react-hook-form


I'm trying to create a form validation with react-hook-form in my current project. I've already tried different approaches but always I got errors because of the ref attribute. If I change the <FormField> to input, it starts to work. Any idea how to solve this?

Contact

import React from 'react';
import { useForm } from "react-hook-form";
import FormField from '../../components/FormField';
import Button from '../../components/Button';

const Contact = () => {
    const { handleSubmit, register, errors } = useForm();
    const onSubmit = values => console.log(values);

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <FormField
                name="email"
                onChange={() => { console.log("changed!") }}
                ref={register({
                    required: "Required",
                    pattern: {
                        value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                        message: "invalid email address"
                    }
                })}
            />
            <p style={{ color: "red" }}>
                {errors.email && errors.email.message}
            </p>


            <Button>Submit</Button>
        </form>
    );
};

export default Contact;

FormField

import React from "react";
import PropTypes from "prop-types";
import styled, { css } from "styled-components";


const FormFieldWrapper = styled.div`
  position: relative;
  textarea {
    min-height: 150px;
  }
  input[type="color"] {
    padding-left: 67px;
  }
`;

const Label = styled.label``;

Label.Text = styled.span`
  color: #e5e5e5;
  height: 57px;
  position: absolute;
  top: 0;
  left: 16px;

  display: flex;
  align-items: center;

  transform-origin: 0% 0%;
  font-size: 18px;
  font-style: normal;
  font-weight: 300;

  transition: 0.1s ease-in-out;
`;

const Input = styled.input`
  background: #53585d;
  color: #f5f5f5;
  display: block;
  width: 100%;
  height: 57px;
  font-size: 18px;

  outline: 0;
  border: 0;
  border-top: 4px solid transparent;
  border-bottom: 4px solid #53585d;

  padding: 16px 16px;
  margin-bottom: 45px;

  resize: none;
  border-radius: 4px;
  transition: border-color 0.3s;

  &:focus {
    border-bottom-color: var(--primary);
  }
  &:focus:not([type="color"]) + ${Label.Text} {
    transform: scale(0.6) translateY(-10px);
  }
  ${({ value }) => {
    const hasValue = value.length > 0;
    return (
      hasValue &&
      css`
        &:not([type="color"]) + ${Label.Text} {
          transform: scale(0.6) translateY(-10px);
        }
      `
    );
  }}
`;

function FormField({ label, type, name, value, onChange, ref }) {
  const isTypeTextArea = type === "textarea";
  const tag = isTypeTextArea ? "textarea" : "input";
  return (
    <FormFieldWrapper>
      <Label>
        <Input
          as={tag}
          type={type}
          value={value}
          name={name}
          onChange={onChange}
          ref={ref}
        />
        <Label.Text>{label}:</Label.Text>
      </Label>
    </FormFieldWrapper>
  );
}

FormField.defaultProps = {
  type: "text",
  value: "",
};

FormField.propTypes = {
  label: PropTypes.string,
  name: PropTypes.string.isRequired,
  type: PropTypes.string,
  value: PropTypes.string,
  onChange: PropTypes.func,
  ref: PropTypes.func
};

export default FormField;

Errors: enter image description here


Solution

  • Referring to the docs the register should be used as below so we won't get refs issues and also the register will change the value inside the input so we don't need to pass a value prop :

    Contact :

    import React from "react";
    import { useForm } from "react-hook-form";
    import FormField from "../../components/FormField";
    import Button from "../../components/Button";
    
    const Contact = () => {
      const { handleSubmit, register, errors } = useForm();
      const onSubmit = (values) => console.log("values", values);
    
      return (
        <form onSubmit={handleSubmit(onSubmit)}>
          <FormField
            name="email"
            onChange={() => {
              console.log("changed!");
            }}
            register={register}
          />
          <p style={{ color: "red" }}>{errors.email && errors.email.message}</p>
    
          <Button>Submit</Button>
        </form>
      );
    };
    
    export default Contact;
    

    FormField :

    import React from "react";
    import PropTypes from "prop-types";
    import styled, { css } from "styled-components";
    
    const FormFieldWrapper = styled.div`
      position: relative;
      textarea {
        min-height: 150px;
      }
      input[type="color"] {
        padding-left: 67px;
      }
    `;
    
    const Label = styled.label``;
    
    Label.Text = styled.span`
      color: #e5e5e5;
      height: 57px;
      position: absolute;
      top: 0;
      left: 16px;
    
      display: flex;
      align-items: center;
    
      transform-origin: 0% 0%;
      font-size: 18px;
      font-style: normal;
      font-weight: 300;
    
      transition: 0.1s ease-in-out;
    `;
    
    const Input = styled.input`
      background: #53585d;
      color: #f5f5f5;
      display: block;
      width: 100%;
      height: 57px;
      font-size: 18px;
    
      outline: 0;
      border: 0;
      border-top: 4px solid transparent;
      border-bottom: 4px solid #53585d;
    
      padding: 16px 16px;
      margin-bottom: 45px;
    
      resize: none;
      border-radius: 4px;
      transition: border-color 0.3s;
    
      &:focus {
        border-bottom-color: var(--primary);
      }
      &:focus:not([type="color"]) + ${Label.Text} {
        transform: scale(0.6) translateY(-10px);
      }
      ${({ value = {} }) => { // here you should find an other approch because there is no value props
        const hasValue = value.length > 0;
        return (
          hasValue &&
          css`
            &:not([type="color"]) + ${Label.Text} {
              transform: scale(0.6) translateY(-10px);
            }
          `
        );
      }}
    `;
    
    const FormField = ({ label, type, name, onChange, register }) => {
      const isTypeTextArea = type === "textarea";
      const tag = isTypeTextArea ? "textarea" : "input";
      return (
        <FormFieldWrapper>
          <Label>
            <Input
              as={tag}
              type={type}
              //   value={value} it's not a controlled input! so the register'ill provide the value
              name={name}
              onChange={onChange}
              ref={register({
                required: "Required",
                pattern: {
                  value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                  message: "invalid email address",
                },
              })}
            />
            <Label.Text>{label}:</Label.Text>
          </Label>
        </FormFieldWrapper>
      );
    };
    
    FormField.defaultProps = {
      type: "text",
      value: "",
    };
    
    FormField.propTypes = {
      label: PropTypes.string,
      name: PropTypes.string.isRequired,
      type: PropTypes.string,
      value: PropTypes.string,
      onChange: PropTypes.func,
      ref: PropTypes.func,
    };
    
    export default FormField;