The incredible secret to building forms in ReactJS

I've tried to follow the React way of building forms and it is a baffling, complex exercise. First there is the initial hurdle of choosing and installing a framework, they all seem to flower and die of obsolete dependencies within months. When you stumble upon one compatible with your project, you find yourself shoehorned into some arbitrary syntax just to output a basic form, and then discover most of it becomes obsolete in the framework's next iteration. Throw in something fundamental like form validation and the wheels fall off the architecture.

Surely there's an easier way?

Consider this article subtitled "A sane approach to Forms in React" (skim but don't read it) ...sane approach, eh? Full of dispatchers, mutations updating state, HigherOrderComponents (what is this, the Illuminati?)... no offence to the author, but this is just making life way harder than it has to be. Now here's something crazy to think about:

You don't need React for forms.

The modern JavaScript DOM has all you need to manipulate forms - the React framework just gets in the way. All that rendering, props, state and mutations are not required on a form because the user is entering their data. It is not your application's data until the user submits it. Sure, you might need to hide or show stuff and do validation, but most of the time you're not looping through thousands of records or writing new html to the screen or crunching complex responses to pre-submitted input data that might necessitate the ReactJS abstraction into props and state. Just let the user fill in the form!

With that in mind, here's a nuts-and-bolts implementation of an extensible form with simple validation.

constructor(){
this.state = {
inError: false;
}
this.handleSubmit = this.handleSubmit.bind();
}
handleSubmit(event){
event.preventDefault();
if (!event.target.checkValidity()) {
this.setState({ inError: true });
return;
}
this.setState({ inError: false });
let data = new FormData(event.target);
fetch('someurl', {'method':'POST','body':data}).then(()=>{ alert('done!') },()=>{ alert('error!') });
}
render() {
return (
<form noValidate aria-live="aggressive" onSubmit={ this.handleSubmit } className={ this.state.inError ? 'displayErrors' : '' }>
<input id="form_surname" type="text" name="surname" required pattern="\w+" />
<label
for="form_surname">Surname<span> requires some letters</span></label/>
<button>OK</button>
</form>
);
}

It looks like a bit is going on here, but most of it just requires ReactJS structures and accessibility helpers. We are using a ReactJS component state to flag the form as invalid, which is useful because it can be used elsewhere by ReactJS when deciding what to do with the form. But until it gets submitted, the rest of the logic is handled purely by HTML DOM, JavaScript & CSS.

First off, we have a simple HTML form in the render() method. The form tag has a "noValidate" attribute to tell the browser not to use it's built-in validation tools, giving us full control over the form. It's also using an aria-live attribute so accessibility readers can respond to changes in the form. The onSubmit attribute captures the form submission event and passes it to our handleSubmit method, where our validation occurs. Lastly it has a class attribute that gets applied if the form is invalid, which our CSS will respond to.

This example form has one text input field. It has the "required" attribute to trigger validation, but it also has a "pattern" attribute to decide if the value should be considered valid. (The "pattern" and "required" attributes also work on <textarea> fields but only "required" works on <select> fields.) Finally I've added an accessibility label with a nested span for the validation message, which our CSS will respond to.

The CSS below is used to highlight errors. It adds a red border to invalid elements and displays the error message from inside their label's span. Screen readers should recognise the visible text has changed and report the errors because of the aria-live attribute on the form (although I've had mixed results testing this.)

form label > span {
display:none;
}
form.displayErrors :invalid {
border-color: red;
}
form.displayErrors :invalid + label > span{
display: initial;
color:red;
}

The logic of the form is handled by pure JavaScript. The method .checkValidity() does the validation of the form and returns true or false. We update the state accordingly, which triggers the CSS to change. If the form is valid, the FormData constructor turns the user's raw input into structured data that is ready to submit to the server. The fetch() method sends that data to the server via familiar AJAX, and the .then() method waits for a response before doing the first argument for success, or the second for failure.

So have you guessed the secret?

I've presented this code as a ReactJS component but it all works fine without ReactJS too. I know there are some good folks out there working hard on ReactJS form components that can do some complex things, and plenty of developers are still loading up on JavaScript validation libraries, but 90% of the time it is massive overengineering. This code is all you actually need for a working form, ReactJS or otherwise.

But wait, the labels are after the formfields?

Most forms aren't written like this and yes, it looks terrible. You will need some positioning CSS to move the labels above or left of the input fields. The labels had to be coded after the fields because CSS can't target parents or items in front of things. I'd suggest wrapping all fields in a relative-positioned div, add padding to the top, and then absolutely position the label at the top. Or use CSS grid to reorder things. But that's another discussion for designers to ponder.

We hope this guide was helpful. If you have any further questions or need more advice regarding form creation, feel free to leave a comment or get in contact with us and we'll give you a hand. 

Add Your Comment

No one has commented on this page yet.