Project Notes by James Priest
This site contains code notes for project 1 of my Udacity React Nanodegree project. Click the link below for more information on the course.
The first thing I did was start with a fresh Create React App instance.
npx create-react-app reactnd-project-myreads
cd reactnd-project-myreads
npm start
The last command tests that the app installed properly.
This step consisted of copying the starter site files to the repo.
This includes:
The Jekyll static site generator allows me to document the steps I take to build this project.
Jekyll is what GitHub uses to generate GitHub Pages. Creating a local copy allows previews of these docs locally before pushing to GitHub.
The My Reads App allows you to track books and place them on one of three bookshelves.
Live Demo: reactnd-project-myreads@1-starter-files on CodeSandbox
Each shelf corresponds to one of the following
The app also lets you search for books and add them to one of the three category shelves.
Lastly the app allows you to move books between shelves.
Below a link to the roadmap I follow for taking a mock-up or static site and building a full fledged React application from it.
That page is part of the Twelve Main Concepts of React outlined by Dan Abramov, one of the dev evangelists of the UI library.
Additionally, I’ve found the easiest way to work on small projects is to keep all the components on one page to start. This way I don’t have to worry about the plumbing of connecting multiple pages.
I also tend to work from the top down which is what is recommended for smaller projects.
Once I get to the lowest component in a hierarchy I then start to split the components into their own files.
The first step was to look at the UI and determine each of the logical areas.
I then drew boxes around each of these areas and broke it down according to function.
This became my hierarchy of components. The components are split between two pages - a List page and a Search page.
Here’s the nested representation.
I installed the React Router package.
npm install --save react-router-dom
Next I imported the BrowserRouter component into index.js and wrapped <App />
with it.
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './index.css';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
Next I imported Route and Link components into App.js.
// App.js
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
This is the part where we move away from using state to determine which component to display and instead use React Router.
The old code used something like this.
// App.js
class BooksApp extends Component {
state = {
showSearchPage: false,
};
render() {
return (
<div className="app">
{this.state.showSearchPage ? (
// <div className="search-books">
// button onClick={() => this.setState({ showSearchPage: false })
) : (
// <div className="list-books">
// button onClick={() => this.setState({ showSearchPage: true })}
)}
</div>
)
}
}
The updated code now uses Route and Link. I also split out the code into it’s own List and Search components.
// App.js
class BooksApp extends Component {
state = {};
render() {
return (
<div className="app">
<Route exact path="/" component={BookList} />
<Route path="/search" component={BookSearch} />
</div>
)
}
}
class BookList extends Component {
render() {
return (
<div className="list-books">
{/* UI code */}
</div>
)
}
}
class BookSearch extends Component {
render() {
return (
<div className="search-books">
{/* UI code */}
</div>
)
}
}
Live Demo: reactnd-project-myreads@2-routing on CodeSandbox
The React docs recommend that once the UI/component hierarchy is determined you should build out a static version in React.
This version should take the data model and render the UI without any interactivity yet - meaning we should split out each component in its hierarchy and pass data using props but not implement any state since state is reserved for interactivity.
This image shows roughly how were going to organize the code. Currently all code is in App.js and will remain in one file while I split up the UI.
Live Demo: reactnd-project-myreads@3-split-components on CodeSandbox
Here’s how I split up the code starting with the home page.
// App.js
class BooksApp extends Component {
render() {
return (
<div className="app">
<Route exact path="/" component={BookList} />
<Route path="/search" component={BookSearch} />
</div>
);
}
}
class ListBooks extends Component {
render() {
const { bookshelves } = this.props;
return (
<div className="list-books">
<div className="list-books-title">
<h1>MyReads</h1>
</div>
<Bookcase bookshelves={bookshelves} />
<OpenSearchButton />
</div>
);
}
}
const OpenSearchButton = () => {
return (
<div className="open-search">
<Link to="search">
<button>Add a Book</button>
</Link>
</div>
);
};
const Bookcase = props => {
const { bookshelves } = props;
return (
<div className="list-books-content">
<div>
{bookshelves.map(shelf => (
<Bookshelf key={shelf.key} shelf={shelf} />
))}
</div>
</div>
);
};
const Bookshelf = props => {
const { shelf } = props;
return (
<div className="bookshelf">
<h2 className="bookshelf-title">{shelf.name}</h2>
<div className="bookshelf-books">
<ol className="books-grid">
<Book book={{}} />
</ol>
</div>
</div>
);
};
const Book = props => {
const { book } = props;
return (
<li>
<div className="book">
<div className="book-top">
<div
className="book-cover"
style={{
width: 128,
height: 193,
backgroundImage:
'url("http://books.google.com/books/content?id=PGR2Aw...")',
}}
/>
<BookshelfChanger />
</div>
<div className="book-title">To Kill a Mockingbird</div>
<div className="book-authors">Harper Lee</div>
</div>
</li>
);
};
class BookshelfChanger extends Component {
render() {
return (
<div className="book-shelf-changer">
<select>
<option value="move" disabled>
Move to...
</option>
<option value="currentlyReading">Currently Reading</option>
<option value="wantToRead">Want to Read</option>
<option value="read">Read</option>
<option value="none">None</option>
</select>
</div>
);
}
}
Below is a picture of roughly how were going to break out the Search page components.
Live Demo: reactnd-project-myreads@3-split-components on CodeSandbox
I separated the code into a couple more components than the image shows.
// App.js
class BookSearch extends Component {
render() {
return (
<div className="search-books">
<SearchBar />
<SearchResults />
</div>
);
}
}
const SearchBar = props => {
return (
<div className="search-books-bar">
<CloseSearchButton />
<SearchBooksInput />
</div>
);
};
const CloseSearchButton = () => {
return (
<Link to="/">
<button className="close-search">Close</button>
</Link>
);
};
class SearchBooksInput extends Component {
render() {
return (
<div className="search-books-input-wrapper">
<input type="text" placeholder="Search by title or author" />
</div>
);
}
}
const SearchResults = props => {
return (
<div className="search-books-results">
<ol className="books-grid">
<Book />
</ol>
</div>
);
};
As I started to breakdown the UI into separate components it began to differ a bit from what I outlined in the previous section. This is because I got a better clarity of how the components should be nested as I was splitting up the code.
So far we have this hierarchy.
The next step will be to bring the data in and start to pass that down as props.
The next thing I did was to bring the data into the app as static data which I assigned to state. This is so I can start passing props to child components.
I first started by doing an ‘getAll’ Ajax request using Postman.
This returned the data that defines which books show up on each shelf within the MyReads app. It returned the data in this format.
{
"books": [
{
"title": "Learning Web Development with React and Bootstrap",
"authors": [
"Harmeet Singh",
"Mehul Bhatt"
],
"publishedDate": "2016-12-30",
"description": "Build real-time responsive web apps using React and...",
"industryIdentifiers": [
{
"type": "ISBN_10",
"identifier": "1786462494"
},
{
"type": "ISBN_13",
"identifier": "9781786462497"
}
],
"readingModes": {
"text": false,
"image": false
},
"pageCount": 278,
"printType": "BOOK",
"maturityRating": "NOT_MATURE",
"allowAnonLogging": false,
"contentVersion": "preview-1.0.0",
"panelizationSummary": {
"containsEpubBubbles": false,
"containsImageBubbles": false
},
"imageLinks": {
"smallThumbnail": "http://books.google.com/books/content?",
"thumbnail": "http://books.google.com/books/content?"
},
"language": "en",
"previewLink": "http://books.google.com/books?id=sJf1vQAACAAJ&dq=redux",
"infoLink": "http://books.google.com/books?id=sJf1vQAACAAJ&dq=redux",
"canonicalVolumeLink": "https://books.google.com/books/about/Learning",
"id": "sJf1vQAACAAJ",
"shelf": "currentlyReading"
},
{
"title": "another book"
}
]
}
I then created data.js to hold the books data and export a getAll
method.
// data.js
const getAll = [
{
title: 'The Linux Command Line',
subtitle: 'A Complete Introduction',
// more data...
},
{
title: 'The React Handbook',
subtitle: 'Learning to Build Dynamic UIs',
// more data...
},
// next book...
];
export default getAll;
I imported data.js into App.js and assigned it to the state
property for our BooksApp component. At the same time I created a bookshelves
constant to use with my Bookcase component.
// App.js
// other import statements...
import getAll from './data';
class BooksApp extends Component {
bookshelves = [
{ key: 'currentlyReading', name: 'Currently Reading' },
{ key: 'wantToRead', name: 'Want to Read' },
{ key: 'read', name: 'Have Read' },
];
state = {
books: getAll,
};
Now all of our books are available as a books
property of our BooksApp state.
Now is the time to use props to pass the data down the hierarchy.
Here are the changes for the List Books page.
// App.js
class BooksApp extends Component
bookshelves = [
{ key: 'currentlyReading', name: 'Currently Reading' },
{ key: 'wantToRead', name: 'Want to Read' },
{ key: 'read', name: 'Have Read' },
];
state = {
books: getAll,
};
render() {
const { books } = this.state;
return (
<div className="app">
<Route
exact
path="/"
render={() => (
<ListBooks bookshelves={this.bookshelves} books={books} />
)}
/>
<Route path="/search" render={() => <SearchBooks books={books} />} />
</div>
);
}
}
class ListBooks extends Component {
render() {
const { bookshelves, books } = this.props;
return (
<div className="list-books">
<div className="list-books-title">
<h1>MyReads</h1>
</div>
<Bookcase bookshelves={bookshelves} books={books} />
<OpenSearchButton />
</div>
);
}
}
const Bookcase = props => {
const { bookshelves, books } = props;
return (
<div className="list-books-content">
<div>
{bookshelves.map(shelf => (
<Bookshelf key={shelf.key} shelf={shelf} books={books} />
))}
</div>
</div>
);
};
const Bookshelf = props => {
const { shelf, books } = props;
const booksOnThisShelf = books.filter(book => book.shelf === shelf.key);
return (
<div className="bookshelf">
<h2 className="bookshelf-title">{shelf.name}</h2>
<div className="bookshelf-books">
<ol className="books-grid">
{booksOnThisShelf.map(book => (
<Book key={book.id} book={book} shelf={shelf.key} />
))}
</ol>
</div>
</div>
);
};
const Book = props => {
const { book, shelf } = props;
return (
<li>
<div className="book">
<div className="book-top">
<div
className="book-cover"
style={{
width: 128,
height: 193,
backgroundImage: `url(${book.imageLinks.thumbnail})`,
}}
/>
<BookshelfChanger book={book} shelf={shelf} />
</div>
<div className="book-title">{book.title}</div>
<div className="book-authors">{book.authors.join(', ')}</div>
</div>
</li>
);
};
class BookshelfChanger extends Component {
render() {
return (
<div className="book-shelf-changer">
<select value={this.props.shelf}>
<option value="move" disabled>
Move to...
</option>
<option value="currentlyReading">Currently Reading</option>
<option value="wantToRead">Want to Read</option>
<option value="read">Read</option>
<option value="none">None</option>
</select>
</div>
);
}
}
This now has our books showing up on the appropriate bookshelves.
Live Demo: reactnd-project-myreads@4-add-props on CodeSandbox
Here are the components for the Search Books page
// App.js
class SearchBooks extends Component
render() {
const { books } = this.props;
return (
<div className="search-books">
<SearchBar />
<SearchResults books={books} />
</div>
);
}
}
const SearchResults = props => {
const { books } = props;
return (
<div className="search-books-results">
<ol className="books-grid">
{books.map(book => (
<Book key={book.id} book={book} shelf="none" />
))}
</ol>
</div>
);
};
Live Demo: reactnd-project-myreads@4-add-props on CodeSandbox
Now we create a moveBook
method which is in charge of moving a book from one bookshelf to another.
It lives at the top of the component stack in BooksApp so that it can access state which we also lifted to live here.
// App.js
import getAll from './data';
class BooksApp extends Component {
bookshelves = [
{ key: 'currentlyReading', name: 'Currently Reading' },
{ key: 'wantToRead', name: 'Want to Read' },
{ key: 'read', name: 'Read' },
];
state = {
books: getAll
};
moveBook = (book, shelf) => {
const updatedBooks = this.state.books.map(b => {
if (b.id === book.id) {
b.shelf = shelf;
}
return b;
});
this.setState({
books: updatedBooks,
});
};
}
render() {
const { books } = this.state;
return (
<div className="app">
<Route
exact
path="/"
render={() => (
<ListBooks
bookshelves={this.bookshelves}
books={books}
onMove={this.moveBook}
/>
)}
/>
<Route
path="/search"
render={() => (
<SearchBooks
books={searchBooks}
onMove={this.moveBook}
/>
)}
/>
</div>
);
}
}
Next the method needs to be passed down to the BookshelfChanger component. This means it has to go through:
Here is the ListBooks component showing how this.props
is destructured and then passed to the next nested component.
// App.js
class ListBooks extends Component {
render() {
const { bookshelves, books, onMove } = this.props;
return (
<div className="list-books">
<div className="list-books-title">
<h1>MyReads</h1>
</div>
<Bookcase bookshelves={bookshelves} books={books} onMove={onMove} />
<OpenSearchButton />
</div>
);
}
}
This same pattern is followed all the way down to BookshelfChanger. Once there, onMove
is invoked in the handleChange
method.
The pattern we see here is that of a Controlled Component where React is used to manage the element’s state making React the “single source of truth”.
We do this by creating a state property to manage the select element’s value. We then assign it to the select element’s value
attribute. Lastly, we create an onChange
method to update state as the value changes.
// App.js
class BookshelfChanger extends Component {
state = {
value: this.props.shelf,
};
handleChange = event => {
this.setState({ value: event.target.value });
this.props.onMove(this.props.book, event.target.value);
};
render() {
return (
<div className="book-shelf-changer">
<select value={this.state.value} onChange={this.handleChange}>
<option value="move" disabled>
Move to...
</option>
<option value="currentlyReading">Currently Reading</option>
<option value="wantToRead">Want to Read</option>
<option value="read">Read</option>
<option value="none">None</option>
</select>
</div>
);
}
}
It’s in the handleChange method that we invoke the onMove method, passing it the bookId
and shelf
the book is being moved to.
One thing to note is that we are using props to set state. Normally this is an anti-pattern because it leads to the data living in two separate places.
In this case we are using props to set an initial value and don’t need any changes in props to stay in sync with state.
Here’s an excerpt from the old React docs on this.
However, it’s not an anti-pattern if you make it clear that the prop is only seed data for the component’s internally-controlled state
Here’s the UI with books that are moved from “Currently Reading” to “Want to Read”.
Live Demo: reactnd-project-myreads@5-add-books-state on CodeSandbox
The next thing I did was to remove the reference to hard-coded data and add an Ajax request to dynamically pull down the books.
This is done in the componentDidMount method of BooksApp.
// App.js
// more import...
import * as BooksAPI from './BooksAPI';
class BooksApp extends Component {
bookshelves = [
{ key: 'currentlyReading', name: 'Currently Reading' },
{ key: 'wantToRead', name: 'Want to Read' },
{ key: 'read', name: 'Read' },
];
state = {
books: []
};
componentDidMount = () => {
BooksAPI.getAll().then(books => {
this.setState({ books: books });
});
};
render() {
// render code...
}
}
Now we need to invoke the Ajax requests that updates the database whenever we move a book to a new shelf.
// App.js
class BooksApp extends Component {
// code...
moveBook = (book, shelf) => {
BooksAPI.update(book, shelf).then(books => {
console.log(books);
});
const updatedBooks = this.state.books.map(b => {
if (b.id === book.id) {
b.shelf = shelf;
}
return b;
});
this.setState({
books: updatedBooks,
});
};
// more code...
We test that the data is being updated by capturing the response and logging it out to the console.
This shows the book.id by shelf.
Live Demo: reactnd-project-myreads@6-add-books-ajax on CodeSandbox
The Search page is responsible for a few things which are triggered by the Search input. Here are the requirements:
The first thing to do is create a new state property called searchBooks
which will contain the search results. We’ll do this at the top of the hierarchy in BooksApp.
We also create a searchForBooks
method and a resetSearch
method. These will be invoked from the SearchBooksInput and CloseSearchButton components.
// App.js
class BooksApp extends Component {
//code...
state = {
books: [],
searchBooks: [],
};
// more code...
searchForBooks = query => {
if (query.length > 0) {
BooksAPI.search(query).then(books => {
if (books.error) {
this.setState({ searchBooks: [] });
} else {
this.setState({ searchBooks: books });
}
});
} else {
this.setState({ searchBooks: [] });
}
};
resetSearch = () => {
this.setState({ searchBooks: [] });
};
What the code does is it invokes the Ajax call if the query length is greater than zero otherwise it clears the data. It then sets the state to an empty array if there is no match (error returned). Otherwise state is set with the results.
Below is where we pass the handlers as props within our Search Route.
// App.js
class BooksApp extends Component {
// code...
render() {
const { books, searchBooks } = this.state;
return (
<div className="app">
{/* ListBooks Route */}
<Route
path="/search"
render={() => (
<SearchBooks
books={searchBooks}
onSearch={this.searchForBooks}
onMove={this.moveBook}
onResetSearch={this.resetSearch}
/>
)}
/>
</div>
);
}
}
Here we show the props being passed through child components on their way to their destination.
// App.js
class SearchBooks extends Component {
render() {
const { books, onSearch, onResetSearch } = this.props;
return (
<div className="search-books">
<SearchBar onSearch={onSearch} onResetSearch={onResetSearch} />
<SearchResults books={books} />
</div>
);
}
}
const SearchBar = props => {
const { onSearch, onResetSearch } = props;
return (
<div className="search-books-bar">
<CloseSearchButton onResetSearch={onResetSearch} />
<SearchBooksInput onSearch={onSearch} />
</div>
);
};
Here we invoke the reset handler when the button inside CloseSearchButton component is clicked.
// App.js
const CloseSearchButton = props => {
const { onResetSearch } = props;
return (
<Link to="/">
<button className="close-search" onClick={onResetSearch}>
Close
</button>
</Link>
);
};
Lastly, we set up the search input as a controlled component. We use onChange to update local state and to trigger the onSearch handler (searchForBooks method defined in BooksApp).
// App.js
class SearchBooksInput extends Component {
state = {
value: '',
};
handleChange = event => {
const val = event.target.value;
this.setState({ value: val }, () => {
this.props.onSearch(val);
});
};
render() {
return (
<div className="search-books-input-wrapper">
<input
type="text"
value={this.state.value}
placeholder="Search by title or author"
onChange={this.handleChange}
autoFocus
/>
</div>
);
}
}
The next thing we need to do implement throttle / debounce so that the number of Ajax calls are limited to a reasonable number.
Throttle limits the number of calls to once within a certain time period and debounce will wait a certain period of time after the last call to invoke the function.
In our case we need debounce which we’ll implement on our searchForBooks method by wrapping the arrow function like this.
First we need to install the library.
npm install throttle-debounce
// App.js
import { debounce } from 'throttle-debounce';
class BooksApp extends Component {
// code...
searchForBooks = debounce(300, false, query => {
console.log(query);
if (query.length > 0) {
BooksAPI.search(query).then(books => {
console.log(books);
if (books.error) {
this.setState({ searchBooks: [] });
} else {
this.setState({ searchBooks: books });
}
});
} else {
this.setState({ searchBooks: [] });
}
});
Next we need to make sure the app handles books which are missing authors or thumbnails.
This can be done with logical && format for backgroundImage and book.authors.
// App.js
const Book = props => {
const { book, shelf, onMove } = props;
return (
<li>
<div className="book">
<div className="book-top">
<div
className="book-cover"
style={{
width: 128,
height: 193,
backgroundImage: `url(${book.imageLinks &&
book.imageLinks.thumbnail})`,
}}
/>
<BookshelfChanger book={book} shelf={shelf} onMove={onMove} />
</div>
<div className="book-title">{book.title}</div>
<div className="book-authors">
{book.authors && book.authors.join(', ')}
</div>
</div>
</li>
);
};
Now the interface returns data as we type and clears the page when we erase the input field. Here’s a screenshot.
Live Demo: reactnd-project-myreads@7-add-search-ajax on CodeSandbox
The next piece involved getting the books from search results to reflect the same category (shelf) as my main page if any of the results contained books I’ve already added.
This required me to refactor some code so I could pass books from my main page along with books from the search request.
Both myBooks
and searchBooks
state needed to be passed to SearchBooks component.
// App.js
class BooksApp extends Component {
state = {
myBooks: [],
searchBooks: [],
};
// code...
render() {
const { myBooks, searchBooks } = this.state;
return (
<div className="app">
<Route
exact
path="/"
render={() => (
<ListBooks
bookshelves={this.bookshelves}
books={myBooks}
onMove={this.moveBook}
/>
)}
/>
<Route
path="/search"
render={() => (
<SearchBooks
searchBooks={searchBooks}
myBooks={myBooks}
onSearch={this.searchForBooks}
onMove={this.moveBook}
onResetSearch={this.resetSearch}
/>
)}
/>
</div>
);
}
}
Next the SearchBooks and SearchResults components are updated to pass and receive both myBooks
and searchBooks
as props.
// App.js
class SearchBooks extends Component {
render() {
const {
searchBooks,
myBooks,
onSearch,
onResetSearch,
onMove,
} = this.props;
return (
<div className="search-books">
<SearchBar onSearch={onSearch} onResetSearch={onResetSearch} />
<SearchResults
searchBooks={searchBooks}
myBooks={myBooks}
onMove={onMove}
/>
</div>
);
}
}
const SearchResults = props => {
const { searchBooks, myBooks, onMove } = props;
return (
<div className="search-books-results">
<ol className="books-grid">
{searchBooks.map(book => (
<Book
key={book.id}
book={book}
shelf={book.shelf ? book.shelf : 'none'}
onMove={onMove}
/>
))}
</ol>
</div>
);
};
Next I needed to have the Search Results reflect the state of any books I’ve already added to my shelves. A book should read “none” if it hasn’t been added.
// App.js
const SearchResults = props => {
const { searchBooks, myBooks, onMove } = props;
const updatedBooks = searchBooks.map(book => {
myBooks.map(b => {
if (b.id === book.id) {
book.shelf = b.shelf;
}
return b;
});
return book;
});
return (
<div className="search-books-results">
<ol className="books-grid">
{updatedBooks.map(book => (
<Book
key={book.id}
book={book}
shelf={book.shelf ? book.shelf : 'none'}
onMove={onMove}
/>
))}
</ol>
</div>
);
};
The code maps over the books in my search results and for each book it then maps over my added books and if there’s a match it sets the shelf property.
Live Demo: reactnd-project-myreads@8-sync-books-and-search on CodeSandbox
The image shows a search result that displays books I’ve already added. It now shows the shelf it currently sits on.
The next step is to update the bookMove method to handle books that have been added from Search and which do not have a shelf property.
// App.js
class BooksApp extends Component {
// code..
moveBook = (book, shelf) => {
// update db
BooksAPI.update(book, shelf);
let updatedBooks = [];
updatedBooks = this.state.myBooks.filter(b => b.id !== book.id);
if (shelf !== 'none') {
book.shelf = shelf;
updatedBooks = updatedBooks.concat(book);
}
this.setState({
myBooks: updatedBooks,
});
};
This code updates the db and filters out the book from myBooks
. Then, if a shelf other than ‘none’ is specified the shelf
property is added to the book and the book is added to our state.
This shows the book displayed on our main pages on the proper shelf.
Live Demo: reactnd-project-myreads@8-sync-books-and-search on CodeSandbox
The next step is to separate each component into its own file to promote easy reuse. Rather than one large App.js file we now have the following.
import React, { Component } from 'react';
import { Route } from 'react-router-dom';
import { debounce } from 'throttle-debounce';
import * as BooksAPI from './BooksAPI';
import './App.css';
import ListBooks from './ListBooks';
import SearchBooks from './SearchBooks';
class BooksApp extends Component {
bookshelves = [
{ key: 'currentlyReading', name: 'Currently Reading' },
{ key: 'wantToRead', name: 'Want to Read' },
{ key: 'read', name: 'Read' },
];
state = {
myBooks: [],
searchBooks: [],
};
componentDidMount = () => {
BooksAPI.getAll().then(books => {
this.setState({ myBooks: books });
});
};
moveBook = (book, shelf) => {
BooksAPI.update(book, shelf);
let updatedBooks = [];
updatedBooks = this.state.myBooks.filter(b => b.id !== book.id);
if (shelf !== 'none') {
book.shelf = shelf;
updatedBooks = updatedBooks.concat(book);
}
this.setState({
myBooks: updatedBooks,
});
};
searchForBooks = debounce(300, false, query => {
if (query.length > 0) {
BooksAPI.search(query).then(books => {
if (books.error) {
this.setState({ searchBooks: [] });
} else {
this.setState({ searchBooks: books });
}
});
} else {
this.setState({ searchBooks: [] });
}
});
resetSearch = () => {
this.setState({ searchBooks: [] });
};
render() {
const { myBooks, searchBooks } = this.state;
return (
<div className="app">
<Route
exact
path="/"
render={() => (
<ListBooks
bookshelves={this.bookshelves}
books={myBooks}
onMove={this.moveBook}
/>
)}
/>
<Route
path="/search"
render={() => (
<SearchBooks
searchBooks={searchBooks}
myBooks={myBooks}
onSearch={this.searchForBooks}
onMove={this.moveBook}
onResetSearch={this.resetSearch}
/>
)}
/>
</div>
);
}
}
export default BooksApp;
import React, { Component } from 'react';
import Bookcase from './Bookcase';
import OpenSearchButton from './OpenSearchButton';
class ListBooks extends Component {
render() {
const { bookshelves, books, onMove } = this.props;
return (
<div className="list-books">
<div className="list-books-title">
<h1>MyReads</h1>
</div>
<Bookcase bookshelves={bookshelves} books={books} onMove={onMove} />
<OpenSearchButton />
</div>
);
}
}
export default ListBooks;
import React from 'react';
import Bookshelf from './Bookshelf';
const Bookcase = props => {
const { bookshelves, books, onMove } = props;
return (
<div className="list-books-content">
<div>
{bookshelves.map(shelf => (
<Bookshelf
key={shelf.key}
shelf={shelf}
books={books}
onMove={onMove}
/>
))}
</div>
</div>
);
};
export default Bookcase;
import React from 'react';
import Book from './Book';
const Bookshelf = props => {
const { shelf, books, onMove } = props;
const booksOnThisShelf = books.filter(book => book.shelf === shelf.key);
return (
<div className="bookshelf">
<h2 className="bookshelf-title">{shelf.name}</h2>
<div className="bookshelf-books">
<ol className="books-grid">
{booksOnThisShelf.map(book => (
<Book key={book.id} book={book} shelf={shelf.key} onMove={onMove} />
))}
</ol>
</div>
</div>
);
};
export default Bookshelf;
import React from 'react';
import BookshelfChanger from './BookshelfChanger';
const Book = props => {
const { book, shelf, onMove } = props;
return (
<li>
<div className="book">
<div className="book-top">
<div
className="book-cover"
style={{
width: 128,
height: 193,
backgroundImage: `url(${book.imageLinks &&
book.imageLinks.thumbnail})`,
}}
/>
<BookshelfChanger book={book} shelf={shelf} onMove={onMove} />
</div>
<div className="book-title">{book.title}</div>
<div className="book-authors">
{book.authors && book.authors.join(', ')}
</div>
</div>
</li>
);
};
export default Book;
import React, { Component } from 'react';
class BookshelfChanger extends Component {
state = {
value: this.props.shelf,
};
handleChange = event => {
this.setState({ value: event.target.value });
this.props.onMove(this.props.book, event.target.value);
};
render() {
return (
<div className="book-shelf-changer">
<select value={this.state.value} onChange={this.handleChange}>
<option value="move" disabled>
Move to...
</option>
<option value="currentlyReading">Currently Reading</option>
<option value="wantToRead">Want to Read</option>
<option value="read">Read</option>
<option value="none">None</option>
</select>
</div>
);
}
}
export default BookshelfChanger;
import React, { Component } from 'react';
import SearchResults from './SearchResults';
import SearchBar from './SearchBar';
class SearchBooks extends Component {
render() {
const {
searchBooks,
myBooks,
onSearch,
onResetSearch,
onMove,
} = this.props;
return (
<div className="search-books">
<SearchBar onSearch={onSearch} onResetSearch={onResetSearch} />
<SearchResults
searchBooks={searchBooks}
myBooks={myBooks}
onMove={onMove}
/>
</div>
);
}
}
export default SearchBooks;
import React from 'react';
import { Link } from 'react-router-dom';
const CloseSearchButton = props => {
const { onResetSearch } = props;
return (
<Link to="/">
<button className="close-search" onClick={onResetSearch}>
Close
</button>
</Link>
);
};
export default CloseSearchButton;
import React, { Component } from 'react';
class SearchBooksInput extends Component {
state = {
value: '',
};
handleChange = event => {
const val = event.target.value;
this.setState({ value: val }, () => {
this.props.onSearch(val);
});
};
render() {
return (
<div className="search-books-input-wrapper">
<input
type="text"
value={this.state.value}
placeholder="Search by title or author"
onChange={this.handleChange}
autoFocus
/>
</div>
);
}
}
export default SearchBooksInput;
import React from 'react';
import Book from './Book';
const SearchResults = props => {
const { searchBooks, myBooks, onMove } = props;
const updatedBooks = searchBooks.map(book => {
myBooks.map(b => {
if (b.id === book.id) {
book.shelf = b.shelf;
}
return b;
});
return book;
});
return (
<div className="search-books-results">
<ol className="books-grid">
{updatedBooks.map(book => (
<Book
key={book.id}
book={book}
shelf={book.shelf ? book.shelf : 'none'}
onMove={onMove}
/>
))}
</ol>
</div>
);
};
export default SearchResults;
Some of these components could have been left in their parent component files e.g. OpenSearchButton, CloseSearchButton, etc. but I separated everything out nonetheless.
Here’s Main page.
Live Demo: reactnd-project-myreads@9-separate-components on CodeSandbox
Here is the Search page.
Live Demo: reactnd-project-myreads@9-separate-components on CodeSandbox
The last step was to push everything to GitHub and make sure the README instructions were up to date and clearly specified how to install and run the project.
If you chose to develop on your local machine (by either starting with the starter project or starting from scratch with Create React App), you will need to:
- Push your project to GitHub, making sure to push the master branch.
- On the project submission page choose the option “Submit with GitHub”
- Select the repository for this project (you may need to connect your GitHub account first).
Here’s the response I received.
Meets Specifications
Congratulations on completing MyReads on your first attempt.
The detailed process walkthrough is a great addition to the project; be sure to include the link when presenting the portfolio.
If you’re interested to take it up a notch, you can provide test coverage to components with Jest. Get started here:
Keep up the good work and have fun with your coming projects.
Application Setup
The README is detailed and and the process walkthrough is invaluable. Here are some suggestions for the process walkthrough:
- break the process down into stages ( i.e. architecture planning, => UI components => logic => refactoring ). This write-up should be written with minimal mention of tools to explain your thought process which is invaluable in an interview,
- explain your understanding of data mutability/immutability and state management
- testing (try not to ignore this aspect before heading into an interview),
Here’s a handy README template for future projects:
All things said, good work 💯
Main Page
BEST PRACTICE: Try not to break the application down into too many moving parts.
A rationale is given and explained in the Code Review section.
Search Page
Search was well implemented and bug-free 💯
Code Functionality
Component State
Tip ⚡
Assigning array and/or objects to new variables creates shallow copies and might cause ‘illegal’ updates to state.
Explanation in Code Review > App.js.
Error Handling
BEST PRACTICE: When writing asynchronous code, it’s a good idea to include error handling.
Examples can be found in the Code Review section.
Here’s breakdown of the comments related to each file.
import { Route } from 'react-router-dom';
import { debounce } from 'throttle-debounce';
import * as BooksAPI from './BooksAPI';
import './App.css';
// import getAll from './data';
import ListBooks from './ListBooks';
import SearchBooks from './SearchBooks';
class BooksApp extends Component {
bookshelves = [
{ key: 'currentlyReading', name: 'Currently Reading' },
{ key: 'wantToRead', name: 'Want to Read' },
{ key: 'read', name: 'Read' },
];
BEST PRACTICE:
Try not to pollute the scope of this
with variables, especially if those variables are constants. These can be moved out of the components scope or into a separate file.
state = {
myBooks: [],
searchBooks: [],
};
componentDidMount = () => {
BooksAPI.getAll().then(books => {
this.setState({ myBooks: books });
});
};
BEST PRACTICE:
It’s good practice to catch
potential errors from a remote server. Many things might go wrong:
An error key can be created in the state
object for this purpose.
moveBook = (book, shelf) => {
BooksAPI.update(book, shelf);
let updatedBooks = [];
updatedBooks = this.state.myBooks.filter(b => b.id !== book.id);
if (shelf !== 'none') {
book.shelf = shelf;
updatedBooks = updatedBooks.concat(book);
}
this.setState({
myBooks: updatedBooks,
});
};
A few factors to note about updateBooks
assignment.
Array.prototype.filter
creates/returns a new array after running the filtering function but it only provides a shallow copy - meaning nested objects are still passed by ref. Read about this method here:
this.setState
.Read more about shallow vs deep copies:
Read about using previous state in this.setState
here:
import React from 'react';
import BookshelfChanger from './BookshelfChanger';
const Book = props => {
const { book, shelf, onMove } = props;
Tip ⚡
The props
object received from this component’s parent can be destructured as parameters for a stateless component.
return (
<li>
<div className="book">
<div className="book-top">
<div
className="book-cover"
style={{
width: 128,
height: 193,
backgroundImage: `url(${book.imageLinks &&
book.imageLinks.thumbnail})`,
BEST PRACTICE:
Display placeholders to improve the user’s experience when the server returns incomplete data.
}}
/>
<BookshelfChanger book={book} shelf={shelf} onMove={onMove} />
</div>
<div className="book-title">{book.title}</div>
<div className="book-authors">
{book.authors && book.authors.join(', ')}
BEST PRACTICE:
Use the ‘short-circuit’ pattern with care. When building user-centric UI you’d still want to return something instead of null
or undefined
.
See alternative in code example above.
import React, { Component } from 'react';
class BookshelfChanger extends Component {
state = {
value: this.props.shelf,
};
handleChange = event => {
this.setState({ value: event.target.value });
this.props.onMove(this.props.book, event.target.value);
};
Tip ⚡
Destructure event
and assign it’s value to this.state.value
with the shorthand method.
render() {
// console.log(this.props.shelf);
BEST PRACTICE:
It’s a good idea to remove logging statements before presenting the code in your portfolio.
Additionally, breakpoints are efficient compared to console.log
:
import React from 'react';
import { Link } from 'react-router-dom';
const CloseSearchButton = props => {
BEST PRACTICE:
There are no written rules on the size of a component before it should be separated into its own file. However, a few considerations apply to keep code base maintenance sane.
Don’t put code in it’s own file/component if:
EXPLANATION:
In the production environment the code base will last years until it becomes ‘legacy code’. Maintenance, optimizations and updates are performed by the team on a daily basis.
A separate component for each UI element will only get you a meeting with HR.
Here’s a summary of the changes and requirements based on the code review.
App.js
error
key in state to trigger message in UI.else
statements.Book.js
null
or undefined
.BookshelfChanger.js
event
with shorthand method.error
key in state to trigger message in UI.else
statements.class BooksApp extends Component {
bookshelves = [
{ key: 'currentlyReading', name: 'Currently Reading' },
{ key: 'wantToRead', name: 'Want to Read' },
{ key: 'read', name: 'Read' },
];
bookshelves = [
{ key: 'currentlyReading', name: 'Currently Reading' },
{ key: 'wantToRead', name: 'Want to Read' },
{ key: 'read', name: 'Read' },
];
class BooksApp extends Component {
state = {
myBooks: [],
searchBooks: [],
};
componentDidMount = () => {
BooksAPI.getAll().then(books => {
this.setState({ myBooks: books });
});
};
render() {
const { myBooks, searchBooks, error } = this.state;
return (
// ui code...
)
}
state = {
myBooks: [],
searchBooks: [],
error: false
};
componentDidMount = () => {
BooksAPI.getAll()
.then(books => {
this.setState({ myBooks: books });
})
.catch(err => {
console.log(err);
this.setState({ error: true });
});
};
render() {
const { myBooks, searchBooks, error } = this.state;
if (error) {
return <div>Network error. Please try again later.</div>;
}
return (
// ui code...
)
}
}
moveBook = (book, shelf) => {
BooksAPI.update(book, shelf);
let updatedBooks = [];
updatedBooks = this.state.myBooks.filter(b => b.id !== book.id);
if (shelf !== 'none') {
book.shelf = shelf;
updatedBooks = updatedBooks.concat(book);
}
this.setState({
myBooks: updatedBooks,
});
};
moveBook = (book, shelf) => {
BooksAPI.update(book, shelf).catch(err => {
console.log(err);
this.setState({ error: true });
});
if (shelf === 'none') {
this.setState(prevState => ({
myBooks: prevState.myBooks.filter(b => b.id !== book.id)
}));
} else {
book.shelf = shelf;
this.setState(prevState => ({
myBooks: prevState.myBooks.filter(b => b.id !== book.id).concat(book)
}));
}
};
null
or undefined
.import React from 'react';
import BookshelfChanger from './BookshelfChanger';
const Book = props => {
const { book, shelf, onMove } = props;
return (
<li>{/* UI code */}</li>
);
};
import React from 'react';
import BookshelfChanger from './BookshelfChanger';
const Book = ({ book, shelf, onMove }) => (
<li>{/* UI code */}</li>
);
return (
<li>
<div className="book">
<div className="book-top">
<div
className="book-cover"
style={{
width: 128,
height: 193,
backgroundImage: `url(${book.imageLinks &&
book.imageLinks.thumbnail})`,
<li>
<div className="book">
<div className="book-top">
<div
className="book-cover"
style={{
width: 128,
height: 193,
backgroundImage: `url(${
book.imageLinks
? book.imageLinks.thumbnail
: 'icons/book-placeholder.svg'
})`
<div className="book-authors">
{book.authors && book.authors.join(', ')}
<div className="book-authors">
{book.authors ? book.authors.join(', ') : 'Unknown Author'}
event
with shorthand method.import React, { Component } from 'react';
class BookshelfChanger extends Component {
state = {
value: this.props.shelf,
};
handleChange = event => {
this.setState({ value: event.target.value });
this.props.onMove(this.props.book, event.target.value);
};
import React, { Component } from 'react';
class BookshelfChanger extends Component {
state = {
value: this.props.shelf,
};
handleChange = event => {
const { value } = event.target;
this.setState({ value });
this.props.onMove(this.props.book, value);
};
The last piece is to consolidate components based on the following recommendations.
Don’t put code in it’s own file/component if:
- its length is a few lines and called only a few times from other components,
- props need to be passed multiple levels to reach the component,
- creating it causes future maintenance to be a headache due to component saturation
Given this instruction I decided to follow this new, simplified hierarchy.
Here’s the screenshot of the UI which hasn’t changed but the underlying structure has.
Live Demo: reactnd-project-myreads@10-code-review-enhancements on CodeSandbox