react-hook-form: idiomatic approach for two-way mapping between form state and input state

Viewed 614

I'm currently using Formik for my forms but considering switching to react-hook-form. One of my concerns is with inputs whose DOM state is not identical to the intended DOM state. One example is inputs with type="number", whose value at the DOM level is a string. So I might want to map a string to a number. In Formik:

<input
  type="number"
  value={formikProps.values.myNumericField ?? ''}
  onChange={(event) => {
    formikProps.setFieldValue(
      'myNumericField',
      event.currentTarget.value === '' ? null : parseInt(event.currentTarget.value)
    );
  }}
/>

What is the idiomatic way of doing this in react-hook-form?

1 Answers

You can use a controlled input instead and wrap it in react-hook-form's Controller.

First, create your usual controlled input, which takes at least 2 props, value and onChange, and notifies the parent about changes:

onst NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(({
  onChange,
  ...inputProps
}, ref) => {
  const handleChange = useCallback((e) => { 
    const { value } = e.target;
    const parsedValue = parseInt(value, 10);

    // If the value is not a number, we don't want to clear the input. Instead, 
    // just use the original string value and handle validation outside the input.
    // You can be more strict about this by passing 0 or '' instead and changing
    // the type attribute below to "number".
    onChange(value === `${ parsedValue }` ? parsedValue : value);
  }, [onChange]);

  return (
    <input
      ref={ ref }
      type="text"
      onChange={ handleChange }
      { ...inputProps } />
  );
});

To use this component with react-hook-form, simply wrap it in a Controller component, which will pass onChange, onBlur, value and ref to your component:

import ReactDOM from "react-dom";
import React, { useCallback } from "react";
import { useForm, Controller } from "react-hook-form";

const App: React.FC = () => {
  const { handleSubmit, control, watch } = useForm();

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <Controller
        as={NumberInput}
        control={control}
        name="number"
        defaultValue=""
      />

      <pre>{ JSON.stringify(watch(), null, "  ") }</pre>

      <button type="submit">Submit</button>
    </form>
  );
};

const rootElement = document.getElementById("root");

ReactDOM.render(<App />, rootElement);
Related