In this lesson, we will look at how to respond to events in React and use those events to manage the ever-changing state in our application.
Table of Contents
- State — Data that is used by an application at a particular point in time. State is often mutable, meaning it can be changed over time, usually in response to user actions or other events
- Stateful Component — A component that depends on state and is re-rendered whenever the state changes.
- Hooks — Functions that provide a wide variety of features for React components. They all begin with
use()
. useState
– A react hook for managing state within a React component. It returns an array with a state value and a setter function. It triggers the component to re-render when the state changes.- Lifting state up — A practice where state is defined in a parent component so that it can be used by its child components.
- Controlled Form — A form whose value changes are controlled by a piece of state.
In this lesson, we'll be using an app called instapets
to demonstrate building stateful components. A stateful component is one that depends on state and re-renders whenever the state changes.
State is the data that is used by an application at a particular point in time. State is often mutable, meaning it can be changed over time, usually in response to user actions or other events
Right now the app is not stateful. It renders 3 hard-coded pet pictures, the form doesn't work and neither do the "Like" buttons.
Let's build this thing!
Let's tackle the likes buttons first.
Each InstagramPost
component renders a picture, a caption and a button to increment and display likes.
Notice how we added an
onClick
prop with thehandleClick
callback function.
const InstagramPost = ({ picture }) => {
let likes = 0;
const handleClick = {
}
return (
<div className="insta-pic">
<img alt={picture.caption} src={picture.src} />
<p>{picture.caption}</p>
<button onClick={handleClick}>❤️ {likes}</button>
</div>
);
};
export default InstagramPost;
A stateful component is one that renders state — data values that may change.
Q: What data values does this component render? Is any of that considered "state"?
likes
andpicture
These values are not considered state because they are hard-coded! They will not change.
Let's make likes
a piece of mutable state.
We want to update likes
each time we click on the Like
button. So, maybe this will work?
const InstagramPost = ({ picture }) => {
console.log('rendering InstagramPost');
let likes = 0;
const handleClick = () => {
likes++;
console.log(likes);
}
return (
<div className="insta-pic">
<img alt={picture.caption} src={picture.src} />
<p>{picture.caption}</p>
<button onClick={handleClick}>❤️ {likes}</button>
</div>
);
};
While this does increment the likes
value, it doesn't cause the component to re-render because React isn't watching this value for changes.
So how do we make the component re-render with the updated likes
value?
We need a hook. Hooks in react are functions that perform a variety of jobs. They can be identified by their name which starts with "use":
useState()
useEffect()
useNavigate()
useParams()
useContext()
- etc...
The useState
hook allows us to create a piece of state that React will watch and when the state changes, it will re-render.
Here's how it works:
// InstagramPost.jsx
import { useState } from "react";
useState
is a named export of thereact
package (note the{}
around the function in theimport
statement).
const InstagramPost = () => {
const [likes, setLikes] = useState(0);
// handleClick and the return statement
};
useState
must be called at the top of a component. Otherwise weird stuff happens.useState(0)
returns an array with two values:- A piece of state data (
likes
) with a starting value (0
) - A "setter" function for updating that state data (
likes
) and re-rendering the component
- A piece of state data (
- The convention is to name state variables like
[something, setSomething]
using array destructuring.
const handleClick = () => {
setLikes(likes + 1); // this is OK but can cause some race issues
setLikes((currentLikes) => currentLikes + 1); // this is better when the next value depends on the current value
likes++; // Don't do this
};
- When the event handler is clicked, we'll invoke
setLikes
which either accepts:- the new value that we want to set
likes
to or... - a callback function for turning the current value of
likes
into the next value oflikes
.
- the new value that we want to set
- As we saw, incrementing
likes
directly does not cause the component to re-render setLikes
will cause the component to re-render with the provided value as the new value forlikes
import { useState } from 'react';
const InstagramPost = ({ picture }) => {
const [likes, setLikes] = useState(0)
const handleClick = () => {
setLikes((currentLikes) => currentLikes + 1)
}
return (
<div className="insta-pic">
<img alt={picture.caption} src={picture.src} />
<p>{picture.caption}</p>
<button onClick={handleClick}>❤️ {likes}</button>
</div>
);
};
Quiz!
- Why did we pass in
0
when we invokeduseState
? - What does
useState()
return? - What does
setLikes()
do? What kinds of inputs does it take?
Interestingly setLikes
does NOT change the value of likes
within the handleClick
callback. It tells React to re-render the InstagramPost
component with a new value of likes
.
You can see this if you place a console.log(likes)
statement inside of handleClick
callback.
This kind of makes sense: setLikes
isn't actually changing any value. It's just saying what the next value should be.
Next up we'll make a form for the user to add new pet pictures.
Creating a form using JSX in React is almost identical to creating a form using HTML. Take a look at NewPetForm.jsx
:
const NewPetForm = () => {
const handleSubmit = (e) => {
e.preventDefault();
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="src-input">Image Source:</label>
<input type="text" name="src" id="src-input"/>
<label htmlFor="caption-input">Caption:</label>
<input type="text" name="caption" id="caption-input" />
<button>Submit</button>
</form>
)
}
- Instead of
for
we usehtmlFor
when connecting labels and inputs. - We use
onSubmit
instead of usingaddEventListener
.
Now, how do we handle the submission event?
A controlled form is a form element whose input values are controlled by React state rather than through DOM manipulation.
To create a controlled form, we will:
- Create a piece of state for each input we want to control
- Assign the
value
prop of the input to the input state value we just created - Assign an
onChange
handler to the input that invokes the state setter function - When handling submissions, we can simply reference the input state values.
- Remember to reset the state values after submission.
const NewPetForm = () => {
// 1. Create a piece of state for each input we want to control
const [src, setSrc] = useState('');
const [caption, setCaption] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
// 4. When handling submissions, we can simply reference the input state values.
console.log(src, caption);
// 5. Remember to reset the state values after submission.
setSrc('');
setCaption('');
}
// 2. Assign the `value` prop of the input to the input state value we just created
// 3. Assign an `onChange` handler to the input that invokes the state setter function
return (
<form onSubmit={handleSubmit}>
<label htmlFor="src-input">Image Source:</label>
<input
type="text"
name="src"
id="src-input"
value={src}
onChange={(e) => setSrc(e.target.value)}
/>
<label htmlFor="caption-input">Caption:</label>
<input
type="text"
name="caption"
id="caption-input"
value={caption}
onChange={(e) => setCaption(e.target.value)}
/>
<button>Submit</button>
</form>
)
}
- Notice how each input has a
value
and anonChange
prop associated with a particular piece of state. - When it is time to submit the form, we can easily use the
src
andcaption
state values without digging through the form.
The last step to putting this together is having the form submission actually add a new picture to the list of pictures.
Here is the component tree of the application:
The challenge is that PicturesList
is where the pictures
are defined but we want to update the list of pictures from NewPetForm
.
If we were to turn the pictures
array into some state like this:
const [pictures, setPictures] = useState(initialPictures);
Q: Where should I put this? Why?
The state should be defined in the
App
which is the closest shared ancestor of theNewPetForm
and thePicturesList
. TheApp
can then pass those values down to its children as props. This is called "lifting state up". Check out the1-instapets-final/
to see how this is done:
App
usesuseState
to define thepictures
andsetPictures
values- It passes down
pictures
toPicturesList
- It makes an
addPicture
helper function and passes it down toNewPetForm
to invoke upon submission.- Notice how
addPicture
sets the state by copying the existing array and adding a new object.