How to Submit Forms and Save Data with React.js and Node.js

In my last two tutorials, I covered the basics of React.js and form error handling in React.js. In each case, I used hard-coded data. React is a front-end library, providing no easy way to save to, or read information from, a database or file. That is the part we will cover in this tutorial – but if you need some refreshing on React, especially on Props & States, feel free to read the linked resources.

React is a user interface library, so it is common to use a different library or framework to do all the back-end work. One of the most common pairings is React.js with Node.js. Lucky for us, we already have a tutorial on the basics of Node right here on HTML5 Hive!

The main React Facebook tutorial¬†does something similar to what we will be doing with this tutorial, and they also provide a Node server, as well as many other server examples within their download files. Although our Node server will be similar, our front-end React interface will have a few extra complications. In fact, I’ll be using the same files from my form error handling React tutorial, with a few updates that will allow the saving and reading of information from a file or database.

BUILD GAMES

FINAL DAYS: Unlock 250+ coding courses, guided learning paths, help from expert mentors, and more.

Download the tutorial files

You can download all the files used in this tutorial from here.

Learn React online

If you are keen to learn React from the ground-up feel free to check Learn and Understand React JS on Zenva Academy which covers all the basics + lots of bonus topics like React Router and Flux.

Tutorial requirements

  • You must have basic knowledge of React.js and JavaScript. For a beginner’s guide to React, please see my past tutorial. In addition, I will be using many of the same functions from my other React tutorial, so you may want to read that one as well.
  • You will need to use the React library, although we will be using CDN links to get around that during testing.
  • You will need to download a text editor of some sort. You can use just a plain text editor, although¬†Notepad++ is popular on Windows, and TextMate is popular on Mac machines. An editor with code¬†highlighting capability is¬†preferable.
  • You will need to read and follow the directions in most of the beginner’s Node.js tutorial in order to create the server that will be used later in this tutorial.

Making revisions to a React user interface

I will be using the same files I created for the error handling tutorial, with some simple revisions that will make it possible to read and save data. The files with all the revisions are provided above. The index file from the previous tutorial remains the same, but now we are going to be pulling data from a file, then posting new form entries to the file.

The original file was set up with some of the code needed to post and read data, but some parts were commented out, since we had no server to do the work. All we have to do is uncomment those lines, and we have this:

