Rafael Menezes

First App - Adding Food To The State

Continuing with our first-app, we now need to log our own food to the list.

So, let’s create form component that knows how to collect all the necessary information about a food entry. Create a new file FoodForm.jsx

import React, { Component } from 'react'

class FoodForm extends Component {
  render() {
    return (
      <form>
        <h3>Add Food</h3>
        <div className="form-group">
          <input className="form-control" name="food" />
        </div>
        <button className="btn btn-primary">Submit</button>
      </form>
    )
  }
}

export default FoodForm

Here we define a component that renders a form with a title, an input field and a button to submit the form. Let’s add this form in the app so we can see it in action. Go to the App component and import the new FoodForm (line 6) and add it to the DOM in the render method (line 18)

...
import FoodForm from './FoodForm'
class App extends React.Component {
  state = {
    consumed: generate(),
  }

  render() {
    return (
      <React.Fragment>
        <Header />
        <div className="container">
          <FoodForm />          {Object.keys(this.state.consumed).map(key => (
            <Food key={key} food={this.state.consumed[key]} />
          ))}
...

Nice, now you can see the form in the browser. If you type something and on the input and click “submit” you will notice that the browser refreshes, this is because of the default behavior of the form tag that redirects the browser to the url in the action attribute but since we don’t have one, it redirects to itself.

To avoid this refresh, let’s prevent the default behavior from happening. In the FoodForm component we have to define a handler for the form submit.

import React, { Component } from 'react'

class FoodForm extends Component {
  handleSubmit = event => {    event.preventDefault()    alert('Submitted!')  }
  render() {
    return (
      <form onSubmit={this.handleSubmit}>        <h3>Add Food</h3>
        <div className="form-group">
          <input className="form-control" name="info" />
        </div>
        <button className="btn btn-primary">Submit</button>
      </form>
    )
  }
}

export default FoodForm

Awesome, now if you click the submit button, it will show the alert. Let’s try now to show in the alert whatever is in the input text.

We have two distinct ways of doing this here. One is using refs and the other is using the component state. Let’s have a look in both of them.

Refs

import React, { Component } from 'react'

class FoodForm extends Component {
  inputRef = React.createRef()
  handleSubmit event => {
    event.preventDefault()
    const val = this.inputRef.current.value    alert(val)
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <h3>Add Food</h3>
        <div className="form-group">
          <input ref={this.inputRef} className="form-control" name="info" />        </div>
        <button className="btn btn-primary">Submit</button>
      </form>
    )
  }
}

Easy. We defined a ref called inputRef (line 4), then we tied this ref to the input tag (line 17) and then in the handler we access the input tag and read the value.

This is the equivalent on jQuery to $('input').value(), it is reading the value straight from the tag element.

Even though this works, and wouldn’t cause any issues in our case, it’s not recommended. The official documentation gives some tips on when to use refs:

Avoid using refs for anything that can be done declaratively.

And this brings us to the second option…

State

import React, { Component } from 'react'

class FoodForm extends Component {
  state = {    input: '',  }
  handleChange = event => {    const input = event.currentTarget.value    this.setState({ input })  }
  handleSubmit = event => {
    event.preventDefault()
    alert(this.state.input)  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <h3>Add Food</h3>
        <div className="form-group">
          <input
            className="form-control"
            onChange={this.handleChange}            name="info"
          />
        </div>
        <button className="btn btn-primary">Submit</button>
      </form>
    )
  }
}

export default FoodForm

In this example, we define the initial state (line 4-6), then we define a method that will handle change events from the input and save the content to the state (lines 8-11). We tie this handler to the input (line 25) and then, when the form is submitted, instead of consulting the input and reading the value, we read the information from the state.

Even though there is more code involved, this is the recommended way and it’s the way we’re going and you’ll se later why it’s a good choice.

First, let’s define the initial state for our food data with some default values so we know where to put things in the future:

import React, { Component } from 'react'

class FoodForm extends Component {
  state = {
    date: new Date().toISOString().split("T")[0],
    name: '',
    serving: 100,
    unit: 'g',
    carbs: 0,
    protein: 0,
    fat: 0,
    calories: 0
  }
  ...

Then we have to modify our form to get all the information about our food entry.

...
<form onSubmit={this.handleSubmit}>
  <h3>Add Food</h3>
  <div className="form-row">
    <div className="form-group col">
      <input
        type="date"
        className="form-control"
        placeholder="Date"
        onChange={this.handleChange}
        value={this.state.date}
        name="date"
        required
      />
    </div>
  </div>
  <div className="form-row">
    <div className="form-group col">
      <input
        type="text"
        className="form-control"
        placeholder="Name"
        onChange={this.handleChange}
        value={this.state.name}
        name="name"
        required
      />
    </div>
    <div className="form-group col">
      <input
        type="number"
        className="form-control"
        placeholder="serving"
        onChange={this.handleChange}
        value={this.state.serving}
        name="serving"
        min="0"
        step="0.1"
      />
    </div>
    <div className="form-group col">
      <input
        type="text"
        className="form-control"
        placeholder="unit"
        onChange={this.handleChange}
        value={this.state.unit}
        name="unit"
      />
    </div>
  </div>
  <div className="form-row">
    <div className="form-group col">
      <input
        type="number"
        className="form-control"
        placeholder="carbs"
        onChange={this.handleChange}
        value={this.state.carbs}
        name="carbs"
        required
        min="0"
        step="0.1"
      />
    </div>
    <div className="form-group col">
      <input
        type="number"
        className="form-control"
        placeholder="protein"
        onChange={this.handleChange}
        value={this.state.protein}
        name="protein"
        required
        min="0"
        step="0.1"
      />
    </div>
    <div className="form-group col">
      <input
        type="number"
        className="form-control"
        placeholder="fat"
        onChange={this.handleChange}
        value={this.state.fat}
        name="fat"
        required
        min="0"
        step="0.1"
      />
    </div>
    <div className="form-group col">
      <input
        type="number"
        className="form-control"
        placeholder="calories"
        onChange={this.handleChange}
        value={this.state.calories}
        name="calories"
        required
      />
    </div>
  </div>
  <button className="btn btn-primary">Submit</button>
</form>
...

In this case, we use the same handler for every input. It’s fine as long as we know how to distinguish from which input the event is coming when the method is invoked. That’s why we define the attribute “name” in our example. Also we won’t need to do any kind of complex data manipulation before saving to the state.

At this point you can’t type anything in any input. Actually you can, but when you do it, it triggers a render update in the component but because we set value with value={this.state.something} it’s kind of ignoring what we type and setting it with the state value. To be able to change the input value we have to update the state when we type.

Update the handleChange method to parse the some values to float before adding:

handleChange = event => {
  const input = event.currentTarget.value || ''
  const name = event.currentTarget.name
  this.setState({
    [name]: ['name', 'date', 'unit'].includes(name) ? input : parseFloat(input),
  })
}

And now that we have all the state set up and the events working, we need to add the data to the consumed list in the App component. The problem is that the FoodForm component doesn’t have access to the App’s state, so how do we do that?

As a component, FoodForm’s only purpose is to capture user input and send the information somewhere through a callback function.

Update the handleSubmit function:

handleSubmit = () => {
  event.preventDefault()
  const food = { ...this.state }
  this.props.onFormSubmit(food)}

This callback function, any other component using FoodForm will have to provide, in our case, it’s the App component:

render() {
  return (
    <React.Fragment>
      <Header />
      <div className="container">
        <FoodForm onFormSubmit={this.addFood} />        {Object.keys(this.state.consumed).map(key => (
          <Food key={key} food={this.state.consumed[key]} />
        ))}
      </div>
    </React.Fragment>
  )
}

This is a conversation like:

– FoodForm: “So, I’m providing a service of capturing food data from the user and delivering to an address. If anybody needs me, send the delivery address to onFormSubmit

– App: “Oh, that’s awesome! Can you capture it for me and send to the address addFood? Then I’ll decide what to do with it.”

Now, the app component has to get the data and add it to the consumed list:

state = {
  consumed: {},}

addFood = food => {  this.setState(current => {    current.consumed[Date.now()] = food    return current  })}

And that’s it, now when you fill the form and click on submit, a new food is added to the list.

Bonus Features

At this point everything is working fine but some things are annoying me.

First, after the submit, the information remains in the form, it would be good to clean it up after. This is a very simple react pattern called state initializer. We just have to create an object with the default information and initialize the state with this object. And after the submit, we set the state again with it:

import React, { Component } from 'react'

class FoodForm extends Component {
  static initialState = {    date: new Date().toISOString().split("T")[0],
    name: '',
    serving: 100,
    unit: 'g',
    carbs: 0,
    protein: 0,
    fat: 0,
    calories: 0
  }

