reactjsreact-hooksyupreact-hook-formfluentui-react

Support callback for changing another field value when using React Hook Form validation


TL;DR

This works: https://codesandbox.io/s/stoic-beaver-ucydi

After refactor with React Hook Form this does not work: https://codesandbox.io/s/objective-cloud-bkunr?file=/src/ControlledTextField.tsx


Long story

Without React Hook Form (works OK)

I've recently built a stateful React form using Fluent UI and wrapped fields in custom components.

I've included a feature where the value in Site URL field is generated as you type in Site Title field (it simply copies the field value and removes characters invalid for a URL in my case).

The (simplified) code was working nicely and looked like this:

import * as React from 'react';
import {useState} from 'react';
import { PrimaryButton } from 'office-ui-fabric-react';
import SiteTitleField from '../../../common/formFields/SiteTitleField';
import SiteUrlField from '../../../common/formFields/SiteUrlField';

export default function MyForm(props) {

  const urlPrefix: string = "https://" + window.location.hostname + "/sites/";

  const [siteTitle, setSiteTitle] = useState();
  const [titleErrorMessage, setTitleErrorMessage] = useState('');

  const [siteUrl, setsiteUrl] = useState();
  const [urlErrorMessage, setUrlErrorMessage] = useState('');

  function handleTitleChange(e) {
    if (e.target.value.length) {
      setTitleErrorMessage('');
    } else {
      setTitleErrorMessage('This field is required.');
    }
    setSiteTitle(e.target.value);
    setsiteUrl(e.target.value.replace(/[^A-Za-z0-9_-]/g, ""));
  }
  
  function handleUrlChange(e) {
    if (e.target.value.length) {
      setUrlErrorMessage('');
    } else {
      setUrlErrorMessage('This field is required.');
    }
    setsiteUrl(e.target.value);
  }
  
  function handleButtonClick(e) {
    // call to API
  }

  return (
    <SiteTitleField
      siteTitle={siteTitle}
      titleErrorMessage={titleErrorMessage}
      handleTitleChange={handleTitleChange}
    />

    <SiteUrlField
      siteUrl={siteUrl}
      urlErrorMessage={urlErrorMessage}
      urlPrefix={urlPrefix}
      handleUrlChange={handleUrlChange}
    />

    <PrimaryButton 
      text="Create a Request" 
      onClick={handleButtonClick}
    />
  );
}

SiteTitleField component:

import * as React from 'react';
import { TextField } from 'office-ui-fabric-react/lib/TextField';

export default function SiteTitleField(props) {
  return (
    <TextField 
      value={props.siteTitle}
      required 
      aria-required="true"
      errorMessage={props.titleErrorMessage}
      label="Site Title" 
      placeholder="Set the title of the site"
      onChange={props.handleTitleChange}
    />
  );
}

SiteUrlField component:

import * as React from 'react';
import { TextField } from 'office-ui-fabric-react/lib/TextField';

export default function SiteUrlField(props) {
  return (
    <TextField
      value={props.siteUrl}
      required
      aria-required="true"
      errorMessage={props.urlErrorMessage}
      label="Site URL"
      prefix={props.urlPrefix}
      placeholder="Set site URL alias"
      onChange={props.handleUrlChange}
    />
  );
}

With React Hook Form (not working properly)

Now I'm trying to refactor my form using React Hook Form and Yup validation schema.

I've wrapped Fluent UI TextField component with React Hook Form Controller component and its render property:

import * as React from 'react';
import { Control, Controller, FieldErrors } from "react-hook-form";
import { TextField } from 'office-ui-fabric-react';

export interface IControlledTextFieldProps {
    control: Control<any>;
    name: string;
    errors: FieldErrors<any>;
    label?: string;
    prefix?: string;
    placeholder?: string;

    onChangeCallback?: (...event: any[]) => void;
    refValue?: string;
}
  
export const ControlledTextField: React.FC<IControlledTextFieldProps> = ({
  control,
  name,
  errors,
  label,
  prefix, 
  placeholder,

  onChangeCallback,
  refValue,

}) => {
  return (
    <Controller
      name={name}
      control={control}
      disabled={disabled}
      render={({ onChange, onBlur, value, name: fieldName }) => (
        <TextField

          onChange={(event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {onChange(refValue); onChangeCallback && onChangeCallback(event);}}
          value={refValue}

          onBlur={onBlur}
          name={fieldName}
          errorMessage={errors[fieldName] && errors[fieldName].message}
          label={label}
          prefix={prefix}
          placeholder={placeholder}
        />
      )}
    />
  );
};

I've replaced the code of SiteTitleField and SiteUrlField accordingly and added a simple Yup validation schema:

const schema = yup.object().shape({
  siteTitle: yup.string().required("Site Title needs to be provided."),
  siteUrl: yup.string().required("Site URL needs to be provided."),
});

const { handleSubmit, errors, control } = useForm<Inputs>({
  resolver: yupResolver(schema)
});

I've wrapped the form with <form> tag and changed the field properties accordingly:

<form onSubmit={handleSubmit(handleButtonClick)}>
    <SiteTitleField
      name="siteTitle"
      control={control}
      errors={errors}
      handleTitleChange={handleTitleChange}
    />
    
    <SiteUrlField
      name="siteUrl"
      control={control}
      errors={errors}
      siteUrl={siteUrl}
      urlPrefix={urlPrefix}
    />
        
    <PrimaryButton 
      text="Create a Request" 
      type="submit"
    />
</form>

Regarding the state I left only the things needed for value copying:

  const [siteUrl, setsiteUrl] = useState();
  
  function handleTitleChange(e) {
    setsiteUrl(e.target.value.replace(/[^A-Za-z0-9_-]/g, ""));
  }

The Problem

I cannot make React Hook Form validation and my value copying feature work at the same time.

Either the validation works great but the user cannot edit the Site URL field, when using this code:

onChange={(event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {onChange(refValue); onChangeCallback && onChangeCallback(event);}}
value={refValue}

or copying and editing field values works great but even with the values entered the validation says that both fields are empty (required), when using this code:

onChange={onChangeCallback}
value={refValue}

Solution

  • Ok, I figured that out.

    Instead of using state to update the field value

      const [siteUrl, setsiteUrl] = useState();
      
      function handleTitleChange(e) {
        setsiteUrl(e.target.value.replace(/[^A-Za-z0-9_-]/g, ""));
      }
    

    I should be using useForm's setValue:

      const { 
        handleSubmit, 
        errors, 
        control, 
        setValue // added
      } = 
      useForm<Inputs>({
        resolver: yupResolver(schema)
      });
    
      function handleTitleChange(e) {
        // changed:
        setValue("siteUrl", e.target.value.replace(/[^A-Za-z0-9_-]/g, ""), {
          shouldValidate: true
        });
      }
    

    and the value in ControlledTextField should be resolved simply as:

    value={value}
    

    Working solution: https://codesandbox.io/s/focused-montalcini-ehbp3?file=/src/App.tsx