var DonationBox = React.createClass({
  getInitialState: function() {
    //this will hold all the data being read and posted to the file
    return {data: []};
  },
  loadDonationsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  componentDidMount: function() {
    this.loadDonationsFromServer();
    setInterval(this.loadDonationsFromServer, this.props.pollInterval);
  },
  handleDonationSubmit: function(donation) {
    //this is just an example of how you would submit a form
    //you would have to implement something separately on the server
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'POST',
      data: donation,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  render: function() {
    return (
      <div className="donationBox">
        <h1>Donations</h1>
        <DonationList data={this.state.data} />
        <DonationForm onDonationSubmit={this.handleDonationSubmit} />
      </div>
    );
  }
});

ReactDOM.render(
  <DonationBox url="/api/donations" pollInterval={2000} />,
  document.getElementById('content')
);

So far, everything we have here is similar to Facebook’s Commenting tutorial. We save all the data from the file in a DonationBox component state, then set an interval to pull new donations from the server so the user can see new donations as they come in, in close to real time. On each form submission, the data is pushed to the database (or in this case, the file). We include JQuery in the index file in order to make the loading and submitting of data easier.

Let’s continue with the changes I’ve made to the form before discussing the server side.

Displaying new data from everyone

We now have a new listing that will display all new donations coming from all users.

var DonationList = React.createClass({
  render: function() {
    var donationNodes = this.props.data.map(function(donation) {
      return (
        <Donation
          contributor={donation.contributor}
          key={donation.id}
          amount={donation.amount}
        >
          {donation.comment}
        </Donation>
      );
    });
    return (
      <div className="donationList">
        {donationNodes}
      </div>
    );
  }
});

Similar to the Facebook tutorial, we are mapping out all the data from the JSON file into a new Donation component for each entry. We then display the entire donationNodes list. Let’s take a look at the new Donation component:
var Donation = React.createClass({
  render: function() {
    return (
      <div className="donation">
        <h2 className="donationContributor">
          {this.props.contributor}: ${this.props.amount}
        </h2>
          {this.props.children.toString()}
      </div>
    );
  }
});

This is actually simpler than what we see in the Facebook tutorial, because we are expecting comments by contributors to be in plain text. We are simply displaying the main donation information, leaving out anything private. In fact, we are only saving the public information for the purposes of this tutorial, but it’s easy enough to store all of the data¬†in a database, then only display the information that is public.

So far, our additions are very similar to what you can find in the Facebook tutorial, but our original Donation app is nothing like that tutorial. This is where the changes become a little more complicated.

Submitting form data

In the original donation app, we had some fairly complicated calls to other components that allowed us to serve up multiple fields from a single component. Now that we have to think about actually saving and displaying that data, we have to add a value attribute to the components that will allow us to empty all the fields after submission, and we also have to save all the data in a state.

var DonationForm = React.createClass({
  getInitialState: function() {
    return {
      contributor: "",
      amount: undefined,
      comment: "",
      email: "",
      department: undefined
    };
  },
  handleSubmit: function(e) {
    //we don't want the form to submit, so we prevent the default behavior
    e.preventDefault();
    
    var contributor = this.state.contributor.trim();
    var amount = this.state.amount;
    var comment = this.state.comment.trim();
    if (!contributor || !amount) {
      return;
    }
    
    //Here we do the final submit to the parent component
    this.props.onDonationSubmit({contributor: contributor, amount: amount, comment: comment});
    this.setState({
      contributor: '',
      amount: undefined,
      comment: '',
      email: '',
      department: undefined
    });
  },
  validateEmail: function (value) {
    // regex from http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
    var re = /^(([^<>()[\]\\.,;:\[email protected]\"]+(\.[^<>()[\]\\.,;:\[email protected]\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
    return re.test(value);
  },
  validateDollars: function (value) {
    //will accept dollar amounts with two digits after the decimal or no decimal
    //will also accept a number with or without a dollar sign
    var regex  = /^\$?[0-9]+(\.[0-9][0-9])?$/;
    return regex.test(value);
  },
  commonValidate: function () {
    //you could do something here that does general validation for any form field
    return true;
  },
  setValue: function (field, event) {
    //If the input fields were directly within this
    //this component, we could use this.refs.[FIELD].value
    //Instead, we want to save the data for when the form is submitted
    var object = {};
    object[field] = event.target.value;
    this.setState(object);
  },
  render: function() {
    //Each form field is actually another component.
    //Two of the form fields use the same component, but with different variables
    return (
      <form className="donationForm" onSubmit={this.handleSubmit}>
        <h2>University Donation</h2>
      
        <TextInput
          value={this.state.email}
          uniqueName="email"
          text="Email Address"
          textArea={false}
          required={true}
          minCharacters={6}
          validate={this.validateEmail}
          onChange={this.setValue.bind(this, 'email')} 
          errorMessage="Email is invalid"
          emptyMessage="Email is required" />
        <br /><br />

        <TextInput
          value={this.state.contributor}
          uniqueName="contributor"
          text="Your Name"
          textArea={false}
          required={true}
          minCharacters={3}
          validate={this.commonValidate}
          onChange={this.setValue.bind(this, 'contributor')} 
          errorMessage="Name is invalid"
          emptyMessage="Name is required" />
        <br /><br />
          
        <TextInput
          value={this.state.comment}
          uniqueName="comment"
          text="Is there anything you'd like to say?"
          textArea={true}
          required={false}
          validate={this.commonValidate}
          onChange={this.setValue.bind(this, 'comment')} 
          errorMessage=""
          emptyMessage="" />
        <br /><br />
      
        {/* This Department component is specialized to include two fields in one */}
        <h4>Where would you like your donation to go?</h4>
        <Department
          value={this.state.department} 
          onChange={this.setValue.bind(this, 'department')} />
        <br /><br />
      
        {/* This Radios component is specialized to include two fields in one */}
        <h4>How much would you like to give?</h4>
        <Radios
          value={this.state.amount}
          values={[10, 25, 50]}
          name="amount"
          addAny={true}
          anyLabel=" Donate a custom amount"
          anyPlaceholder="Amount (0.00)"
          anyValidation={this.validateDollars}
          onChange={this.setValue.bind(this, 'amount')} 
          anyErrorMessage="Amount is not a valid dollar amount"
          itemLabel={' Donate $[VALUE]'} />
        <br /><br />
      
        <h4>Payment Information</h4>
        <Payment />
        <br />
      
        <input type="submit" value="Submit" />
      </form>
    );
  }
});

As you can see, we’ve added new states for each piece of data we plan on saving. The other bits of data can always be added later, but we are just going to focus on the data that we want to display. On submit, we send all the data to the parent component to do the actual saving to the server. We then reset all the fields so they will appear empty after submission.

We also have a new method called setValue. Because we do not have form fields directly within this component, we have to have a way to set the state as we go (using the onChange attribute in each component call). Rather than create a new function for every form field, we use this single function by taking the field as a variable, and using it to create a new object that can be used to set the state for that field. The field name is included in the call to the component.

Each component call also has a new value attribute. This is what allows us to empty all the fields when the form is submitted. I’ve¬†have also added a new text field, with an option for producing a textarea rather than a single line text field. This allows us to add a new comment field for contributors who want to say something about why they’re donating. Now let’s take a look at the few changes made to the form element components.

Emptying fields on form submission

There are no changes to the InputError component, but we do have a few small changes to the other components which will allow us to empty all the fields after submission.

var TextInput = React.createClass({
  getInitialState: function(){
    //most of these variables have to do with handling errors
    return {
      isEmpty: true,
      value: null,
      valid: false,
      errorMessage: "Input is invalid",
      errorVisible: false
    };
  },

  handleChange: function(event){
    //validate the field locally
    this.validation(event.target.value);

    //Call onChange method on the parent component for updating it's state
    //If saving this field for final form submission, it gets passed
    // up to the top component for sending to the server
    if(this.props.onChange) {
      this.props.onChange(event);
    }
  },

  validation: function (value, valid) {
    //The valid variable is optional, and true if not passed in:
    if (typeof valid === 'undefined') {
      valid = true;
    }
    
    var message = "";
    var errorVisible = false;
    
    //we know how to validate text fields based on information passed through props
    if (!valid) {
      //This happens when the user leaves the field, but it is not valid
      //(we do final validation in the parent component, then pass the result
      //here for display)
      message = this.props.errorMessage;
      valid = false;
      errorVisible = true;
    }
    else if (this.props.required && jQuery.isEmptyObject(value)) {
      //this happens when we have a required field with no text entered
      //in this case, we want the "emptyMessage" error message
      message = this.props.emptyMessage;
      valid = false;
      errorVisible = true;
    }
    else if (value.length < this.props.minCharacters) {
      //This happens when the text entered is not the required length,
      //in which case we show the regular error message
      message = this.props.errorMessage;
      valid = false;
      errorVisible = true;
    }
    
    //setting the state will update the display,
    //causing the error message to display if there is one.
    this.setState({
      value: value,
      isEmpty: jQuery.isEmptyObject(value),
      valid: valid,
      errorMessage: message,
      errorVisible: errorVisible
    });

  },

  handleBlur: function (event) {
    //Complete final validation from parent element when complete
    var valid = this.props.validate(event.target.value);
    //pass the result to the local validation element for displaying the error
    this.validation(event.target.value, valid);
  },
  render: function() {
    if (this.props.textArea) {
      return (
        <div className={this.props.uniqueName}>
          <textarea
            placeholder={this.props.text}
            className={'input input-' + this.props.uniqueName}
            onChange={this.handleChange}
            onBlur={this.handleBlur}
            value={this.props.value} />
      
          <InputError 
            visible={this.state.errorVisible} 
            errorMessage={this.state.errorMessage} />
        </div>
      );
    } else {
      return (
        <div className={this.props.uniqueName}>
          <input
            placeholder={this.props.text}
            className={'input input-' + this.props.uniqueName}
            onChange={this.handleChange}
            onBlur={this.handleBlur}
            value={this.props.value} />
      
          <InputError 
            visible={this.state.errorVisible} 
            errorMessage={this.state.errorMessage} />
        </div>
      );
    }
  }
});

The TextInput component has the simplest change because all we have to do is set the value to the props value sent to the component. We were already calling this.props.onChange from the handleChange function for real-time error processing, and simply rearranged the error processing a bit in the parent component so we can save the data. We still call the parent validation method when the user leaves the field.

We do add one additional input field to handle the textarea option. When the text field is a textarea, we have to use the textarea field. Otherwise, it’s a normal text field. There are no other changes.

var Radios = React.createClass({
  getInitialState: function() {
    //displayClass is the class we use for displaying or hiding
    //the optional "any value" text field
    return {
      displayClass: 'invisible',
      valid: false,
      errorMessage: "Input is invalid",
      errorVisible: false
    };
  },
  handleClick: function(displayClass, e) {
    //if we click any option other than the "any value" option,
    //we hide the "any value" text field. Otherwise, show it
    if (displayClass == 'invisible') {
      this.setState(
        {
          displayClass: displayClass,
          errorVisible: false
        }
      );
      this.props.onChange(e);
    }
    else {
      this.setState({displayClass: displayClass});
    }
  },
  handleAnyChange: function(e) {
    //this validation is specifically for the optional "any value" text field
    //Since we have no idea what the requirements are locally, we call the parent
    //validation function, then set the error states accordingly
    if (this.props.anyValidation(e.target.value)) {
      this.setState(
        {
          valid: true,
          errorMessage: "Input is invalid",
          errorVisible: false
        }
      );
      this.props.onChange(e);
    }
    else {
      this.setState(
        {
          valid: false,
          errorMessage: this.props.anyErrorMessage,
          errorVisible: true
        }
      );
    }
  },
  render: function() {
    var rows = [];
    var label = "";
    
    //we have passed in all the options for the radios, so we traverse the array
    for (var i = 0; i < this.props.values.length; i++) {
      //We do this little replace for when we want to display the value as part of
      //additional text. Otherwise, we would just put '[VALUE]' when passing
      //the itemLabel prop from the parent component, or leave out '[VALUE]' entirely
      label = this.props.itemLabel.replace('[VALUE]', this.props.values[i]);
      
      //You'll see that even the <br /> field has a key. React will give you errors
      //if you don't do this. This is just an axample of what's possible, but
      //you would normally add extra spacing with css
      rows.push(<input
        key={this.props.name + '-' + i}
        type="radio"
        ref={this.props.name + '-' + this.props.values[i]}
        name={this.props.name}
        value={this.props.values[i]}
        selected={this.props.value==this.props.values[i]?true:false}
        onClick={this.handleClick.bind(this, 'invisible')} />,
        
        <label key={this.props.name + '-label-' + i} htmlFor={this.props.values[i]}>{label}</label>,
      
        <br key={this.props.name + '-br-' + i} />);
    }
    
    //The "any value" field complicates things a bit
    if (this.props.addAny) {
      //we passed in a separate label just for the option that
      //activates the "any value" text field
      var selected = false;
      label = this.props.anyLabel;
      if (this.props.value != undefined && this.props.values.indexOf(this.props.value) == -1) {
        selected = true;
      }
      rows.push(<input
        key={this.props.name + '-' + i}
        type="radio"
        ref={this.props.name + '-any'}
        name={this.props.name} value="any"
        selected={selected}
        onClick={this.handleClick.bind(this, 'visible')} />,
          
        <label key={this.props.name + '-label-' + i} htmlFor={this.props.values[i]}>{label}</label>);
      
      //and now we add the "any value text field, with all its special variables"
      var value = "";
      if (selected) {
        value = this.props.value;
      }
      
      rows.push(<div key={this.props.name + '-div-' + (i+2)} className={this.state.displayClass}>
        <input
          className="anyValue"
          key={this.props.name + '-' + (i+1)}
          type="text"
          value={value}
          placeholder={this.props.anyPlaceholder}
          onChange={this.handleAnyChange}
          ref={this.props.name} />
      </div>);
    }
    
    //Now we just return all those rows, along with the error component
    return (
      <div className="radios">
        {rows}
        
        <InputError 
          visible={this.state.errorVisible} 
          errorMessage={this.state.errorMessage} />
      </div>
    );
  }
});

The only changes made to the Radios component have to do with the addition of the value attribute. You may have noticed that the default and reset values sent to the Radios and Department components are “undefined.” That’s because there’s always a possibility that one of the radio or select options could be 0 or an empty string, but both components also contain an “any value” text field. The only way to truly unset these radio and¬†select fields, therefore, is to set the value¬†to undefined.

Properly setting the regular radio buttons is easy. We just select the radio button using the selected attribute if it matches the value, as we iterate through each radio button:

selected={this.props.value==this.props.values[i]?true:false}

Just because we are using the value property to empty the fields doesn’t mean that we don’t have to set the current value. Remember that we are saving the state as the user makes changes, but that state change causes the display to update, which means that we have to set the value correctly even when the field is not empty. Otherwise, the fields would be set to empty as soon as a user makes a change!

We then put the “undefined” setting to use when deciding whether to select the “other” option:

var selected = false;
if (this.props.value != undefined && this.props.values.indexOf(this.props.value) == -1) {
  selected = true;
}

If the value is undefined, then we know not to select anything, because we are emptying all the fields. If it’s not undefined, however, and the value does not match any of the other radio buttons, then we know we have a custom value.

For the optional text field, we just need to set the value if it exists:

var value = "";
if (selected) {
  value = this.props.value;
}

There are no changes to the Payment component, but we do have a small change to the Department component (even though we aren’t actually saving the value):
var Department = React.createClass({
  getInitialState: function() {
    return {
      displayClass: 'invisible'
    };
  },
  handleClick: function(e) {
    //We're doing another one of these "any value" fields, only shown when
    //a specific "other" option is chosen
    this.props.onChange(e);
    var displayClass = 'invisible';
    if (e.target.value == 'other') {
      displayClass = 'visible';
    }
    this.setState({displayClass: displayClass});
  },
  render: function() {
    //This is a select field with options and sub-options, plus an "any value" field
    var value = this.props.value;
    if (this.props.value != undefined && ['none', 'muir', 'revelle', 'sixth', 'jacobs', 'global', 'medicine', 'scholarships'].indexOf(this.props.value) == -1) {
      value = 'other';
    }
    else if (this.props.value == undefined) {
      value = 'none';
    }
    
    return (
      <div className="department">
        <select value={value} onChange={this.handleClick} multiple={false} ref="department">
          <option value="none"></option>
          <optgroup label="College">
            <option value="muir">Muir</option>
            <option value="revelle">Revelle</option>
            <option value="sixth">Sixth</option>
          </optgroup>
          <optgroup label="School">
            <option value="jacobs">Jacobs School of Engineering</option>
            <option value="global">School of Global Policy and Strategy</option>
            <option value="medicine">School of Medicine</option>
          </optgroup>
          <option value="scholarships">Scholarships</option>
          <option value="other">Other</option>
        </select>
        <div className={this.state.displayClass}>
          <input className="anyValue" value={this.props.value=='other'?this.props.value:''} type="text" onChange={this.props.onChange} placeholder="Department" ref="any-department" />
        </div>
      
        <InputError 
          visible={this.state.errorVisible} 
          errorMessage={this.state.errorMessage} />
      </div>
    );
  }
});

Since this is a select field with an “any value” text field, we have to check for “undefined” again:
    var value = this.props.value;
    if (this.props.value != undefined && ['none', 'muir', 'revelle', 'sixth', 'jacobs', 'global', 'medicine', 'scholarships'].indexOf(this.props.value) == -1) {
      value = 'other';
    }
    else if (this.props.value == undefined) {
      value = 'none';
    }

The Department component is a little bit different from the others, in that the options are simply hard-coded. The only reason it’s a separate component is to simplify the code in the DonationForm component. As such, the easiest way to set the correct value is to simply check against the hard-coded values. This is similar to what we did in the Radios component, but with hard-coded values.¬†We then set the text field¬†based on whether they’ve selected “other.”

Saving data to the server

Now we get to save the data and watch the changes happen! Our example hinges on the installation of Node.js. Most of what needs to be done is covered in the beginner’s Node.js tutorial. If you follow all the directions in that tutorial, within the folder for your project, you’ll wind up with a package.json file. The only addition to that tutorial is to install body-parser along with express. After installing express, you will need to run the same command, but with body-parser:

npm install body-parser --save

This will give you a package.json file similar to the one included with the files for this tutorial:
{
  "name": "donation-tutorial",
  "version": "0.0.1",
  "description": "Donation tutorial",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "keywords": [
    "react",
    "tutorial",
    "donation",
    "example"
  ],
  "author": "zenva",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.14.1",
    "express": "^4.13.3"
  }
}

We use server.js for our example, since we are creating a server, although the file in the Node.js tutorial is called script.js. You can use either name as long as the name in your package file matches the name of your Node file.

From there, you would create your Node server in whatever way you see fit. The server can do additional processing and error checking, and should include security measures as well. It can also save information to a database, and read from a database. In our example file, we simply save to, and read from, a file called donations.json. In fact, our server is so simple that it’s very similar to¬†Facebook’s server.js file, included within their tutorial files. You can’t really make it more simple than that!

If using the files with this tutorial, all you have to do is install Node.js (which includes npm), then go to the same folder as the package.json file and run the following command on the command line:

npm install

Once it’s installed, just run the following command:
node server.js

But look at the following line in package.json:
"start": "node server.js"

This means that you can also start the server by running the following command:
npm start

The server will then start, and tell you right where you need to go in your browser: http://localhost:3000/

Once you open that URL, you should see a list of donations, with the ability to add new donations. In addition, if you add a field directly to the donations.json file, it will display on the screen almost immediately!

Screen Shot 2015-11-17 at 8.03.00 PM

Comments and Questions?

Please let me know if you have any questions or suggestions in the comments section below. I welcome your input!