  state = {    ...FoodForm.initialState  }
  handleSubmit = () => {
    event.preventDefault()
    const food = { ...this.state }
    this.props.onFormSubmit(food)
    this.setState({...FoodForm.initialState})  }
  ...

If we had more than one way of reseting the state, it would be good to create a reset function to do the dirty job but in our case this is good enough.

Another UX issue for me is the fact that the initial values are zero and every time I want to add a new food, I have to select the content and replace with something else. I would like to automatically select the content every time I click on one input, like when we navigate through the fields using the Tab key.

We just need to set a callback function to the onFocus event on every input. And this callback with select the content for us.

So, on every input add this:

<input
  type="text"
  className="form-control"
  placeholder="Name"
  onFocus={event => event.currentTarget.select()}  onChange={this.handleChange}
  value={this.state.name}
  name="name"
  required
/>

Easy, right?

The next one is related to the app functionality. Usually, when we need to add food to the list, we have to type the amount of carbs, protein, fat and calories, but the things is, the calories amount is a calculation of the other three like I mentioned in the previous post, we could ask the form to calculate that for us when we type.

In this case we will have to create a separate handler that gets the input value but also recalculates the calories:

handleCalculationChange = event => {
  const input = event.currentTarget.value || ''
  const name = event.currentTarget.name
  this.setState(current => {
    current[name] = parseFloat(input)
    current.calories = (current.carbs + current.protein) * 4 + current.fat * 9

    return current
  })
}

Yet, I still should be able to override the calories amount if I want because sometime we know the total calories but we don’t know the rest and we still want to be able to log, right?

So replace onChange={this.handleChange} with onChange={this.handleCalculationChange} but only for the carbs, protein and fat inputs.

So now, every time we type on carbs, protein or fat, the calories are recalculated. But if we still need to specify the calories, we can still do it.

The last is not really a feature, is a verification. When the FoodForm submits the food, it calls a callback function passed through the props but right now any value can be passed on the props. First we want FoodForm to complain if the passed value is not a function, and also we want it to ignore the submit as well.

To do this we will have to instal a separate package called PropTypes that does some validations for us.

So go to the terminal, navigate to the project’s root folder and type npm i prop-types. After the install, import the package on FoodForm and set the class’ propTypes to:

import React, { Component } from 'react'
import PropTypes from 'prop-types'
class FoodForm extends Component {
  static propTypes = {    onFormSubmit: PropTypes.func.isRequired  }
  static initialState = {
    date: new Date().toISOString().split("T")[0],
    name: '',
    serving: 100,
...

Here, we’re defining that the prop onFormSubmit should be a function and is required. To ignore the call if it’s not a function, we will have to do a manual test inside the handleSubmit function:

handleSubmit = event => {
  event.preventDefault()
  if (this.props.onFormSubmit instanceof Function) {    const food = { ...this.state }    this.props.onFormSubmit(food)    this.setState({ ...FoodForm.initialState })  }}

Now if you go to the App component and change the onFormSubmit to something else like a string or a number, nothing happens.

And that’s it! If you have any questions, let me know in the comments.

You can find the complete code at https://bitbucket.org/rafaelsm-blog/food-logger/src/first-app-adding-food-to-the-state/