Adding a React form component to create new records

Updated: 

Learn to build a dynamic React form component for creating new records in your Rails application. Enhance your full-stack development skills with this hands-on tutorial.

This lesson is from The Complete React on Rails Course.

Now that we’ve created all the components for listing events and a Model for storing the data, let’s move on to adding a form component for creating new events.

This feature is a bit more complex than simply listing and displaying events, so it’ll give us a chance to dig deeper into React.

Let’s start by creating a new component called EventForm. We’ll make it a class component so that we can use state to control user input and submission of the form.

app/javascript/packs/EventForm.js:

import React from 'react'

class EventForm extends React.Component {
  render () {
    return (
      <div>
        <h4>Create an Event:</h4>
        <form>
          <input type="text" name="title" placeholder="Title" />
          <input type="text" name="start_datetime" placeholder="Date" />
          <input type="text" name="location" placeholder="Location" />
          <button type="submit">Create Event</button>
        </form>
      </div>
    )
  }
}

export default EventForm


This component will display a simple form with input fields for the event title, start time and location, and a submit button.

We can use it in our main Eventlite component to display the form above the list of events.

Eventlite.js:

import EventForm from './EventForm'

const Eventlite = props => (
  <div>
    <EventForm />
    <EventsList events={props.events} />
  </div>
)


HTML form elements, including the <input> fields in our form, maintain their own state and update it based on user input.

However, we want to use React to manage the whole state of the app, including the state of the form. So we will create what is known as a “Controlled Component” - an input form element whose value is controlled by React.

First, we must initialise the component state in the class constructor of the component:

app/javascript/packs/EventForm.js:

class EventForm extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      title: '',
      start_datetime: '',
      location: ''
    }
  }
  ...
}


We’ve initialised the state with three blank values one for each of the input fields.

Now we can tie the state of the form to the component by setting the input field values to use the values from the component state:

app/javascript/packs/EventForm.js:

  render () {
    return (
      <div>
        <h4>Create an Event:</h4>
        <form>
          <input type="text" name="title" placeholder="Title" value={this.state.title} />
          <input type="text" name="start_datetime" placeholder="Date" value={this.state.start_datetime} />
          <input type="text" name="location" placeholder="Location" value={this.state.location} />
          <button type="submit">Create Event</button>
        </form>
      </div>
    )
  }


If you look in the browser developer console, you will now see a ‘Failed prop type’ warning:

Warning: Failed prop type: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.
Since we haven’t specified how React should handle user input, it makes the fields read-only. You can test this by trying to type in the input fields. You won’t be able to!

As per the warning, we need to add an onChange handler. Let’s create a function called handleInput which will update state based on user input.

app/javascript/packs/EventForm.js:

class EventForm extends React.Component {
  ...

  handleInput(event) {
    const name = event.target.name;
    const newState = {};
    newState[name] = event.target.value;
    this.setState(newState)
    event.preventDefault();
  }


handleInput takes the change event (note that this is the browser events) as an argument. The target is the field from which the change event is fired.

Now we need to set the value of the onChange attribute of our input fields to use handleInput.

app/javascript/packs/EventForm.js:

  render () {
    return (
      <div>
        <h4>Create an Event:</h4>
        <form>
          <input type="text" name="title" placeholder="Title" value={this.state.title} onChange={this.handleInput} />
          <input type="text" name="start_datetime" placeholder="Date" value={this.state.start_datetime} onChange={this.handleInput} />
          <input type="text" name="location" placeholder="Location" value={this.state.location} onChange={this.handleInput} />
          <button type="submit">Create Event</button>
        </form>
      </div>
    )
  }


One last thing we need to do before this works is bind handleInput to the correct scope in the constructor of the component. We need to do this because otherwise when the handler function is invoked, the this value is undefined.

app/javascript/packs/EventForm.js:

class EventForm extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      title: '',
      start_datetime: '',
      location: ''
    }
    this.handleInput = this.handleInput.bind(this)
  }
  ...
}


Now, the form inputs work again but the state is handled by React.

handleInput uses the field’s name and value to update the state. For example, if the user enters “London Retail Expo” into the Title field, the call to this.setState will look like this:

this.setState({title: "London Retail Expo"})
You can check this by printing out the value of newState in the console with console.log.

One way around having to explicitly bind functions is to define them as arrow functions. So we can remove the call to bind in the constructor and redefine handleInput like this:

app/javascript/packs/EventForm.js:

  handleInput = (event) => {
    const name = event.target.name
    const newState = {}
    newState[name] = event.target.value
    this.setState(newState)
    event.preventDefault()
  }


Arrow functions use lexical binding, so this function will automatically bind to the component instance, instead of defaulting to an undefined context.

So now, our full component code looks like this:

app/javascript/packs/EventForm.js:

class EventForm extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      title: '',
      start_datetime: '',
      location: ''
    }
  }

  handleInput = (event) => {
    const name = event.target.name
    const newState = {}
    newState[name] = event.target.value
    this.setState(newState)
    event.preventDefault()
  }

  render () {
    return (
      <div>
        <h4>Create an Event:</h4>
        <form>
          <input type="text" name="title" placeholder="Title" value={this.state.title} onChange={this.handleInput} />
          <input type="text" name="start_datetime" placeholder="Date" value={this.state.start_datetime} onChange={this.handleInput} />
          <input type="text" name="location" placeholder="Location" value={this.state.location} onChange={this.handleInput} />
          <button type="submit">Create Event</button>
        </form>
      </div>
    )
  }
}

export default EventForm