How to use React Hook Form together with Fluent UI React (aka Office UI Fabric React)

Very often you need some fields in your SPFx web parts. Like text fields, dropdowns, checkboxes, etc. While you can manually perform form validation and form data collection, you can also use a helper library. React form libraries simplify a lot of things, however, they also require some time to learn the API and to adapt a library to your UI framework. 

React Hook Form (RHF) is one of such libraries. It's based solely on react hooks and gives a nicer way of managing and validating your forms, no matter which UI framework you use. In SPFx we mostly use Fluent UI React (formerly Office UI Fabric). In this post, I'm going to show how you can configure React Hook Form so that it plays nicely with Fluent UI.

The source code for this post is available on GitHub here

IMPORTANT. The CodeSandboxes were updated to match the latest react-hook-form 7. However, as of now (Apr 2021) we cannot use react-hook-form 7 together with SharePoint Framewrok because of the dependency on TypeScript 4 from react-hook-form. Thus the code sample on GitHub is based on react-hook-form 6 (I will update it when SPFx supports TypeScript 4).

Register with React Hook Form

In the simplest scenario, you just need to supply

ref={register}

for your field component. In Fluent UI you have a componentRef property, however, it doesn't work well in all situations. 

RHF provides different ways to integrate with any UI library. The most common is using a special Controller component. Think of it as a component, which takes care of "value" management for your field - updating, change, blue, validation. Yet you have to configure everything correctly. 

Let's use TextField as an example. RHF supports different form validation modes. OnChange, OnBlur, OnSumbit, or just all of them. If we want to support it, then we should think about:

  • how to notify RHF about updated (changed) value
  • how to notify about blur event
  • how to display the error message from RHF, if validation fails
  • how to set the initial value

Fortunately, Fluent UI provides the needed methods and events to handle all the above scenarios. 

Let's start with an interface, which defines all props for our Controlled TextField: 

export interface HookFormProps {
  control: Control<any>;
  name: string;
  rules?: UseControllerProps["rules"];
  defaultValue?: any;
}

These properties are used inside RHF's Controller component. control and errors come from useForm hook, everything else is controlled by a developer. 

The sample configuration for TextField might be:

export const ControlledTextField: FC<HookFormProps & ITextFieldProps> = (
  props
) => {
  return (
    <Controller
      name={props.name}
      control={props.control}
      rules={props.rules}
      defaultValue={props.defaultValue || ""}
      render={({
        field: { onChange, onBlur, name: fieldName, value },
        fieldState: { error }
      }) => (
        <TextField
          {...props}
          onChange={onChange}
          value={value}
          onBlur={onBlur}
          name={fieldName}
          errorMessage={error && error.message}
          defaultValue={undefined}
        />
      )}
    />
  );
};

Let's take a closer look. For Controller we should specify some required properties:

  • name - form field name
  • control - one of the useForm hook return values
  • rules - a set of rules for validating this field. A developer specifies how to validate it
  • defaultValue is a self-explanatory 

Then we use the "render props" pattern to inject Fluent UI's TextField component. RHF provides some useful methods to be attached to the controlled component. These are onChange, onBlur. We use corresponding TextField properties to map RHF methods. defaultValue should be undefined in all cases because we use TextField in a controlled mode. And finally, errorMessage is a way to dynamically and natively present a validation error. 

Use controlled TextField component in code

Below is the code sandbox, which gives you an example of how you can use above component: 

This sample introduces a single required text field with a custom error message. 

All validation happens by Save click. handleSubmit method accepts two callbacks - the first one for a successful validation, the second one if validation fails. If your field needs validation, you should supply the required rules through the rules property. Read here about supported rules. 

Validate form fields with Yup

Sometimes it's more convenient to perform a form validation using a separate helper library (especially if you have a form-heavy solution). You can use Yup for that purpose together with RHF and Fluent UI. 

Here is another sample, which uses Yup for validation:

The key difference from the previous example is, that we don't use rules property here, but instead rely solely on Yup rules and validation logic, which in some cases is more powerful than RHF. 

Bigger example

I created a code sandbox with different controls and validation scenarios in order to cover more real-life situations. Sure, it's not the full list, but it least it gives you an idea, how to perform form validation using various conditions:

Title image attribution: Certificate vector created by vectorjuice - www.freepik.com