Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Form validation with xState and react-hook-form
Georgi Todorov
Georgi Todorov

Posted on

     

Form validation with xState and react-hook-form

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}`);},},});
Enter fullscreen modeExit fullscreen mode

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>);}
Enter fullscreen modeExit fullscreen mode

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,});}}
Enter fullscreen modeExit fullscreen mode

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]);
Enter fullscreen modeExit fullscreen mode

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,},});
Enter fullscreen modeExit fullscreen mode

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,});}}
Enter fullscreen modeExit fullscreen mode

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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
johnkingzy profile image
Kingsley Solomon
Software Engineer || passionate about design patterns, complex algorithms, microservice arch., data structure and UI/UX
  • Location
    Lagos, Nigeria
  • Work
    Software 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. 😊

CollapseExpand
 
gtodorov profile image
Georgi Todorov
Software developer.
  • Location
    Sofia, Bulgaria
  • Joined

Thank you for your comment! I like the idea of involving thewatch method in the flow, might try it in another blog post.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Software developer.
  • Location
    Sofia, Bulgaria
  • Joined

More fromGeorgi Todorov

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp