
TLDR
If you are curious about the full-working example, it ishere.
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 uponthis great video 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.
exportconstformMachine=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:{asyncsubmitForm(context,event){// Imagine something asynchronous herealert(`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, andSUBMIT_FORM
, which is used to trigger the form submission.
When theSET_FORM_INPUT_VALUE
event is received, thesetFormInputValue
action is executed, which updates the value of the specified field in the form context. When theSUBMIT_FORM
event is received, the form transitions to thesubmitting
state.
When the form is in thesubmitting
state, it invokes a service calledsubmitForm
, which represents the asynchronous submission of the form data to a backend system. When the service is done, it triggers theclearFields
action and transitions back to theediting
state.
Form
Now we can work on the page that will be displaying our form.
typeFormData={firstName:string;lastName:string;};exportdefaultfunctionDefaultValuesExample(){const[state,send]=FormMachineReactContext.useActor();const{handleSubmit,formState:{errors},control,}=useForm<FormData>({defaultValues:{firstName:"",lastName:""},});return(<formonSubmit={handleSubmit((data)=>{send({type:"SUBMIT_FORM"});})}><Controllercontrol={control}name="firstName"rules={{required:{value:true,message:"First name is required."},}}render={({field:{onChange,value}})=>{return(<inputplaceholder="First name"onChange={({currentTarget:{value}})=>{onChange(value);send({type:"SET_FORM_INPUT_VALUE",key:"firstName",value:value,});}}value={value}/>);}}/>{errors.firstName&&<span>Thisfieldisrequired</span>}<Controllercontrol={control}name="lastName"rules={{required:{value:true,message:"Las name is required."},}}render={({field:{onChange,value}})=>{return(<inputplaceholder="Last name"onChange={({currentTarget:{value}})=>{onChange(value);send({type:"SET_FORM_INPUT_VALUE",key:"lastName",value,});}}value={value}/>);}}/>{errors.lastName&&<span>Thisfieldisrequired</span>}<inputtype="submit"/></form>);}
The first step is to use the new xState utilitycreateActorContext
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 anerrors
object that we can use to display any errors if the validation rules are not met, as well as ahandleSubmit
function that is responsible for targeting the submitting state of the form machine. For now, we only pass the default values of thefirstName
andlastName
fields.
Since we have decided to work with controlled inputs, using theController
component will give us the most benefit from the library.
Each field is rendered using theController
component, which integrates the react-hook-form library with the state machine. The name prop of theController
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.
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 theclearFields
action. However, the inputs still hold the values that we've typed in. It appears that thereact-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 anuseEffect
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 thesetValue
method provided by react-hook-form.
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 theuseForm
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 newvalues
option into our example, we can simply pass the machine context values to thevalues
prop. Here's the updated code:
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 thedefaultValues
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 theuseEffect
because any updates to our machine context are automatically reflected in the form component, without the need to synchronise values.
Lastly, ouronChange
event handler from the Controller component is now much cleaner and safer:
onChange={({currentTarget:{value}})=>{// no need to update the input values explicitlysend({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.
Top comments(2)

- LocationLagos, Nigeria
- WorkSoftware Engineer at Andela
- Joined
Thanks for sharing your thoughts. I also lean towards using react-hook-form for managing form state, primarily for tasks like validation, while utilizing the state machine for other specific tasks, such as controlling the editing and submission behavior. Regarding your concern about keeping the state synchronized upon submission, the reset utility from useForm() can be quite handy. Additionally, to maintain cleaner code, you might consider replacing individual send() calls in the onChange handlers of each field with the watch utility from useForm(). This, combined with useEffect, can efficiently monitor overall form changes.
Your article is well-composed and effectively conveys the concept. These are just a few suggestions I have. 😊
For further actions, you may consider blocking this person and/orreporting abuse