React 3
What will we learn today?
Learning Objectives - React Week 3
Data fetching (advanced)
- Allow effects to update in response to prop changes
- Explain why a component with an effect dependent on props is broken with empty
useEffect()
dependencies ([]
) - Be able to fix a component with an effect dependent on props using the
useEffect()
dependencies - Can describe the "lifecycle" of a component with
useEffect()
when props change
- Explain why a component with an effect dependent on props is broken with empty
Forms
- Create a simple form in React using the controlled component pattern
- Can initialise state with
useState()
- Be able to set the input
value
to the state variable - Can explain why the input does not change when typing if
onChange
is not set - Be able to update the state using an
onChange
handler
- Can initialise state with
- Use data from a submitted form to update the application
- Be able to handle an
onSubmit
event to the form - Be able to collect the form state variables and use them (setting state, POST request)
- Be able to handle an
Recap
Last week we looked at using props and state to create React components that change with user input (interactive example):
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={increment}>Click me</button>
</div>
);
}
export default Counter;
We also looked at fetching data in our React components (interactive example):
import React, { useState, useEffect } from "react";
const MartianPhotoFetcher = () => {
const [marsPhotos, setMarsPhotos] = useState();
useEffect(() => {
fetch(
`https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?earth_date=2015-6-3&api_key=gnesiqnKCJMm8UTYZYi86ZA5RAnrO4TAR9gDstVb`
)
.then(res => res.json())
.then(data => setMarsPhotos(data));
}, []);
if (!marsPhotos) {
return null;
} else {
return (
<img
src={marsPhotos.photos[0].img_src}
alt="Mars Rover"
style={{ width: "100%" }}
/>
);
}
};
export default MartianPhotoFetcher;
Updating Data Fetching when Props Change
Last week we looked at how we could fetch data from an API and render it in our React applications. However, there was a problem with the method that we learned before. To understand this problem we first have to understand the lifecycle of a component.
The Circle of Life
Let's take a look at an example:
Exercise A |
---|
1. Open this CodeSandbox. |
2. Take 5 minutes to read the code. |
3. Click the "Fetch image for 2019" button. If you're feeling confident: predict what is going to happen before you click the button. |
4. Now click the "Fetch image for 2020" button. What did you expect to happen? What actually happened? Can you explain why? |
Together let's "play computer" to break down exactly what is happening with these components:
- When the page loads, the
App
function component is called - It doesn't have any
date
state already, so we initialise it to an empty string (""
) withuseState
It renders the 2 buttons, but because
date
is an empty string, it does not render theMartianImageFetcher
component. Insteadnull
is returned, which means that nothing is renderedfunction App() { const [date, setDate] = useState(""); ... return ( <div> <button onClick={handle2019Click}>Fetch image for 2019</button> <button onClick={handle2020Click}>Fetch image for 2020</button> {date ? <MartianImageFetcher date={date} /> : null} </div> ); }
- When we click the "Fetch image for 2019" button, the
handle2019Click
click handler is called - The state is set by
setDate
to be"2019-01-01"
, and a re-render is triggered - The
App
function component is called again This time,
useState
remembers that we havedate
state and it is set to"2019-01-01"
function App() { ... function handle2019Click() { setDate("2019-01-01"); } ... return ( ... <button onClick={handle2019Click}>Fetch image for 2019</button> ... ); }
- Now
App
does renderMartianImageFetcher
and passes thedate
state as a prop (also nameddate
) - The
MartianImageFetcher
function component is called for the first time useState
knows that we haven't got anyimgSrc
state so initialises it tonull
- We queue an effect, which will run after we render for the first time
Because the
imgSrc
state is set tonull
, we returnnull
. This means that nothing is renderedfunction MartianImageFetcher(props) { const [imgSrc, setImgSrc] = useState(null); useEffect(() => { ... }, []); if (!imgSrc) { return null; } else { return <img src={imgSrc} />; } }
- Now that the component has rendered for the first time, the effect is run
- A
fetch
request is made to the NASA API (🚀!) When the request data comes back, we set the
imgSrc
state to a piece of the data, which triggers a re-renderfunction MartianImageFetcher(props) { ... useEffect(() => { fetch( `https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?earth_date=${ props.date }&api_key=gnesiqnKCJMm8UTYZYi86ZA5RAnrO4TAR9gDstVb` ) .then(res => res.json()) .then(data => { setImgSrc(data.photos[0].img_src); }); }, []); ... }
- The
MartianImageFetcher
function component is called again useState
remembers that theimgSrc
state is set to the data from the API- This time, we do not queue an effect. We used an empty array (
[]
) as theuseEffect
dependencies argument which means that we only queue effects on the first render We do have
imgSrc
state set, so we render the image using the data from the API 🎉function MartianImageFetcher(props) { const [imgSrc, setImgSrc] = useState(null); ... if (!imgSrc) { return null; } else { return <img src={imgSrc} />; } }
Phew! That was a lot of work just to render an image! But we're not quite done yet, we still need to find out what happens when we click the "Fetch image for 2020" button:
- In the
App
component, thehandle2020Click
click handler is called - The
date
state is set to"2020-01-01"
and a re-render is triggered - The
App
function component is called again and thedate
state is set to"2020-01-01"
The
date
prop that is passed toMartianImageFetcher
is different which means that it has to re-renderfunction App() { ... function handle2020Click() { setDate("2020-01-01"); } ... return ( ... <div> ... <button onClick={handle2020Click}>Fetch image for 2020</button> ... {date ? <MartianImageFetcher={date} /> : null} ... </div> ... ); }
- In the
MartianImageFetcher
componentuseState
remembers that we already hadimgSrc
state. It is set to the image from 2019 - Again, we do not queue the effect because this is a re-render and
useEffect
has been passed an empty array[]
Because
imgSrc
state has been set previously we render the image from 2019function MartianImageFetcher(props) { const [imgSrc, setImgSrc] = useState(null); useEffect(() => { ... }, []); return <img src={imgSrc} />; }
Exercise B |
---|
1. Did you spot where the bug was? Discuss with a group of 2 - 3 students where you think the bug is. |
2. Report back to the rest of the class where you think the bug happened. |
The key that the useEffect
in MartianImageFetcher
is only run once. This is because we told React that the queue should be queued on the first render only. However, as we saw, sometimes you need the effect to run again when some data changes. In this case the date
prop, changed from "2019-01-01"
to "2020-01-01"
, meaning that we have to fetch data different data. We call this a dependency of the effect.
useEffect
dependencies array
To solve this problem, we can tell React to queue the effect on the first render and when a dependency changes. We do this by adding the dependency variable to the array (interactive example):
function MartianImageFetcher(props) {
const [imgSrc, setImgSrc] = useState(null);
useEffect(() => {
...
}, [props.date]);
...
}
Now when the date
prop changes, React knows that the effect must be run again, this time with the 2020 data. Because of this behaviour, the second argument to useEffect
is called the dependencies argument. We use it whenever we have something in our effect that depends on a variable outside of the effect function.
To help you understand this better, try "playing computer" again, but this time think about what happens when we use [props.date]
for the dependencies argument. Think carefully about what changes with step 6 after we click the 2020 button.
Exercise |
---|
1. Open the pokedex React application from last week and open the src/BestPokemon.js file. |
2. Copy the BestPokemonSelector component from this CodeSandbox. Then paste it into src/BestPokemon.js . |
3. Change the default export so that it exports BestPokemonSelector instead of BestPokemonFetcher . |
4. Take a few minutes to read what the BestPokemonSelector component does. If you have questions, ask a Teaching Assistant to help. |
5. In the BestPokemonFetcher component change the URL to use backticks (`...` ) instead of double-quotes (" ). Then replace the number 1 with ${props.pokemonId} . What will this do? Click here if you don't knowThe URL will contain the pokemonId instead of always fetching the pokemon with id of 1 |
6. Open your browser and find the BestPokemonSelector component. Before you click the buttons, think about what you expect will happen. Then click the "Fetch Bulbasaur" button to find out what actually happens. |
7. Refresh the page. What happens now if you click the "Fetch Bulbasaur" button, then click the "Fetch Charmander" button? Is this what you expected? Explain to someone why this happens. |
8. Fix the bug by adding props.pokemonId to the useEffect dependencies array in BestPokemonFetcher . Remember that you can test if the bug still happens by refreshing the page, clicking one of the buttons, then the other button. |
ESLint rules for React Hooks
As you may have noticed, VSCode highlighted the empty dependencies array when you changed the URL passed to fetch
in BestPokemonFetcher
.
This is because your React application is using the rules from eslint-plugin-react-hooks
, a package created by the developers who make React. It helps you to find bugs in React Hooks code by highlighting places where you might be missing dependencies.
If you see a red squiggly line underneath your useEffect
dependencies array, you can hover your mouse over and it will tell you which variable is missing so you can add it to the dependencies array. Here's an example:
Loading state
Exercise A |
---|
1. Open this CodeSandbox. |
2. Click the "Fetch image for 2019" button and wait for the image to load. |
3. Now click the "Fetch image for 2020" button. Do you think this is a good experience for the user? Explain what you think is wrong to a Teaching Assistant. |
In the application above, the image from 2020 takes a while to load. This makes it feel like the app is broken: the user might think that they didn't actually click the 2020 button or that it is not working correctly. We are not telling the user that something is happening, it's just taking a bit of time to load.
We can fix this by adding a loading state. Let's take a look (interactive example):
function MartianImageFetcher(props) {
...
if (!imgSrc) {
return "Loading...";
} else {
return <img src={imgSrc} />;
}
}
Previously, we were just rendering nothing (by returning null
) when we didn't have any imgSrc
. We can tell the user that this by instead rendering something telling them that we're still waiting for the data to come back.
There is still a problem though: when we click to fetch another image, we still have imgSrc
set to the previous image. What we could do instead is set the imgSrc
back to null
when we know that we're fetching another image (interactive example):
function MartianImageFetcher(props) {
...
useEffect(() => {
setImgSrc(null);
...
}, [props.date]);
...
}
Exercise B |
---|
1. Open the pokedex React application again and open the src/BestPokemon.js file. |
2. In the BestPokemonFetcher component, instead of returning null if there is no pokemon , return "Loading..." . |
3. Now add setPokemon(null) inside the useEffect callback, before the call to fetch . |
4. Try clicking on the "Fetch Bulbasaur" and "Fetch Charmander" buttons quickly. Do you see the loading state? (It may only appear for a flash, the Pokemon API is very fast). |
Working with forms in React
Modern web applications often involve interacting with forms such as creating an account, adding a blog post or posting a comment. This would involve using inputs, buttons and various form elements and being able to get the values entered by users to do something with it (like display them on a page or send them in a POST request). So, how do we do this in React?
A popular pattern for building forms and collect user data is the controlled component pattern. A pattern is a repeated solution to a problem that is useful in multiple similar cases. Let's have a look at an example (interactive example):
class SimpleReminder extends Component {
constructor(props) {
super(props);
this.state = {
reminder: ""
};
}
handleChange = event => {
this.setState({
reminder: event.target.value
});
};
render() {
return (
<div>
<input
type="text"
placeholder="Some reminder"
value={this.state.reminder}
onChange={this.handleChange}
/>
<p>Today I need to remember to... {this.state.reminder}</p>
</div>
);
}
}
We're controlling the value
of the input by using the value from the reminder
state. This means that we can only change the value by updating the state. It is done using the onChange
attribute and the method handleChange
which is called every time the input value changes (typically when a new character is added or removed). If you didn't call this.setState()
in the handleChange
method, then the input's value would never change and it would appear as if you couldn't type in the input! Finally, the value we keep in the reminder
state is displayed on the screen as today's reminder.
In addition, instead of just saving the value of the input in the state, we could have also transformed the string before we set it with this.setState()
, for example by calling toUpperCase()
on the string.
Let's have a look at a more complex example where we want to build a form to let users enter information to create a personal account (interactive example):
class CreateAccountForm extends Component {
constructor(props) {
super(props);
this.state = {
username: "",
email: "",
password: ""
};
}
handleChange = event => {
this.setState({
[event.target.name]: event.target.value
});
};
submit = () => {
console.log("Do something with the form values...");
console.log(`Username = ${this.state.username}`);
console.log(`Email = ${this.state.email}`);
console.log(`Password = ${this.state.password}`);
};
render() {
return (
<div>
<div>
<input
type="text"
name="username"
placeholder="Username"
value={this.state.username}
onChange={this.handleChange}
/>
</div>
<div>
<input
type="text"
name="email"
placeholder="Email"
value={this.state.email}
onChange={this.handleChange}
/>
</div>
<div>
<input
type="password"
name="password"
placeholder="Password"
value={this.state.password}
onChange={this.handleChange}
/>
</div>
<button type="submit" onClick={this.submit}>
Create account
</button>
</div>
);
}
}
We now have three different inputs named username
, email
and password
, and we keep each entered value in a state with the same name. The method handleChange
is reused to keep track of each change of value. The trick here is to use the name of the input element to update the corresponding state. Finally, when the user clicks on the submit button, the submit
method is called to process the values. They are currently just displayed in the console but you could imagine validating the format of these values and sending them in a POST request.
Additional note: Have you seen this strange syntax in the setState
of handleChange
? It's called a computed property name. In a Javascript object, you can use a variable wrapped in square brackets which acts as a dynamic key, such as:
const myFirstKey = "key1";
const myFirstValue = "value1";
const dynamicKeyObject = { [myFirstKey]: myFirstValue };
console.log(dynamicKeyObject); // => { key1: "value1" }
Exercise D Open the
pokedex
React application again and open thesrc/CaughtPokemon.js
file. In this exercise, instead of recording the number of caught Pokemons, we are going to record the names of each Pokemon you caught.
- Make sure the
CaughtPokemon
component is written as a class component- Add an
<input>
in therender
method before thebutton
(hint:<input type="text" />
)- Add a
value
property to the<input>
set to the statepokemonNameInput
- Initialize the state
pokemonNameInput
in the constructor to an empty string''
(you can try to set something else than an empty string and verify that this value is correctly displayed in your input)- Create a new
handleInputChange
method- Add a
onChange
handler to the<input>
that will callthis.handleInputChange
- Add a parameter called
event
to thehandleInputChange
method and add aconsole.log
withevent.target.value
. In your browser, try writting something in the<input>
. What do you see in the JavaScript console?- Use
setState
inhandleInputChange
to recordevent.target.value
in the statepokemonNameInput
. In your browser, try writting something in the<input>
. What do you see this time in the JavaScript console?- We are now going to save the user input when clicking on the
<button>
. InitializecaughtPokemon
to an empty array[]
instead of 0 in theconstructor
. In therender
, use.length
to display the number of items in the state arraycaughtPokemon
(hint: it should still display0
on the screen). Finally, delete the content ofcatchPokemon
method (it should be empty, we will rewrite it later).- In
catchPokemon
method, create a variablenewCaughtPokemon
set to the statecaughtPokemon
and add the value of the statepokemonNameInput
to it (hint: usepush()
to add a new item in an array).- In
catchPokemon
method, usesetState
to record the variablenewCaughtPokemon
in the statecaughtPokemon
. Open your browser, enter a pokemon name in the<input>
and click on the button. Can you see the number of caught pokemon incrementing as you click on the button?- We are now going to display the names of the caught pokemon. In the
render
method, add a<ul>
element and use the.map()
method on thecaughtPokemon
state to loop over each pokemon and return a<li>
element for each.- Empty the
<input>
after clicking on the button. For this, incatchPokemon
method, set the state ofpokemonNameInput
to an empty string''
. 14: (STRETCH GOAL) Make sure the user cannot add a pokemon to thecaughtPokemon
state if the value ofpokemonNameInput
state is empty.
Further Reading
There is a new update to React, which adds a new feature called Hooks. It allows you to access the special super powers of state and lifecycle in regular function components. There is an extensive guide in the official React tutorial.
Homework
- If you haven't already, complete the in-class exercises on your
pokedex
app - Complete all of the lesson 3 exercises in the cyf-hotel-react project
- Try to complete the Stretch Goal exercises in the cyf-hotel-react homework