Form validation with xState and react-hook-form
Tags: xstate, reacthookform, webdev, nextjs
Published: 2023-04-07 20:11:10

###TLDR
If you are curious about the full-working example, it is [here](https://github.com/g-todorov/xstate-validation-example).

###Background
The last couple of projects I worked on, our team had the resources to create its own form validation system. We had custom React hook, xState machines and all validating methods written ourselves. However, my current project has a smaller team and we rely more on established open source solutions. 
As xState is our state management tool of choice, I conducted some quick research to find a compatible validation library. I stumbled upon [this great video](https://www.youtube.com/watch?v=Xa0H-vf2VuQ) and decided that will give react-hook-form a go for our forms validation.

###Use case
To demonstrate the process, we will introduce a simple form that consists of just two required fields and a submit button. To provide a more realistic example, we will pass an xState actor that will be responsible for controlling the form on the respective page.

###Initial implementation
I always prefer to use controlled inputs whenever possible. I find it more comfortable when the field value is stored and ready to react to any external event.

#### Machine
This means that we now need a machine that will take care of the form operations.

```javascript
export const formMachine = createMachine(
  {
    id: "formMachine",
    context: { form: { firstName: "", lastName: "" } },
    initial: `editing`,
    states: {
      editing: {
        on: {
          SET_FORM_INPUT_VALUE: { actions: ["setFormInputValue"] },
          SUBMIT_FORM: { target: "submitting" },
        },
      },
      submitting: {
        invoke: {
          src: "submitForm",
          onDone: { actions: ["clearFields"], target: "editing" },
        },
      },
    },
  },
  {
    actions: {
      setFormInputValue: assign((context, event) => {
        return {
          ...context,
          form: {
            ...context.form,
            [event.key]: event.value,
          },
        };
      }),
      clearFields: assign((context, event) => {
        return { form: { firstName: "", lastName: "" } };
      }),
    },
    services: {
      async submitForm(context, event) {
        // Imagine something asynchronous here
        alert(
          `First name: ${context.form.firstName} Last name: ${context.form.lastName}`
        );
      },
    },
  }
);
```

You may find this self-explanatory but let's have a few words about the code above. The machine represents a simple form with two fields, and two states, editing and submitting.

When the form is in the editing state, it can receive two types of events: `SET_FORM_INPUT_VALUE`, which is used to update the value of a field, and `SUBMIT_FORM`, which is used to trigger the form submission. 

When the `SET_FORM_INPUT_VALUE` event is received, the `setFormInputValue` action is executed, which updates the value of the specified field in the form context. When the `SUBMIT_FORM` event is received, the form transitions to the `submitting` state.

When the form is in the `submitting` state, it invokes a service called `submitForm`, which represents the asynchronous submission of the form data to a backend system. When the service is done, it triggers the `clearFields` action and transitions back to the `editing` state.

#### Form
Now we can work on the page that will be displaying our form.

```javascript
type FormData = {
  firstName: string;
  lastName: string;
};

export default function DefaultValuesExample() {
  const [state, send] = FormMachineReactContext.useActor();

  const {
    handleSubmit,
    formState: { errors },
    control,
  } = useForm<FormData>({
    defaultValues: { firstName: "", lastName: "" },
  });

  return (
    <form
      onSubmit={handleSubmit((data) => {
        send({ type: "SUBMIT_FORM" });
      })}
    >
      <Controller
        control={control}
        name="firstName"
        rules={{
          required: { value: true, message: "First name is required." },
        }}
        render={({ field: { onChange, value } }) => {
          return (
            <input
              placeholder="First name"
              onChange={({ currentTarget: { value } }) => {
                onChange(value);
                send({
                  type: "SET_FORM_INPUT_VALUE",
                  key: "firstName",
                  value: value,
                });
              }}
              value={value}
            />
          );
        }}
      />
      {errors.firstName && <span>This field is required</span>}

      <Controller
        control={control}
        name="lastName"
        rules={{
          required: { value: true, message: "Las name is required." },
        }}
        render={({ field: { onChange, value } }) => {
          return (
            <input
              placeholder="Last name"
              onChange={({ currentTarget: { value } }) => {
                onChange(value);
                send({
                  type: "SET_FORM_INPUT_VALUE",
                  key: "lastName",
                  value,
                });
              }}
              value={value}
            />
          );
        }}
      />
      {errors.lastName && <span>This field is required</span>}

      <input type="submit" />
    </form>
  );
}
```

The first step is to use the new xState utility `createActorContext` to obtain access to the form machine actor on our page.

Next, we set up the useForm hook from the react-hook-form library, which is its flagship feature. It helps us manage the input state and validation. It returns an `errors` object that we can use to display any errors if the validation rules are not met, as well as a `handleSubmit` function that is responsible for targeting the submitting state of the form machine. For now, we only pass the default values of the `firstName` and `lastName` fields.

Since we have decided to work with controlled inputs, using the `Controller` component will give us the most benefit from the library. 

Each field is rendered using the `Controller` component, which integrates the react-hook-form library with the state machine. The name prop of the `Controller` component specifies the name of the field in the form data object, and the rules prop specifies the validation rules for the field.

#### Downsides
We now have a functional form with validation. While react-hook-form is definitely a developer-friendly solution that saves a lot of work, I still have a couple of concerns.

```javascript
onChange={({ currentTarget: { value } }) => {
  onChange(value);
  send({
    type: "SET_FORM_INPUT_VALUE",
    key: "lastName",
    value,
  });
}}
```

As advised in the react-hook-form documentation, we must update both the validation state and the input state to ensure their values remain in sync. However, in my experience, this process can be error-prone.

To illustrate this point, I just need to run the application. and enter values into the form fields. After clicking the submit button, the correct values are displayed in the alert modal, and the context is cleared using the `clearFields` action. However, the inputs still hold the values that we've typed in. It appears that the `react-hook-form` still keeps its state internally, which is to be expected. Therefore, we must sync the state again to ensure consistency.

Since we need to sync the input values with the form state, we can use an `useEffect` hook that depends on the form machine's state value. If the state is submitting, we can assume that it's safe to clear the inputs using the `setValue` method provided by react-hook-form.

```javascript
const [state, send] = FormMachineReactContext.useActor();

const {
  handleSubmit,
  formState: { errors },
  control,
  setValue,
} = useForm<FormData>({
  defaultValues: { firstName: "", lastName: "" },
});

useEffect(() => {
  if (state.matches("submitting")) {
    setValue("firstName", "");
    setValue("lastName", "");
  }
}, [state.value]);
```

###Improved implementation

Although the validation library integration was quick and reliable, there were some downsides. However, in recent releases of react-hook-form, a new option called values has been added to the `useForm` hook:

> The values props will react to changes and update the form values, which is useful when your form needs to be updated by external state or server data.

To integrate it in our example we simply need to pass the machine context values to the values prop.

To integrate this new `values` option into our example, we can simply pass the machine context values to the `values` prop. Here's the updated code:

```javascript
const [state, send] = FormMachineReactContext.useActor();

const {
  handleSubmit,
  formState: { errors },
  control,
} = useForm<FormData>({
  values: {
    firstName: state.context.form.firstName,
    lastName: state.context.form.lastName,
  },
});
```

I find the `defaultValues` prop to be unnecessary for my case, as my form context already has initial values that are directly consumed by the input component.

Additionally, We can eliminate the `useEffect` because any updates to our machine context are automatically reflected in the form component, without the need to synchronise values.

Lastly, our `onChange` event handler from the Controller component is now much cleaner and safer:

```javascript
onChange={({ currentTarget: { value } }) => {
  // no need to update the input values explicitly
  send({
    type: "SET_FORM_INPUT_VALUE",
    key: "firstName",
    value: value,
  });
}}
```

###Conclusion
Both xState and react-hook-form are highly flexible and adaptable, making them suitable for various form validation cases. With xState, you can define and manage the state of your application in a clear and structured way, while react-hook-form provides an easy and efficient way to manage form state and validation. Together, they can streamline your form development process and provide a solid foundation for building reliable and scalable forms.