Handling User Input in React — CRUD

Chapter 4 from the book; Vumbula React — Co-authored by Edmond Atto

John Kagga
The Andela Way

--

Cover Image by Edmond Atto

Recap:
-
In Chapter One, get introduced to React with ES6. If you are new to React , simply need a refresher, or need a gentle introduction to the ES6 features that are most frequently used throughout this book, review chapter one.
- In Chapter Two, get introduced to React components. They are the building blocks of any React Application you will build. Go ahead and review that chapter if you are not yet comfortable with React components and then head back here.
- In Chapter Three, get introduced to State in React. Understanding State and how it works will unlock your ability to build powerful components. Go ahead and review that chapter if you are not yet comfortable with React State and then, head back here.

Vumbula.io

Majority of the applications you will build will have the bulk of their functionality centered around detecting and responding to user input. This could be via a click or more likely, a form. This chapter will focus on forms and making sure you have a good understanding of how forms are used in React.

Forms are the most common way to receive input from a user, for example, forms are used to collect users’ login details. When the user clicks the login button, these details are submitted and they may then be handed over to the application’s backend (authentication) service for processing. Depending on whether the login was successful or not, the frontend is updated accordingly. Consequently, forms also make it possible for users to update already existing information such as their username on a social media site when they believe they’ve found a cooler one.

When working with forms in React, two types of components are typically used:

=> Controlled components

=> Uncontrolled components

Controlled Components

HTML form elements are unique because by default, they maintain some internal state. Specifically, form elements such as the <input> and <textarea> maintain and update their own internal state. For this reason, we have to think more carefully about how we use them in React.

In chapter 3, it is pointed out that a component’s mutable data is stored in its state property. It makes sense then, to combine the HTML forms’ “natural” abilities with React’s state to make React’s state the singular data source.

This combination creates a situation where the component that renders a form also controls what action is taken upon user input. For this reason, such a component is called a controlled component. Inputs that live inside controlled components are known as controlled inputs.

Here’s an example of a controlled component that renders a login form. For demonstration purposes, we shall use the username field.

In the above example, the LoginForm component is setup with a state object containing a username property. This property will hold the value/text entered by the user.

Initially, there is no text displayed in the input field because its value attribute is set to this.state.username which is initialised with username set to an empty string.

When the user clicks on the input field and starts typing, each keystroke triggers the onChange event handler. The handleChange function is then called and the current value (text) in the input is saved to state using setState().

setState() causes the component to rerender and the text displayed in the input field is now fetched from this.state.username. The text in the h3 is also updated upon the re-render.

This flow ensures that the input, h3 and state are always in sync since the state object is the single source of truth for the component.

Using controlled components ensures that:
=> the inputs (username field in this example) and the data (state) are always in sync
=> the UI (h3 tag in this example) and the data (state) are always in sync.

Working with multiple inputs

It is unlikely that an application will have a form with just one field. Let’s add an extra field to the LoginForm component from before to explore how to implement multiple controlled inputs with minimal markup.

In order to use a single handleChange function for multiple inputs, each input field is given a name attribute. The handleChange function is altered to perform a different action depending on the input target. Here, we use the power of ES6’s computed property name [name]: value to update the state key corresponding to a particular input’s name attribute.

Uncontrolled Components

In uncontrolled components, form data is handled by the DOM, unlike controlled components in which the form data is handled by a React component.

Uncontrolled components leverage the fact that HTML form elements maintain their own internal state. When dealing with uncontrolled inputs, state management via a React component is not required.

In uncontrolled components, form data is accessed using refs. Think of a ref as a tag that you receive when you check your bag in at the airport (just go with it). When your flight lands, you present your tag which serves as a reference to which bag is yours. The person at the bag desk takes your tag and returns minutes later with your bag. Similarly, HTML forms know which data belongs to which input field and by assigning an input a ref, you can then retrieve its value later.

In this analogy, you can only retrieve your bag after the flight has landed. Similarly, you can only use refs to fetch form data after a form has been submitted.

