Two-Way Data-Binding and Form Validation in React

Jennie Kim Eldon
6 min readDec 24, 2015

--

How do you solve a problem like user interactions mutating your DOM? Well, there’s always jQuery…

But since I don’t feel like eating sadness, let’s just use React instead. Here’s what we’re building:

Fork on Github

I found the React documentation on uncontrolled form components to be a little sparse, so I’m sharing my example in the hope that others find it useful.

Build Setup

We’re using Webpack to build all of our JavaScript into bundle.js. I based the config off of Dan Abramov’s react-hot-boilerplate (a good way to get started).

Here’s the index.html:

Our entry point is index.js, and our root component is app.js. So far, all of this should feel pretty standard.

The App component is the root for all our app components. So far, we only have one, the User component, which holds all our form logic. This is where it gets interesting.

Basic Layout and ReactLink

To be perfectly honest, reconciling forms, which are mutable by definition, with React’s one-way data flow always felt wrong to me. It’s like wearing socks with sandals — it’s not illegal or anything, but it seriously violates the spirit of the device.

Forms, however, are decidedly A Thing, and cannot be avoided. React acknowledges this with a helper object called ReactLink, which is available through React’s LinkedStateMixin.

Of course, you don’t have to use the mixin; the code is fairly straightforward, and you can choose to compose the helper if you prefer. I chose to use the mixin because it didn’t seem too magical, it’s supported in React’s official docs, and only God can judge me.

The basic layout is below. We have 4 input fields, each with a valueLink that connects it with the app state. Note that the valueLink accepts a string that maps to a key in your app state object; nested values are not supported (at least, I couldn’t figure out how to get around this, opting instead to flatten my state object).

If you’re like me, you want to get the most basic thing working, and then make it not look like butt as quickly as possible. To that end, here are some basic styles:

You may have noticed that we have two buttons here that do nothing. Let’s fix that.

Adding Save and Cancel Click Handlers

We want our buttons to do two things:

SAVE: Pass user input to an action handler or service, depending on whatever flux implementation you’re using. In this sample code, I just console.log the “saved” object.

CANCEL: Clear user input and replace it with the most-recently saved data from the store. In this sample, I use hard-coded mock data to represent the store object.

To support this desired functionality, let’s add three methods to our component: handleSaveClick, handleCancelClick, and a private method (FYI: whenever I use the word “private” to refer to a JavaScript object, imagine giant air quotes around it) called _updateUser that handleSaveClick will call.

With the handlers in place, we now need to add them to the button elements in our render function. (Note: In an app with more than one form view, you’d probably want to split off the buttons into a separate component; that way, you can reuse them across views.)

Tada! If your users are perfectly controlled robots that would never provide an invalid entry, you can stop right here and enjoy the rest of your day on what sounds like an incredible planet for front end developers!

But, for those of us on Planet Earth, it’s time to move on to form validation.

Form Validation, aka ‘Users Do the Darnedest Things!’

What goals do we want to achieve here?

  1. Users should not be able to submit invalid inputs (the logic of what constitutes validity we’ll deal with later)
  2. Users should receive visual feedback after attempting to submit an invalid entry

OK. Easy stuff first. Let’s add valid and invalid classes to our CSS to support goal #2:

.valid {
border: solid 1px #dbe2e8;
}
.invalid {
border: solid 1px red !important;
}

Now, for the trickier part: setting up the actual valid and invalid states. Our app’s local state now needs to hold on to some data that says whether or not an entry is valid. Ideally, this state would also determine how the containing element is rendered (applying either the valid or invalid style to it).

We can add this new bit of data to our app state in our getInitialState hook:

Operating under the assumption that the store will not pass down invalid data on initial load, we’ve set the state to true for all inputs. This may not be good logic. For example, empty strings are listed as defaults here, but what if our app doesn’t accept empty values? In fact, this is exactly the case with our app. But we’re going to tackle that logic next, so let’s whistle past that graveyard for a few more minutes.

Let’s think about how we want styles to be applied. We want each element’s style to be determined by its state of validity (“State of Validity” is an awesome name for a Tom Clancy thriller, btw). We can create a simple private method for this called _getInputStyleName:

Now, in our render, we can call _getInputStyleName to set the className on each component.

To test this, if you change the isValid values in getInitialState to false, a red border should appear around the affected inputs. Once that’s working, we can progress to…

VALIDATORS!!!

For no particular reason, I like to say validators the way they call out “REGULATORSSS” in the classic Warren G hit, “Regulate.”

And, much like Warren G and Nate Dogg, validators are here to enforce certain standards and protect your enterprise.

There are many options to choose from when deciding how to do validation. You can go for a library tailored for React like formsy-react, a more general option like validator, or even write your own validation service, replete with Dalek ASCII art.

I went with validator. Don’t forget to npm install and import it.

import validator from ‘validator’;

Next, we create the validation methods, and add a call to _validate in handleSaveClick:

Finally, it’s not enough to validate on save — we also need to add a check in the _updateUser method to ensure that invalid entries don’t get through. To avoid the tedium of calling a validation method on each input, we’ll create a catchall method called _areValid.

And, for good measure, we’d better sanitize the values before passing along the updated user object. After all, we aren’t animals.

Now we have a form that applies validation logic on save! This addresses the concern around empty strings in getInitialState mentioned earlier. In the rare case that an empty string is passed down on load (if we’re dealing with a legacy database, for example), the user will only get an error on save. Whether or not this is adequate depends on your use case.

Here’s our form now! Isn’t it perfect?!!

Son of a b… eekeeper.

Handling State on Cancel

For the record, the majority of my time programming is spent shuttling between states of happy dances and cursing. After initial victory, I felt the familiar sensation of defeat, aka Death By a Thousand Edge Cases.

I totally forgot to handle the isValid state in handleCancelClick! My bad. I remedied this in the final, complete user.js component, below (see line 108).

Now go watch the Warren G “Regulate” music video and then build some forms!

Link to source code

--

--

Jennie Kim Eldon

Senior Product Manager @udacity, learning enthusiast, programmer, cool mom