Search code examples
reactjstypescriptselectonchangereact-hook-form

react-hook-form overwrites onChange function and updates value late


I made a customized Select component with useState and onChange. And I tried to use it with react-hook-form.

Here's the code for Select component.

const Select = forwardRef<HTMLInputElement, ComponentPropsWithoutRef<"input">>((props, ref) => {
  const internalRef = useRef<HTMLInputElement>(null);
  const [fruit, setFruit] = useState("");
  const [isOpen, setIsOpen] = useState(false);

  useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
    ref,
    () => internalRef.current,
    []
  );

  const onClickFruit = (e: React.MouseEvent<HTMLLIElement>) => {
    const fruit = e.currentTarget.textContent as string;
    setFruit(fruit);
    setIsOpen(false);
  };

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFruit(e.target.value);
  };

  return (
    <div>
      <div onClick={() => setIsOpen((prev) => !prev)}>
        <input 
          id="input"
          type="text"
          ref={internalValue} 
          value={fruit} 
          // problem No.1 comes from below.
          onChange={onChange} 
          {...props} 
        />
      </div>
      {isOpen && (
        <ul>
          <li onClick={onClickFruit}>apple</Li>
          <li onClick={onClickFruit}>peach</Li>
          <li onClick={onClickFruit}>grape</Li>
        </ul>
      )}
    </div>
  );
};

And I used this component in the Form component with react-hook-form as below.

interface FormValue {
  fruit: string;
}

const Form = () => {
  const {register, handleSubmit, formState: {errors}} = useForm<FormValue>({ mode: "onChange" });

  const onSubmit = (e: FieldValues) => {
    console.log(e);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Select {...register("fruit", {
          validate: (value) => value !== "peach" || "No Peach Allowed!",
        })} 
      />
      <button type="submit">submit</button>
    </form>
  );
};

Problems are,

  1. I can only write text in input when {...props} comes before onChange. It seems react-hook-form's onChange overwrites input's onChange. What if I send onChange function as prop and want to let both onChange functions work?
  2. When I select apple and change to peach then submit the form, it logs apple on the console. I expected peach to be logged.
  3. When I select peach or write peach, validation isn't triggered though I am using onChange mode.
  4. error message comes out only after I click the submit button.

How can I solve them? Please help.

p.s. I tried Controller. I could write but it logged {fruit: undefined} when the form was submitted. Even worse, the input's value wasn't updated when I click the fruits in li.


Solution

  • Issue 1: To fix this, destructure props to {onChange, ...restProps} and to input component pass only restProps so that RHF's onChange doesn't override custom onChange

    Issue 2 & 3 : You need to call destructured onChange in both onChange and onClickFruit functions so that RHF will have updated values. So, these functions can be updated as below

    const onClickFruit = (e: React.MouseEvent<HTMLLIElement>) => {
        const fruit = e.currentTarget.textContent as string;
        onChange(fruit)
        setFruit(fruit);
        setIsOpen(false);
      };
    
      const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setFruit(e.target.value);
        onChange(e.target.value);
      };
    

    And update input as below

          <input 
              id="input"
              type="text"
              ref={internalValue} 
              value={fruit}
              onChange={onInputChange} 
              {...restProps} 
            />