Here is an example of an uncontrolled component.

In this example, notice that the input has a ref attribute. The input element is passed as input to the arrow function and is then assigned to this.input.

When the form is submitted, the handleSubmit function is fired and at this point, the text entered by the user can be accessed using this.input.value.

Using Default Values in Controlled Components

In cases where the user needs to update an already existing value, for example a profile update, the input field should display the pre-existing value and remain editable.

During a React component’s render lifecycle, a form’s value attribute will always override the value attribute in the DOM. Consequently, you can use React to set the initial value and leave subsequent updates as uncontrolled.

In the LoginForm component above, the input field initially renders with the text cool-guy because of the value passed to the defaultValue attribute. Alternatively, a value from state can be passed in here.

Upon submission of the form, the input field’s value attribute overrides the defaultValue.

Controlled Vs Uncontrolled

Using controlled components is widely viewed as the preferred way to work with forms in React. This because they are more powerful than uncontrolled components and offer a number of benefits, that is to say:

=> The inputs, data and UI are always in sync

=> They allow for instant field validation

=> They allow for custom input formatting before submission for example converting all entered email addresses to lowercase before form submission

That said, using controlled components where forms with numerous input fields are involved can be tedious. This is because you would be required to write onChange handlers covering every possible way your data can change and channel all input data through a React component.

In situations where the form you are dealing with is relatively simple and only requires submission of user data with no dynamic UI updates during user input, or input formatting before submission, uncontrolled components could be a better choice.

Project Three (Continued)

You now know enough about forms to build out the rest of the features for project three from the previous chapter.

Adding form data to state

It is time to use the form on top of the page to add names and their corresponding ages. To do this, some changes need to be made to the class component, particularly to the form element.

In order to get the name and age entered by the user, we need to add a ref attribute to the name and age input elements. The ref attribute accepts a callback which receives the underlying DOM element as its argument.

The ref callback is then used to store a reference to the text input of the DOM element within an instance variable, in our case the instance variables are the name and age as shown in the code snippet below.

Add the ref lines to your name and age input elements to match the code shown above.

Now that we have a reference to the text entered by the user, we need to add it to state when the save button is clicked. To do this effectively, we need to add an onSubmit event handler to the form element which will be called when the save (submit) button is clicked. This onSubmit handler attribute expects a function which will be executed when the save button is clicked.

Therefore, define an arrow function with a name of onSubmit that accepts event as its only argument within the Application component. Within the onSubmit function prevent the default button behaviour (of reloading the page when it is clicked) by adding event.preventDefault().

We also need to get the name and age text entered by the user from the instance variables we set in the ref callbacks. After all that we update the component state using this.setState as shown in the code snippet below.

Finally, add the onSubmit attribute to the form element and this.onSubmit as its value referencing the onSubmit function defined within the component.

<form className="form-inline" onSubmit={this.onSubmit}>

Now open the index.js file in the browser and type kagga in the name input field and 30 in the age input field then click the save button. A new card will be added on the page as shown in the image below.

State immutability

You can now add the form data into state and display it on the page.

But wait…did you see it? It is okay if you did not. In the introduction of chapter 3, it was pointed out that state in React should never be mutated that to say, should be immutable.

Looking back at the onSubmit function, we mutated state when we used the push method on the data array from this.state.data.

The right way to update state is to create a new data array and then update the state with that new array. This can be achieved in many ways but we are going to use the ES6 spread operator to create a new data array and also add the new info object containing the name and age from the form as shown in the code snippet below. Make the necessary changes to earlier code.

With the above changes we still get the same results With the added benefit of state immutability. Find all code for this section here.

Project Four: Building a Shopping List App

In this project, we shall combine everything we’ve learnt until this point as we build our shopping list app. Our app will allow for CRUD functionality.

The starter files are available for download here, in there, you will find TODOs to guide you if you would like to attempt the project on your own. The final code for the project is also available here and has solutions to all the TODOs in the starter code.

