React State
#
Learning Objectives- Learn about state and forms
- Learn how to update state
- Learn how to make one source of truth
- Learn about events and use onChange and submit
#
Form Inputs / Controlled Components HTML elements like input
maintain their own state (this their default behavior as just plain HTML: as you enter or delete letters/characters into an input you see those letters change).
We'll need to give React control over our inputs, so that React's state will maintain the state of our inputs. That way there is one source of truth for our data (React state).
Let's add an input
render() { return ( <div> <h1> Big Time Shopping </h1> <form> <input type="text" /> </form> <ul> {this.state.products.map((product) => { return ( <li> {product.name} | {product.price} | {product.description} </li> ); })} </ul> </div> );}
We can still type into this field, but we need to connect it to React.
Let's expand state to hold our input value
constructor(props) { super(props); this.state = { products: products, value: "", };}
Now let's tie it into our input
render() { return ( <div> <h1> Big Time Shopping </h1> <form> <input type="text" value={this.state.value} /> </form> <ul> {this.state.products.map((product) => { return ( <li> {product.name} | {product.price} | {product.description} </li> ); })} </ul> </div> );}
If we've done it right, we won't be able to type in our form: We have set the state of the input field's value to be an empty string and currently we have no way of updating it.
React is optimized to only re-render elements that have changed/been updated. That is one its key features. We don't update state directly, rather, we use a function called setState
to update state.
Let's code it out.
We'll add a function that allows us to update the state of our input
class App extends React.Component { constructor(props) { super(props); this.state = { products: products, value: "", }; } handleChange(event) { console.log(event.target.value); } // ... // render ()}
And we'll call that function using the onChange
event (we covered event listeners/handlers in unit 1 with vanilla js/jQuery - the syntax is a little different but the principals are the same). One of the most common is click, but there are others like like mouse over, key up or key down or form submission.
render () { return ( <div> <h1> Big Time Shopping </h1> <form> <input type="text" value={this.state.value} onChange={this.handleChange}/> </form> // ...
Now we can see the letters we type log in the console. They still are not updating in the input field.
We may think we can just update state directly like in the example below but we CAN'T.
handleChange(event) { console.log(event.target.value); this.state.value = event.target.value; // NO!!!}
This is because we are trying to mutate state directly. React will not update the view if you mutate state directly. So you must update it using the setState
built-in function
handleChange(event) { this.setState({ value: event.target.value });}
Now if we try, we'll get an error that is something along the lines of Cannot read property 'setState' of undefined.
This probably seems strange. Several lines below we saw we were able to access this.state.products
inside of our render function. So why can't we access state here?
It has to do with this
and in simple terms JavaScript this
is defined when the function is invoked, not where it is defined. Since we are calling it inside the render function, we've changed scope and the value of this
is different than what it should be.
You can take some time to console log the value of this between the render()
and the return
and then again inside the handleChange
function to see that they are different.
We need to fix the context of this
. To do that we have to bind the value of this
inside the constructor. Now we'll see that our handleChange
function has the right context. Your constructor should now look like this:
constructor() { super(); this.state = { products: products, value: "", };
this.handleChange = this.handleChange.bind(this);}
And now our input box should update!
#
Updating more than one input fieldIt's very likely that our app will have more than one input field. We could write a handleChange
function for every field. But that seems like a lot of work.
What would be better would be to follow the proper guidelines for creating inputs.Which is to use label elements. Creating forms properly, by including labels and possibly fieldsets, helps improve accessibility - especially in the case of screen readers.
When we properly update our HTML with labels, we'll also end up with each input having an id
we can match this id
to the matching key in state and this will then make updating our input fields much easier.
Let's upgrade our product names first
this.state = { products: products, name: "", price: 0, description: "",};
Now, let's add a label element with the attribute hmtlFor
- the value will match the key for the input. Note just like class
is a reserved word in JavaScript, so is for
. So we have to use the React syntax which will get rendered into the proper attribute
Then, in the input element we'll add an id that will match the for value as well.
<h1>Big Time Shopping</h1><form> <label htmlFor="name"></label> <input type="text" value="{this.state.name}" onChange="{this.handleChange}" id="name" /></form><ul>...</ul>
One more step. Let's update our handleChange
function in order to restore the functionality
handleChange(event) { this.setState({ [event.target.id]: event.target.value });}
#
Add more fieldsAdding more fields should now be a snap! This is pretty typical for React in that it takes a lot of set up to do simple things, but once you have a good base, it's much easier to build on top of it.
Let's add inputs for price and description
<form> <label htmlFor="name">Name</label> <input type="text" value="{this.state.name}" onChange="{this.handleChange}" id="name" /> <br /> <label htmlFor="price">Price</label> <input type="number" value="{this.state.price}" onChange="{this.handleChange}" id="price" /> <br /> <label htmlFor="description">Description</label> <input type="textarea" value="{this.state.description}" onChange="{this.handleChange}" id="description" /> <br /> <input type="submit" /></form>
#
Preview and then submitTo demonstrate state and rendering, we'll make a little preview box. Let's put this after our last input and before the start of our ul
<div> <h2>Preview our new item</h2> <h3>Name: {this.state.name}</h3> <h4>Price: {this.state.price}</h4> <h5>Description: {this.state.description}</h5></div>
Take some time to make sure that as you update each input field, each preview gets properly updated as well.
When we are ready, we'd like to submit this new item and see it's name and price show up in our list.
#
Add a new itemOnce we are happy with our new item we want to submit it.
Upon submission we'll add our new item object into our array of products
Remember, form submits, by default, refresh the page. So the first thing we'll have to do is add an event listener for the submit event. Then we'll prevent the default behavior.
<form onSubmit="{this.handleSubmit}">...</form>
function handleSubmit(event) { event.preventDefault(); console.log("you prevented the default");}
You should see your console.log and it should stay. If you see it for a moment and it disappears, then you have not yet prevented the default behavior of submit.
Next, since we will be accessing this
, we will need to bind the value of this
for this function in the constructor
constructor(props) { super(props); this.state = { products: products, name: "", price: 0, description: "", }; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this);}
We'll make a new object with the correct structure for our products array. Then we'll add it to our array using ES6 syntax, rather than using a method, as recommended in the official React documentation
function handleSubmit(event) { event.preventDefault(); const newItem = { name: this.state.name, price: this.state.price, description: this.state.description, }; this.setState({ products: [...this.state.products, newItem], });}
#
One More Nice TouchIt would be nice to have the form reset once we have submitted our new item
handleSubmit(event) { event.preventDefault(); const newItem = { name: this.state.name, price: this.state.price, description: this.state.description, }; this.setState({ products: [...this.state.products, newItem], name: "", price: 0, description: "", });}