Lifting state up

We now have our app state stored partly in the Eventlite component and partly in the EventForm component. This is not strictly a problem, but it’s generally good practice to keep state in one component, especially for a simple app like ours.

In our case, it makes most sense to keep all state in the parent Eventlite component.

This practice of “lifting state up” is a common pattern when several components need to reflect the same changing data. The React team recommends lifting the shared state up to their closest common ancestor.

We’ll move state and form input handling code (handleInput and handleSubmit) from EventForm up to the Eventlite component. We’ll then pass these values and functions as props.

And then we can also convert EventForm from a class component to a functional component.

app/javascript/packs/EventForm.js:
import React from 'react'
import Event from './Event'

const EventForm = (props) => (
  <div>
    <h4>Create an Event:</h4>
    <form onSubmit={props.handleSubmit}>
      <input type="text" name="title" placeholder="Title" value={props.title} onChange={props.handleInput} />
      <input type="text" name="start_datetime" placeholder="Date" value={props.start_datetime} onChange={props.handleInput} />
      <input type="text" name="location" placeholder="Location" value={props.location} onChange={props.handleInput} />
      <input type="submit" value="Create Event" />
    </form>
  </div>
)

export default EventForm

As you can see, this greatly simplifies the EventForm component. The field values and functions are now populated from the props (for example, props.title instead of this.state.title and props.handleSubmit instead of this.handleSubmit).

In Eventlite, the handleInput and handleSubmit functions and form state can be copied and pasted from EventForm.

We need one small change in handleSubmit - since the Eventlite state also includes events, we need to exclude that from our AJAX call, so that we're only sending the form field data and not the list of events unnecessarily.

We can do that by picking out the form field values from the state into a variable called newEvent and only submitting that in handleSubmit.

In addition, we need to pass these down to EventForm as props.

app/javascript/packs/Eventlite.js:
import React from 'react'
import ReactDOM from 'react-dom'
import axios from 'axios'

import EventsList from './EventsList'
import EventForm from './EventForm'

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

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

  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)
    })
  }

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

  render() {
    return (
      <div>
        <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>
    )
  }
}