After cloning the repository, cd into your project directory and install the dependencies by running npm install or yarn install. Run npm start or yarn start to view the project in the browser and make sure that everything is working well.

In the src folder, you will find a file App.js that contains an App component. Inside the App component, there are functional components which include Nav, Jumbotron, AddItem and Footer.

Adding Items to the Shopping List

  • Add name and price as properties to the state object. These will hold the new item before it is saved to state.
  • After the name and price have been saved as a new item in the items array that is within state, they are reset to their defaults.

Destructure the name and price from the state object and pass them as props to the AddItem component.

  • Also within the AddItem component, destructure the name and price within the function argument parentheses.
  • Add a value attribute to both the name and price input elements with the variables destructed from the component argument list as necessary.
  • Add an attribute name to both the name and price input elements with string values of name and price respectively.
  • We need to check the type of the props we are passing to the AddItem component. To do this we use the prop-types package. Follow the steps below to add type checking.
  • Import PropTypes from the prop-types package, note that this package is already installed, it is part of the dependencies in the package.json but not bundled with React. It should always be installed separately using npm.

Add a proptypes object for name and price with a type of string and mark them as required as shown below.

At this point your component should look like this:

In order to get the name and price the user types into the input fields, we need to add an onChange event listener to both the name and price inputs.

  • Create an arrow function called handleInputChange which accepts event as its own argument within the App component.
  • Within the function, use the passed in event parameter to get the target input element; from the target get the value and name of the input element.

Use the setState function to add the name and/or price to the name and price properties in the state object.

Define an onChange prop on the AddItem component with a value of this.handleInputChange

<AddItem
name={name}
price={price}
onChange={this.handleInputChange}
/>
  • In the AddItem file and component, add the onChange prop to the list of destructured elements in the function argument list.
  • Add onChange to the propTypes object as a required function.
  • Add an onChange attribute to both input elements with the value of the onChange prop.

At this point your AddItem component should look like this

  • Head over to the browser, let’s check out our progress.

Open the React developer tools and look at the state section. Notice that within the state section, the name and price properties have empty strings as their values.

As you type into the name or price input fields, the state updates with each keystroke.

  • The name and price now need to be added to the items array in state, so that they are rendered when the save button is clicked. Let’s do this now.
  • Define an arrow function called addItem which accepts event as its only argument
  • Within it call preventDefault() on event, to prevent the default behaviour of the button.
  • Use destructing to get the set name and price from state.
  • Since an id is needed when saving an item to be used as a key, get the length of the existing items array in state. Then, use the ternary operator to either increment the id of the last element in the items array or to use 1 as the id if the items array is empty.

Use the setState function to add the new item to the items array. Remember not to mutate state. Use the spread operator for the existing items within the array and the Object.assign function for adding the new item to the array. Set the name and price back to their defaults as shown below.

  • Define an onSubmit prop on the AddItem component with a value of this.addItem within the App component.
  • Within the AddItem component, add an onSubmit to the list of destructured elements in the function argument list.
  • Add onSubmit to the proptypes object as a required function.
  • Add an onSubmit attribute to the form with the value of onSubmit.
  • Moment of truth, open the app in your browser. At this point you should be able to view the added item when you click the save button.

The final working code for this section can be found here.

Editing/Updating the Items on the Shopping List

In this section we are going to tackle editing and updating the items. The general idea is to click the edit button so that the name and price fields turn into input fields, thus giving the user the ability to modify their content. After modifying the name and price the user can then click the save button in order for the name and price to revert to their display mode. The starter code for this section can be found here.

Let’s get started

  • Define an arrow function with a name of toggleItemEditing which accepts index as its only argument. The index will be used to find the item to be edited.
  • Within this function use the setState method and within it, define the item’s key. To set its value, loop through the items array and when the item with the passed in index is found, add an isEditing property with a value of !item.isEditing. This will toggle the isEditing boolean accordingly. The function implementation is as shown below.
  • Add toggleEdit as a prop to the ItemCard component and define an arrow function that calls the toggleItemEditing function passing it the index as the argument.
  • This function acts as a callback and will only execute when a button is clicked.
