Today we will look a React life cycle methods and using a public API.
Create the app
npx create-react-app pokedex
Change into the app folder
cd pokedex
Install dependencies
npm install
Run the app
npm start
See a spinning logo?
Back at terminal lets open Visual Studio Code
code .
Remove parts of the code we don't need:
/src/App.js
import React from 'react';
import './App.css';
function App() {
return (
<div>
</div>
);
}
export default App;
Convert this to make App
a class component:
/src/App.js
import React, { Component } from 'react';
import './App.css';
class App extends Component {
render() {
return (
<div>
</div>
);
}
}
export default App;
All life cycle methods are called automatically by React. render
is a life cycle method.
If we don't implement our own version React will run the default behavior.
Mount is when the JSX has been converted into HTML and has been inserted into the DOM.
Another commonly used life cycle method is componentDidMount
. componentDidMount
is called after constructor (if there is any) and after first render
. This method is only called once, when the component is first mounted to the DOM.
Add the following code and watch the console to see which will run first.
/src/App.js
class App extends Component {
componentDidMount() {
console.log('App is running componentDidMount');
}
render() {
console.log('App is running render');
return (
<div>
Hello
</div>
);
}
}
When would you expect the componentDidUpdate
method to be called?
/src/App.js
class App extends Component {
componentDidMount() {
console.log('App is running componentDidMount');
}
componentDidUpdate() {
console.log('App is running componentDidUpdate');
}
render() {
console.log('App is running render');
return (
<div>
Hello
</div>
);
}
}
componentDidUpdate
is called immediately after updating occurs. It is not called from the initial render. If we change anything componentDidUpdate
will be called.
When would you expect componentWillUnmount
to be called?
/src/App.js
class App extends Component {
componentDidMount() {
console.log('App is running componentDidMount');
}
componentDidUpdate() {
console.log('App is running componentDidUpdate');
}
componentWillUnmount() {
console.log('App is running componentWillUnmount');
}
render() {
console.log('App is running render');
return (
<div>
Hello
</div>
);
}
}
The opposite of componentWillMount
, componentWillUnmount
is called as the component is removed from the DOM. This can be useful for clean-up code eg: clear interval timers, unresolved promised, cancel anything pending.
It is not being triggered in this example but in a situation where a component is conditionally displayed will trigger it.
For example:
{checkPassed ? <Greeting /> : <Notification message='Error' />}
When would you expect shouldComponentUpdate
to be called?
/src/App.js
shouldComponentUpdate(newProps, newState) {
console.log('App is running shouldComponentUpdate');
return true;
}
By default shouldComponentUpdate
will return true
. We can implement our own implementation that will check if this.state
is equivalent to newState
or this.props
is equivalent to newProps
.
http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
What would be the best life cycle method for us to make our API call?
/src/App.js
componentDidMount() {
console.log('App is running componentDidMount');
const url = 'https://pokeapi.co/api/v2/pokemon';
fetch(url)
.then(resp => resp.json())
.then(json => {
console.log(json);
})
}
The results includes invalid pokemon. So we will splice
the array to get the one we want.
/src/App.js
class App extends Component {
componentDidMount() {
console.log('App is running componentDidMount');
const url = 'https://pokeapi.co/api/v2/pokemon?offset=0&limit=151';
fetch(url)
.then(resp => resp.json())
.then(json => {
const pokemon = json.results
console.log(pokemon);
})
}
/src/App.js
class App extends Component {
state = {
pokemon: []
}
componentDidMount() {
console.log('App is running componentDidMount');
const url = 'https://pokeapi.co/api/v2/pokemon?offset=0&limit=151';
fetch(url)
.then(resp => resp.json())
.then(json => {
const pokemon = json.results;
this.setState({ pokemon: pokemon });
})
}
componentDidUpdate() {
console.log('App is running componentDidUpdate');
}
componentWillUnmount() {
console.log('App is running componentWillUnmount');
}
shouldComponentUpdate(newProps, newState) {
console.log('App is running shouldComponentUpdate');
return true;
}
render() {
console.log(`App is running render ${this.state.pokemon.length}`);
return (
<div>
We have {this.state.pokemon.length} pokemon
</div>
);
}
}
Console output shows the first time the component was rendered the pokemon were not loaded.
Navigated to http://localhost:3000/
App.js:35 App is running render 0
App.js:9 App is running componentDidMount
App.js:29 App is running shouldComponentUpdate
App.js:35 App is running render 151
App.js:20 App is running componentDidUpdate
We can add extra console logs to shouldComponentUpdate
to see what is happening.
/src/App.js
shouldComponentUpdate(newProps, newState) {
console.log('App is running shouldComponentUpdate');
console.log(newProps)
console.log(newState)
return true;
}
The data we get back from the pokemon API is being saved in the state
. We can access this data in the render
method to display each of the pokemon.
/src/App.js
render() {
console.log(`App is running render ${this.state.pokemon.length}`);
return (
<div>
We have {this.state.pokemon.length} pokemon
{ this.state.pokemon.map(poke => <h2>{poke.name}</h2>) }
</div>
);
}
This gives us a warning.
index.js:1375 Warning: Each child in a list should have a unique "key" prop.
We can fix this by using the index
on the map
.
Could we have used something else for the key
?
/src/App.js
render() {
console.log(`App is running render ${this.state.pokemon.length}`);
return (
<div>
We have {this.state.pokemon.length} pokemon
{ this.state.pokemon.map((poke, index) => <h2 key={index}>{poke.name}</h2>) }
</div>
);
}
/src/App.js
render() {
console.log(`App is running render ${this.state.pokemon.length}`);
return (
<div>
We have {this.state.pokemon.length} pokemon
<PokeInfo />
{ this.state.pokemon.map((poke, index) => <h2 key={index}>{poke.name}</h2>) }
</div>
);
}
At the top of the file after the React
import add the import for our new component:
/src/App.js
import PokeInfo from './components/PokeInfo';
Create a minimal component to stop the errors from displaying:
/src/components/PokeInfo.js
import React, { Component} from 'react';
class PokeInfo extends Component {
render() {
return (
<div>
<h3>More Information</h3>
</div>
);
}
}
export default PokeInfo;
In our App component we will add an onClick
event to our pokemon h2
heading. The onClick
is epecting a reference to a method we want to run. We will add it as an anonymous function which is going to be called when we click. This means we can pass a value to handlePokemonInfo
.
In handlePokemonInfo
we will console log the poke
to confirm we have the correct value.
/src/App.js
handlePokemonInfo(poke) {
console.log(poke)
}
render() {
console.log(`App is running render ${this.state.pokemon.length}`);
return (
<div>
We have {this.state.pokemon.length} pokemon
<PokeInfo />
{ this.state.pokemon.map((poke, index) => <h2 key={index} onClick={() => this.handlePokemonInfo(poke)}>{poke.name}</h2>) }
</div>
);
}
Now we need to make the App
component kn ow ab out which pokemon was selected.
We will add a new field to the state
to keep track of the select pokemon:
/src/App.js
state = {
pokemon: [],
selected: null
}
We will update the code in the handlePokemonInfo
to set the selected pokemon the state
when it is clicked.
/src/App.js
handlePokemonInfo(poke) {
this.setState({ selected: poke })
}
We can see all the life cycle methods that are being called each time we are updating the state.
App is running shouldComponentUpdate
App.js:33 {}
App.js:34 {pokemon: Array(151), selected: {…}}
App.js:43 App is running render 151
App.js:22 App is running componentDidUpdate
Now we have the selected pokemon in the state
we want to pass it down to the PokeInfo
component via props
.
/src/App.js
render() {
console.log(`App is running render ${this.state.pokemon.length}`);
return (
<div>
We have {this.state.pokemon.length} pokemon
<PokeInfo pokemon={this.state.selected} />
{ this.state.pokemon.map((poke, index) => <h2 key={index} onClick={() => this.handlePokemonInfo(poke)}>{poke.name}</h2>) }
</div>
);
}
We want to load the pokemon details in our PokeInfo
component. Last time we used the life cycle method componentDidMount
.
Let's add some console log's to our PokeInfo
component.
/src/components/PokeInfo.js
class PokeInfo extends Component {
componentDidMount() {
console.log('POKEINFO DID MOUNT')
}
render() {
console.log(this.props)
return (
<div>
<h3>More Information</h3>
</div>
);
}
}
In render
we log the props
. We can see that the props are null
at first then change once we click on a pokemon heading.
We use CAPS for the console log message in componentDidMount
. We can see that it gets called once. Not every time the props
are updated.
Let's try componentDidUpdate
/src/components/PokeInfo.js
componentDidUpdate() {
console.log('POKEINFO DID UPDATE')
}
This method is being called every time we click on a pokemon heading.
We will add thew fetch
for details to this method. We can look at the props
we have. We have a url
that we will need to use to call for more details.
/src/components/PokeInfo.js
componentDidUpdate() {
console.log('POKEINFO DID UPDATE')
const { url } = this.props.pokemon;
fetch(url)
.then(resp => resp.json())
.then(json => {
console.log(json);
})
}
We get an error:
TypeError: Cannot read property 'url' of null
We shouldn't update is we don't have the details. We can check to see if this.props.pokemon
exists before trying to access the url
.
/src/components/PokeInfo.js
componentDidUpdate() {
console.log('POKEINFO DID UPDATE')
if (this.props.pokemon) {
const { url } = this.props.pokemon;
fetch(url)
.then(resp => resp.json())
.then(json => {
console.log(json);
})
}
}
Great. The error has been fixed.
Looking at the console log we see that the json returned contains the poke we want to render. So we will set the state
with that value.
/src/components/PokeInfo.js
componentDidUpdate() {
console.log('POKEINFO DID UPDATE')
if (this.props.pokemon) {
const { url } = this.props.pokemon;
fetch(url)
.then(resp => resp.json())
.then(json => {
this.setState({ poke: json })
})
}
}
We will need to set the initial state
of the poke object to be an empty object.
/src/components/PokeInfo.js
state = {
poke: null
}
Now we have the values, let's render them. Checking the console log we can see the values returned in the poke
. We can use object destructuring to access them in the render method.
/src/components/PokeInfo.js
render() {
const { id, name, height, weight } = this.state.poke;
return (
<div>
<h3>More Information</h3>
<h4>{id}: {name}</h4>
<p>Height: {height} | Weight: {weight} </p>
</div>
);
}
We get null
object errors. Lets check to see if we have anything to display first. This will prevent the null object
errors and stop the 'More Information' heading being displayed too early.
/src/components/PokeInfo.js
render() {
if (this.state.poke) {
const { id, name, height, weight } = this.state.poke;
return (
<div>
<h3>More Information</h3>
<h4>{id}: {name}</h4>
<p>Height: {height} | Weight: {weight} </p>
</div>
);
}
return null
}
Looking at console log we see that there are a lot of updates being called. If we wait long enough the stack will over flow.
Lets add some extra checks and diagnostics to work out why this is happening.
/src/components/PokeInfo.js
loadPokemon = () => {
console.log('loading data')
const { url } = this.props.pokemon;
fetch(url)
.then(resp => resp.json())
.then(json => {
this.setState({ poke: json })
})
}
componentDidUpdate(prevProps) {
console.log('POKEINFO DID UPDATE')
console.log(this.props.pokemon)
console.log(prevProps.pokemon)
const checkPrevPropsExist = (prevProps != null && prevProps.pokemon != null);
const checkCurrentPropsExist = (this.props.pokemon != null)
console.log(`checkPrevPropsExist ${checkPrevPropsExist}`)
console.log(`checkCurrentPropsExist ${checkCurrentPropsExist}`)
if (checkCurrentPropsExist &&
checkPrevPropsExist) {
const checkSelectedPokeDifferent = (prevProps.pokemon.name !== this.props.pokemon.name);
console.log(`prevProps.pokemon.id ${prevProps.pokemon.name}`)
console.log(`this.props.pokemon.id ${this.props.pokemon.name}`)
console.log(`checkSelectedPokeDifferent ${checkSelectedPokeDifferent}`)
if ( checkSelectedPokeDifferent) {
this.loadPokemon();
}
}
}
The constant updates are no longer happening but the first click is not updating properly.
Completely resolving this issue is left as a challenge.
Display extra details for the pokemon in the PokeInfo
component. Display:
- forms
- moves
- species
- stats
Use github API to display your projects:
API docs:
https://developer.github.com/v3/users/
Example:
https://api.github.com/users/leahgarrett/repos
https://reactjs.org/docs/state-and-lifecycle.html https://reactjs.org/docs/faq-state.html