Handling form submissions in React

Now that we have the form in place, let’s complete its functionality by handling form submission.

First, let’s set up the backend by adding a new create action in EventsController. This is just a standard CRUD method which will use the data submitted via the form to create a new event.

app/controllers/events_controller.rb:

  def create
    @event = Event.new(event_params)
    if @event.save
      render json: @event
    else
      render json: @event.errors, status: :unprocessable_entity
    end
  end

  private
  def event_params
    params.require(:event).permit(:title, :start_datetime, :location)
  end

Note that we’re returning the saved event as JSON.

We also need to set up events as resources in the routes config:

config/routes.rb:

  resources :events

Now, let’s return to the frontend. We want to submit the form via AJAX to avoid a page reload.

There are many options for making AJAX requests, including Rails’ own rails-ujs, jQuery, fetch and other libraries.

We are going to use a widely used library called axios.

Let’s install it in our app with yarn:

$ yarn add axios
Just like we did with handling user input, we need to write a handler function called handleSubmit for dealing with form submissions and attach it to the onSubmit event of the form.

app/javascript/packs/EventForm.js:

...
import axios from 'axios'

class EventForm extends React.Component {
  ...
  handleSubmit = e => {
    axios({
      method: 'POST',
      url: '/events',
      data: { event: this.state },
      headers: {
        'X-CSRF-Token': document.querySelector("meta[name=csrf-token]").content
      }
    })
    .then(response => {
      console.log(response)
    })
    .catch(error => {
      console.log(error)
    })
    e.preventDefault()
  }

  render () {
    return (
      <div>
        <h4>Create an Event:</h4>
        <form onSubmit={this.handleSubmit}>
        ...
        </form>
      </div>
    )
  }
}

We define handleSubmit as an arrow function. We use the axios function to POST the form data (stored in state) to the ‘/events’ endpoint.

Note that we also include the Rails CSRF token inside a headers property. Without this, Rails cannot verify the authenticity of the request and will throw a ‘Can’t verify CSRF token authenticity‘ error. We can get the value of the token from the meta tag in the head of the document.

axios is based on JavaScript Promises. The response to the request is handled by chaining calls to .then when the request is successful and .catch when it fails.

Note, we need to set the onSubmit attribute of our form to use this.handleSubmit.

In the code above, we’re just logging the response. Now, let’s think about how we’re going to use the response.

On a successful request, the controller returns the newly created event data as JSON. We want to display this new event in the list of events.

Remember, the list of events is rendered by the EventsList component contained inside the parent Eventlite component (which also contains EventForm).

EventsList receives the list of events as a prop from Eventlite.

We need to add the newly created event to the list of events. At the moment, the Eventlite component itself receives the list of events as a prop.

In order to be able to update the list, we need to store it in state.

We need to convert Eventlite from a functional component to a class component.

We’ll store the list of events in state and also define a function that updates the list when a new event is added. We’ll pass this function as a prop to EventForm so that it can pass the new event data back up to it.

Let’s start by converting Eventlite to a class component:

app/javascript/packs/Eventlite.js:

class Eventlite extends React.Component {
  render() {
    return (
      <div>
        <EventForm />
        <EventsList events={this.props.events} />
      </div>
    )
  }
}

Let’s move the list of events to state. We’ll use the data sent by the controller on initial page load to initialise the state value.

Then we’ll pass the list from state (this.state.events) to EventsList instead of props.

app/javascript/packs/Eventlite.js:

class Eventlite extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      events: this.props.events
    }
  }

  render() {
    return (
      <div>
        <EventForm handleNewEvent={this.addNewEvent} />
        <EventsList events={this.state.events} />
      </div>
    )
  }
}

Note, we’re also passing EventForm a method called addNewEvent for adding the new event to the list via the handleNewEvent prop.

Let’s define addNewEvent:

app/javascript/packs/Eventlite.js:

  addNewEvent = (event) => {
    const events = [event, ...this.state.events]
    this.setState({events: events})
  }

We simply add the event at the top of the list and update the state via call to this.setState.

Finally, we need to call the function with the successful response data from inside the handleSubmit method in EventForm :

app/javascript/packs/EventForm.js:
  handleSubmit = e => {
    axios({
      method: 'POST',
      url: '/events',
      data: { event: this.state },
      headers: {
        'X-CSRF-Token': document.querySelector("meta[name=csrf-token]").content
      }
    })
    .then(response => {
      this.props.handleNewEvent(response.data)
    })
    .catch(error => {
      console.log(error)
    })
    e.preventDefault()
  }

Note that we use an arrow function inside the .then() call, so that we don’t have to worry about binding to the right context for calling handleNewEvent.

We can now successfully create a new event by filling in the form fields and submitting the form. It gets added to the top of the list without refreshing the page.

Remember, in the controller we order events in chronological order. Let’s do the same after adding our new event on the client side instead of adding it to the top of the list.

app/javascript/packs/Eventlite.js:

  addNewEvent = (event) => {
    const events = [event, ...this.state.events].sort(function(a, b){
      return new Date(a.start_datetime) - new Date(b.start_datetime)
    })
    this.setState({events: events})
  }

Now when we create a new event with the form, it gets added to the list in the correct place according to chronological order.