toggleEditing = {() => this.toggleItemEditing(index)}
  • Within the ItemCard component, add an onClick attribute to the edit button with the toggleEditing prop as its value.
  • Use the isEditing property of the item to toggle between showing Edit or Save as the button text. Do not forget to add toggleEditing to the list of propTypes in the ItemCard component.
<button
type="button"
className="btn btn-primary mr-2"
onClick={toggleEditing}
>
{item.isEditing ? "Save" : "Edit"}
</button>
  • At this point, clicking the edit button in the browser will toggle its text between Save and Edit.
  • Also note that the isEditing property changes its value whenever the Edit button is clicked as shown below in the React devtools.
change image
  • Now, we use the item.isEditing property to either render the input fields or display the name and price of the item within the card body.

Also add the value attribute to the name input element with a value of item.name and also the price input element with item.price.

At this point, when you open the app in the browser and click the edit button, the input fields should be visible. Clicking the save button should cause them to disappear as shown below.

Note that attempting to type into the item-name and item-price fields will not work at this point. This is because we are using controlled inputs but the inputs do not have onChange event handlers. Fixing this is fairly forward, we need to write a function to handle the editing functionality.

  • Within the App component define an arrow function with a name of handleItemUpdate which accepts an event and index as its only arguments.
  • This function is similar to one we defined above that was updating the state with the name and price of an item before it was saved into the items array. The difference is that, we use the setState function to find the item with the passed in index and update its name and/or price with the new values. We use the spread operator to populate the already existing item properties.

We return the item after updating it as shown below.

By now, you know the flow. Go ahead and add an onChange prop to the ItemCard component with the above function as its value.

Add the passed in prop to the ItemCard component argument list and use an arrow function which accepts an event. This function returns this prop as the value to the onChange attribute to both the name and price input elements, passing it the event and index as shown below.

onChange = {event => onChange(event, index)}

The onChange prop name can have any name. Here onChange is used for simplicity, but the onChange attribute on the input elements CANNOT have any other name

The ItemCard component looks like this in the end.

The app should now permit update of the name and/or price of any item successfully. Find the final code for this section here.

Deleting an Item from the Shopping List

Deleting an item should be straightforward, the idea is that when a user clicks the delete button, an item is removed from the items array in state. Here is the starter code for this section.

Let’s get started adding the delete functionality.

  • Define an arrow function, onDelete that takes index as its only argument.
  • Within the function, call the setState function and define an object with items as a property key and the value being an empty array.
  • Within the array, use the spread operator to populate the array with items from the zeroth index to the item before the passed in index using the slice method.

At this point, only part of the array is being included in the new array using the spread operator. To add the remaining part of the array without the item with the passed in index (item to be deleted), the spread operator and the slice method are used again to get the items at the index passed in + 1 as shown below.

onDelete = index => {
this.setState({
items: [
...this.state.items.slice(0, index),
...this.state.items.slice(index + 1)
]
});
};
  • Moving on, define an onDelete prop on the ItemCard component with its value being an arrow function that calls the onDelete function in the App component, passing it the index of the item to be deleted.
  • Within the ItemCard component destructure the onDelete prop in the components argument list.
  • Go on and add onDelete to the components propTypes.

Finally add an onClick attribute to the delete button with the onDelete prop as its value as shown below.

<button
type="button"
className="btn btn-primary"
onClick={onDelete}>
Delete
</button>

Save all your changes and open the app in a browser, when you click on the delete button that item card should be deleted and thus, disappear. Find the final code for this section here.

If you have found this article useful, reach over to the 👏 button and hit it as many times as you have enjoyed reading this post. Your responses are also highly appreciated. You can also find me on twitter.

--

--

John Kagga
The Andela Way

Andela |The Andela Way Editor | Arvana |Facebook Dev Circles| Long-life Learner