Handling User Input in React — CRUD
Chapter 4 from the book; Vumbula React — Co-authored 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.
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
andprice
as properties to the state object. These will hold the new item before it is saved to state. - After the
name
andprice
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 thename
andprice
within the function argument parentheses. - Add a value attribute to both the
name
andprice
input elements with the variables destructed from the component argument list as necessary. - Add an attribute name to both the
name
andprice
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 theprop-types
package. Follow the steps below to add type checking. - Import
PropTypes
from theprop-types
package, note that this package is already installed, it is part of the dependencies in thepackage.json
but not bundled with React. It should always be installed separately usingnpm
.
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 acceptsevent
as its own argument within the App component. - Within the function, use the passed in
event
parameter to get thetarget
input element; from thetarget
get thevalue
andname
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 theonChange
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 theonChange
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
andprice
now need to be added to the items array in state, so that they are rendered when thesave
button is clicked. Let’s do this now. - Define an arrow function called
addItem
which acceptsevent
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
andprice
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 theid
of the last element in the items array or to use1
as theid
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 theAddItem
component with a value ofthis.addItem
within the App component. - Within the
AddItem
component, add anonSubmit
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 ofonSubmit
. - 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 acceptsindex
as its only argument. Theindex
will be used to find the item to be edited. - Within this function use the
setState
method and within it, define the item’skey
. To set its value, loop through theitems
array and when the item with the passed inindex
is found, add anisEditing
property with a value of!item.isEditing
. This will toggle theisEditing
boolean accordingly. The function implementation is as shown below.
- Add
toggleEdit
as a prop to theItemCard
component and define an arrow function that calls thetoggleItemEditing
function passing it theindex
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 anonClick
attribute to the edit button with thetoggleEditing
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 addtoggleEditing
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.
- Now, we use the
item.isEditing
property to either render the input fields or display thename
andprice
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 anevent
andindex
as its only arguments. - This function is similar to one we defined above that was updating the state with the
name
andprice
of an item before it was saved into the items array. The difference is that, we use thesetState
function to find the item with the passed inindex
and update itsname
and/orprice
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. HereonChange
is used for simplicity, but theonChange
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 takesindex
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 theItemCard
component with its value being an arrow function that calls theonDelete
function in the App component, passing it theindex
of the item to be deleted. - Within the
ItemCard
component destructure theonDelete
prop in the components argument list. - Go on and add
onDelete
to the componentspropTypes
.
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.