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
valueto the state variable - Can explain why the input does not change when typing if
onChangeis not set - Be able to update the state using an
onChangehandler
- Can initialise state with
- Use data from a submitted form to update the application
- Be able to handle an
onSubmitevent 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
Appfunction component is called - It doesn't have any
datestate already, so we initialise it to an empty string ("") withuseState It renders the 2 buttons, but because
dateis an empty string, it does not render theMartianImageFetchercomponent. Insteadnullis 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
handle2019Clickclick handler is called - The state is set by
setDateto be"2019-01-01", and a re-render is triggered - The
Appfunction component is called again This time,
useStateremembers that we havedatestate 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
Appdoes renderMartianImageFetcherand passes thedatestate as a prop (also nameddate) - The
MartianImageFetcherfunction component is called for the first time useStateknows that we haven't got anyimgSrcstate so initialises it tonull- We queue an effect, which will run after we render for the first time
Because the
imgSrcstate 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
fetchrequest is made to the NASA API (🚀!) When the request data comes back, we set the
imgSrcstate 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
MartianImageFetcherfunction component is called again useStateremembers that theimgSrcstate is set to the data from the API- This time, we do not queue an effect. We used an empty array (
[]) as theuseEffectdependencies argument which means that we only queue effects on the first render We do have
imgSrcstate 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
Appcomponent, thehandle2020Clickclick handler is called - The
datestate is set to"2020-01-01"and a re-render is triggered - The
Appfunction component is called again and thedatestate is set to"2020-01-01" The
dateprop that is passed toMartianImageFetcheris 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
MartianImageFetchercomponentuseStateremembers that we already hadimgSrcstate. It is set to the image from 2019 - Again, we do not queue the effect because this is a re-render and
useEffecthas been passed an empty array[] Because
imgSrcstate 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
pokedexReact application again and open thesrc/CaughtPokemon.jsfile. 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
CaughtPokemoncomponent is written as a class component- Add an
<input>in therendermethod before thebutton(hint:<input type="text" />)- Add a
valueproperty to the<input>set to the statepokemonNameInput- Initialize the state
pokemonNameInputin 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
handleInputChangemethod- Add a
onChangehandler to the<input>that will callthis.handleInputChange- Add a parameter called
eventto thehandleInputChangemethod and add aconsole.logwithevent.target.value. In your browser, try writting something in the<input>. What do you see in the JavaScript console?- Use
setStateinhandleInputChangeto recordevent.target.valuein 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>. InitializecaughtPokemonto an empty array[]instead of 0 in theconstructor. In therender, use.lengthto display the number of items in the state arraycaughtPokemon(hint: it should still display0on the screen). Finally, delete the content ofcatchPokemonmethod (it should be empty, we will rewrite it later).- In
catchPokemonmethod, create a variablenewCaughtPokemonset to the statecaughtPokemonand add the value of the statepokemonNameInputto it (hint: usepush()to add a new item in an array).- In
catchPokemonmethod, usesetStateto record the variablenewCaughtPokemonin 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
rendermethod, add a<ul>element and use the.map()method on thecaughtPokemonstate to loop over each pokemon and return a<li>element for each.- Empty the
<input>after clicking on the button. For this, incatchPokemonmethod, set the state ofpokemonNameInputto an empty string''. 14: (STRETCH GOAL) Make sure the user cannot add a pokemon to thecaughtPokemonstate if the value ofpokemonNameInputstate 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
pokedexapp - 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