"Shawn, the PERF add-on can also tell us how much time was wasted by React and where. It is helpful in determining the parts of our app that we can optimize further."
said Mike.
"What is wasted time?"
"When React re-renders a component tree, some components may not change from their previous representation. However, if they are rendered again, then React has wasted time in rendering the same output. The PERF add-on can keep track of all such time and give us a summary of how React wasted time rendering the same output as before. Let's see this in action." said Mike.
"The PERF add-on tells us that it wasted time in re-rendering the Form component twice, but nothing was changed in the Form component, therefore, it just re-rendered everything as it is." explained Mike.
"Let's see the Form component to understand why it is happening."
// src/Form.js
import React from 'react';
export default React.createClass({
getInitialState() {
return { searchTerm: '' };
},
_submitForm() {
this.props.performSearch(this.state.searchTerm);
},
onChange={(event) => { this.setState({searchTerm:
event.target.value}) }}/>
<span className="input-group-btn">
<button className="btn btn-primary btn-lg"
type="button"
"Shawn, the Form component does not depend on state or props for its rendering.
It renders the same output irrespective of state and props. However, we update its state when a user enters a character in the input box. Due to this, React will re-render it. Nothing is actually changed in the re-rendered output. Therefore, the PERF add-on is complaining about the wasted time." Mike explained.
"This is useful information, but this looks like an insignificant wastage, right?"
Shawn asked.
"Agree. Let's make some changes so that I can show you how React can literally waste a lot of time re-rendering the same output when it shouldn't." said Mike.
"Currently, we only show the first 100 search results returned by the Open Library API. Let's change our code to show all the results on same page."
// src/App.js
"I have introduced a new state to hold the search term, total number of pages to fetch from the Open Library, and current page number being fetched."
"Now, we want to fetch all the results from the API, by default, on the same page.
The API returns us the total number of books found for a query in the numFounds attribute. Based on this, we need to find the total number of pages that we need to fetch from the API."
"Also, each time maximum 100 records are returned that we have stored in state.
offset already."
totalPages = response.numFound / this.state.offset + 1;
"Once we get the total number of pages, we need to keep asking for the search results for the next page until all the pages are fetched. You want to try and get this working?" asked Mike.
"Sure." said Shawn.
// src/App.js
// Called when user hits "Go" button.
_performSearch(searchTerm) {
this.setState({searching: true, searchTerm: searchTerm});
this._searchOpenLibrary(searchTerm);
},
_searchOpenLibrary(searchTerm) {
let openlibraryURI = `https://openlibrary.org/search.json?q=${sear chTerm}&page=${this.state.page}`;
this._fetchData(openlibraryURI).then(this._updateState);
},
// called with the response received from open library API _updateState(response) {
let jsonResponse = response;
let newBooks = this.state.books.concat(jsonResponse.docs);
let totalPages = jsonResponse.numFound / this.state.offset + 1;
let nextPage = this.state.page + 1;
_searchAgain() {
if (this.state.page > this.state.totalPages) { this.setState({searching: false});
} else {
this._searchOpenLibrary(this.state.searchTerm);
} }
"I changed the API URL to include the page parameter. Each time the response is received from API, we update the state with a new page. We also update this.
state.books to include the newly fetched books. Then, the _searchAgain function gets called in the callback of the this.setState call so that it is the correct value of the next page that was set by the this.setState call." explains Shawn.
"Nice, it's an important point to remember not to call the _searchAgain function outside of the this.setState() call as it may get executed before setState() is finished.
Because if we call it outside, the _searchAgain function may use an incorrect value of this.state.page. However, as you have passed the _searchAgain function in the callback to setState, there is no chance this will happen." said Mike.
"The _searchAgain function just keeps fetching the results until all the pages are completed. In this way, we will display all the search results on the page, not just the first 100." informed Shawn.
"That's what I wanted. Good job. Let me cleanup the render method so that spinner will always be displayed at the bottom." said Mike.
// src/App.js render() {
let style = {paddingTop: '5%'};
return (
<div className='container'>
<Header style={style}></Header>
<Form style={style}
performSearch={this._performSearch}>
</Form>
{this.state.totalBooks > 0 ?
<BookList
searchCount={this.state.totalBooks}
_sortByTitle={this._sortByTitle}>
{this._renderBooks()}
</BookList>
: null }
{ this.state.searching ? <Spinner /> : null }
</div>
);
}
"This will make sure that the spinner will be displayed until all the results are displayed. OK, all done. Now let's measure the performance again." said Mike.
"Wow, the wasted time has increased a lot! Did Dan Brown release new books or what? So much extra time than what we saw last time?" said Shawn.
"Haha, I don't think that he released new books just now. Whenever the books on next page are fetched, we add them to the existing books and start fetching books from the next page. However, the books rendered for the previous pages are not changed at all. As we are keeping all of our state in the top-level App component, whenever its state changes, the whole component tree under App gets re-rendered.
Therefore, BookList is rendered again. In turn, all BookRows are rendered again.
This results in a significant amount of time getting wasted in rendering the same BookRow components for the previous pages again and again." said Mike.
"So each time we fetch books from a new page, all books, including the existing ones already present on page, get re-rendered again? I think just appending new book rows to the existing list is better in this case." said Shawn.
"Don't worry. We can easily get rid of this unnecessary wastage of time.
React provides us a hook for short-circuiting the re-render process. It's shouldComponentUpdate."