Basic React form validation in a Rails app

In this lesson we’re going to look at handling Rails form validations with React.

We’ll add a simple validation to check that a value for the title of an Event is present.

At the moment, in our app, the user can submit the form without entering anything in the title field.

The event gets added anyway, with just an event date and location.

We don’t want that. So we’ll add a Rails validation and handle the error when that validation fails.

Let’s start off by adding a validation on the title field of the Event model.

validates_presence_of :title
Now, if we try creating an event without a title, the event won’t get added.

In the catch block of the handleSubmit function, let’s log error.response.data to see the error message in the browser developer console.

  handleSubmit = e => {
    e.preventDefault()
    let newEvent = { title: this.state.title, start_datetime: this.state.start_datetime, location: this.state.location }
    axios({
      method: 'POST',
      url: '/events',
      data: { event: newEvent },
      headers: {
        'X-CSRF-Token': document.querySelector("meta[name=csrf-token]").content
      }
    })
    .then(response => {
      this.addNewEvent(response.data)
    })
    .catch(error => {
      console.log(error.response.data)
    })
  }
And if we look in the browser developer console, we’ll see an error.

That’s coming from the events controller.

  def create
    @event = Event.new(event_params)
    if @event.save
      render json: @event
    else
      render json: @event.errors, status: :unprocessable_entity
    end
  end
It tries to save the event, and when it fails, it renders @event.errors and sets the status to Unprocessable Entity which is the HTTP status code 422.

We can also see the validation error message:

{"title":["can't be blank"]}
But, we don’t show any error message on the app page, so the user won’t know what went wrong.

Now, instead of just logging the error to the console, let’s display it on the page above the form.

We’ll do that by adding the errors returned by the controller to the state of our app and displaying that state value in our Eventlite component.

So let’s start by adding a state property called formErrors and set its initial value to an empty object.

  constructor(props) {
    super(props)
    this.state = {
      events: this.props.events,
      title: '',
      start_datetime: '',
      location: '',
      formErrors: {}
    }
  }
And now let’s use it in the catch block of our axios call.

    ...
    .catch(error => {
      console.log(error.response.data)
      this.setState({formErrors: error.response.data})
    })
And now let’s use this value in our render function to display an error message.

  render() {
    return (
      <div>
        <p>Title {this.state.formErrors.title}</p>
        <EventForm handleSubmit = {this.handleSubmit}
          handleInput = {this.handleInput}
          title = {this.state.title}
          start_datetime = {this.state.start_datetime}
          location = {this.state.location} />
        <EventsList events={this.state.events} />
      </div>
    )
  }
Now, if we refresh the page and submit the form again with the title missing, we’ll see the error message “Title can’t be blank” displayed above the form.

Let’s create a new reusable component called FormErrors and move the display code into that. We’ll need to pass the formErrors value from state as a prop to it.

  render () {
    return (
      <div>
        <FormErrors formErrors = {this.state.formErrors} />
        <EventForm handleSubmit = {this.handleSubmit}
          handleInput = {this.handleInput}
          title = {this.state.title}
          start_datetime = {this.state.start_datetime}
          location = {this.state.location} />
        <EventsList events={this.state.events} />
      </div>
    )
  }
Now let’s create this component.

We’ll create a new file in our packs directory and save it as FormErrors.js.

import React from 'react'

const FormErrors = (props) =>
  <div>
    <p>Title {props.formErrors.title}</p>
  </div>

export default FormErrors
Now we need to import this component into the Eventlite component.

import { FormErrors } from './FormErrors'
Ok let’s try this in the browser now by refreshing the page.

We’ll submit the form with an empty title field.

Now our title field error message appears above the form.

Of course, we don’t want to hard code the name of the field in our component. We want to be able to handle all the errors on any fields in our form.

So let’s generalise the code.

import React from 'react'

const FormErrors = (props) =>
  <div>
    {Object.keys(props.formErrors).map((formErrorField) => {
      return (
        props.formErrors[formErrorField].map((error) => {
          return (
            <p>{formErrorField} {error}</p>
          )
        })
      )
    })}
  </div>

export default FormErrors
We first get all the error field names. They are stored as keys in the formErrors object.

Then we loop through them by calling the map function.

Now, each key in the object has an array of error messages as its value i.e. each field that fails a validation is a key in the object.

And each of those keys has an array of error messages associated with each validation that it failed.

So we access the array of messages inside the formErrors object with the key formErrorField and then we’ll loop through each message and return that to be displayed.

When we submit the form again, this time we get the error message “title can’t be blank” which is dynamically generated without use having to hard code the “title” key.

We can also check if the new component works well when there are multiple validation failures for more than one field. Let’s add a presence validation for the start_datetime and location fields in the Event model:

  validates_presence_of :title, :start_datetime, :location
Now if we submit the form with all the input fields empty, we will see the presence validation error message for all three fields.

So our error handling is working for multiple fields now.

But there’s one thing we still need to take care of.

Now, if we fill in all the form fields and submit the form again, we’ll be able to create an event.

However, we’ll still see the error message above the form.

That’s because we didn’t update the formErrors value in our app state after making a successful form submission.

So let’s do that now.

In our axios call, inside the success callback, let’s call a new function called resetFormErrors.

    .then((response) => {
      this.addNewEvent(response.data)
      this.resetFormErrors()
    })
and let’s define that as

  resetFormErrors () {
    this.setState({formErrors: {}})
  }
Now when we make a successful form submission, the error message disappears because the state got reset to contain no errors.

Now let’s add some more validation rules.

Let’s say we want the length of the title to be at least 3 characters.

So we’ll add another validation:

validates :title, length: {minimum: 3}
And now if we try to create an event with 1 or 2 character title, we’ll get a length validation error.

Finally, let’s make sure that new events are not created for a date or time in the past. We’ll add a custom validation method for that.

validate :start_datetime_cannot_be_in_the_past
And let’s define it as a private method on the Event model:

  private
  def start_datetime_cannot_be_in_the_past
    if start_datetime.present? && start_datetime < Time.now
      errors.add(:start_datetime, "can't be in the past")
    end
  end
Ok, now let’s try to make an event on a past date and see what happens.

We’ll add a valid title and location with a past date. And when we try to create an event, it fails with the error "start_datetime can't be in the past".
Now if we choose a future date and time, we can submit the form successfully and the error disappears.

So that’s some basic form validation using the default Rails model errors.