Today we will create a full stack MERN app. The app will display books.
The back-end we are using has already been written (thanks Anhar!) https://github.com/anharathoi/books-backend-mern
You will need to clone the back-end
git clone https://github.com/anharathoi/books-backend-mern.git
Change into the directory
cd books-backend-mern
Install dependencies
npm install
Run the server
npm start
The back-end is running on port 5000. Our front-end will run on port 3000.
To seed the data
http://localhost:5000/seed
To view the data
http://localhost:5000/books
Create the client app
npx create-react-app books-front-end
Change into the app folder
cd books-front-end
Install dependencies
npm install
Run the app
npm start
See a spinning logo?
Back at terminal lets open Visual Studio Code
code .
First lets convert the App
component to a class component and add a state variable to store our books.
/src/App.js
import React, { Component } from 'react';
import './App.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
books: []
};
}
render() {
console.log('App is running render');
return (
<div>
<h1>Books {this.state.books.length}</h1>
</div>
);
}
}
We want to load the books when the component mounts. We may also load it after we make updates, so we will create loadBooks
as a separate function we can call elsewhere.
/src/App.js
loadBooks = () => {
console.log('loading books')
const url = 'http://localhost:5000/books';
fetch(url)
.then(resp => resp.json())
.then(data => {
console.log(data)
this.setState({ books: data, adding: false });
})
}
componentDidMount() {
console.log('App is running componentDidMount');
this.loadBooks();
}
The heading should now display the number of books.
Now we have the books loading let's display them. We will create a separate method for displaying the books.
/src/App.js
bookList = () => {
return (
<div className="book-list">
{this.state.books.map((item, index) => (
<div >
{item.genre}: {item.title} by {item.author} (rrp ${item.price}) (id: {item.id})
</div>
))}
</div>
)
}
render() {
console.log('App is running render');
return (
<div>
<h1>Books {this.state.books.length}</h1>
{this.bookList()}
</div>
);
}
To add books we will first wire up displaying the a form
- add a button
- add a handler for the button
- add a variable to state for adding
- display the form when adding
Adding the button and handler:
/src/App.js
handleClick = () => {
this.setState({ adding: true });
}
render() {
console.log('App is running render');
return (
<div>
<h1>Books {this.state.books.length}</h1>
<button onClick={this.handleClick}>Add</button>
{this.bookList()}
</div>
);
}
Adding the adding variable to state:
/src/App.js
constructor(props) {
super(props);
this.state = {
books: [],
adding: false
};
}
In the render method we will conditionally display the BookForm
.
/src/App.js
render() {
console.log('App is running render');
return (
<div>
<h1>Books {this.state.books.length}</h1>
<button onClick={this.handleClick}>Add</button>
{this.state.adding ?
<BookForm /> :
this.bookList()
}
</div>
);
}
Create new files for the BookForm
and styling.
We will use the simple form styling from our wish-list
project.
/src/components/BookForm.css
.book-form {
margin-left: 20px;
}
form {
font-size: 16px;
max-width: 500px;
}
input {
margin: 5px 0 25px 0;
display: block;
width: 100%;
font-size: 16px;
}
label {
display: block;
width: 100%;
}
button {
width: 120px;
padding: 10px 30px;
}
/src/components/BookForm.js
import React, { Component } from 'react';
import './BookForm.css';
class BookForm extends Component {
constructor(props) {
super(props);
this.state = {
title: '',
author: '',
genre: '',
price: 0
};
}
handleClick = (e) => {
e.preventDefault();
const newBook = {
title: this.state.title,
author: this.state.author,
genre: this.state.genre,
price: this.state.price
}
this.props.addNewBook(newBook)
}
handleChange = (e) => {
this.setState({ [e.target.id]: e.target.value})
}
render(){
console.log(this.state);
return (
<div className="book-form">
<h1>Book Form</h1>
<form>
<label htmlFor="title">Title</label>
<input onChange={this.handleChange} type="text" id="title" placeholder="title" />
<label htmlFor="author">Author</label>
<input onChange={this.handleChange} type="text" id="author" placeholder="author" />
<label htmlFor="genre">Genre</label>
<input onChange={this.handleChange} type="text" id="genre" placeholder="genre" />
<label htmlFor="price">Price</label>
<input onChange={this.handleChange} type="number" id="price" placeholder="price" />
<button onClick={this.handleClick}>Save</button>
<button onClick={this.handleCancel}>Cancel</button>
</form>
</div>
);
}
}
export default BookForm;
handleCancel
is not implemented but appears to be working?
We will implement this for the challenge.
Lets wire up the 'addNewBook' method.
/src/App.js
addNewBookHandler = (newBook) => {
console.log(newBook);
this.setState({ adding: false });
}
render() {
console.log('App is running render');
return (
<div>
<h1>Books {this.state.books.length}</h1>
<button onClick={this.handleClick}>Add</button>
{this.state.adding ?
<BookForm addNewBook={this.addNewBookHandler} /> :
this.bookList()
}
</div>
);
}
Update the addNewBookHandler
to create a book id so we have a complete object to save.
/src/App.js
addNewBookHandler = (newBook) => {
const newId = this.state.books.length + 1;
newBook.id = newId;
console.log(newBook);
this.setState({ adding: false });
}
Now we have a the information we need to create new book we can ask our back end to create it. We will use a method for this. We will remove the setState
from here as we will do it later. We do not want to setState
more than necessary.
/src/App.js
addNewBookHandler = (newBook) => {
const newId = this.state.books.length + 1;
newBook.id = newId;
console.log(newBook);
this.createBook(newBook);
}
To create the book we will use a 'POST'.
Once the 'POST' is complete we will use our loadBooks
method to update the book list. loadBooks
re-loads the books from the database and calls setState
which will reload the page and hide the form.
/src/App.js
createBook = (book) => {
const url = 'http://localhost:5000/books/newbook';
const data = JSON.stringify(book);
console.log(data)
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: data,
})
.then(response => {
this.loadBooks();
}
)
}
To edit a book we need to be able select a book to edit. We will add a click event to the book list items and a handler for the click event. The click event handler will use a state variable to switch to editing the selected book.
/src/App.js
handleEditClick(book) {
console.log(book)
this.setState({ selectedBook: book })
}
bookList = () => {
return (
<div className="book-list">
{this.state.books.map((item, index) => (
<div onClick={() => this.handleEditClick(item)} key={index}>
{item.genre}: {item.title} by {item.author} (rrp ${item.price}) (id: {item.id})
</div>
))}
</div>
)
}
We need to add this state variable to the constructor
.
/src/App.js
constructor(props) {
super(props);
this.state = {
books: [],
adding: false,
selectedBook: null
};
}
In the render
method we will conditionally display the BookForm
for editing. When editing we will pass the values for the selected book into the form for editing:
/src/App.js
render() {
console.log('App is running render');
return (
<div>
<h1>Books {this.state.books.length}</h1>
<button onClick={this.handleClick}>Add</button>
{this.state.adding ?
<BookForm addNewBook={this.addNewBookHandler} /> :
this.state.selectedBook ?
<BookForm
addNewBook={this.addNewBookHandler}
title={this.state.selectedBook.title}
author={this.state.selectedBook.author}
genre={this.state.selectedBook.genre}
price={this.state.selectedBook.price}
id={this.state.selectedBook.id}
/> :
this.bookList()
}
</div>
);
}
The values have been passed into the BookForm
via props
, now we need to read them out. We will read them out in the constructor
. This will initialize our Book values to the selectedBook
.
/src/components/BookForm.js
constructor(props) {
super(props);
this.state = {
title: props.title || '',
author: props.author || '',
genre: props.genre || '',
price: props.price || 0,
id: props.id || 0
};
}
Update the render method to initialize the input fields with the values from state
.
value={this.state.title}
Here I have converted to a multiline layout for the input fields because it was getting hard to read on one line.
/src/components/BookForm.js
render(){
console.log(this.state);
return (
<div className="book-form">
<h1>Book Form</h1>
<form>
<label htmlFor="title">Title</label>
<input
onChange={this.handleChange}
type="text"
id="title"
placeholder="title"
value={this.state.title}
/>
<label htmlFor="author">Author</label>
<input
onChange={this.handleChange}
type="text"
id="author"
placeholder="author"
value={this.state.author}
/>
<label htmlFor="genre">Genre</label>
<input
onChange={this.handleChange}
type="text"
id="genre"
placeholder="genre"
value={this.state.genre}
/>
<label htmlFor="price">Price</label>
<input
onChange={this.handleChange}
type="number"
id="price"
placeholder="price"
value={this.state.price}
/>
<button onClick={this.handleClick}>Save</button>
<button onClick={this.handleCancel}>Cancel</button>
</form>
</div>
);
}
We need to read the all the values (including the id
) out of the form state
and pass them to the App
component:
/src/components/BookForm.js
handleClick = (e) => {
e.preventDefault();
const newBook = {
title: this.state.title,
author: this.state.author,
genre: this.state.genre,
price: this.state.price,
id: this.state.id
}
console.log(newBook)
this.props.addNewBook(newBook)
}
Lets create a method in our App
component to save the updated book. We will use 'PUT' and the corresponding endpoint in the back-end.
/src/App.js
updateBook(book){
const url = `http://localhost:5000/books/${book.id}`;
const data = JSON.stringify(book);
console.log(data)
return fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: data,
})
.then(response => {
this.loadBooks();
}
)
}
We will update our addNewBookHandler
to handle both updating and creating. We can test to see if an id
exists to see if the book needs to be created or updated.
/src/App.js
addNewBookHandler = (newBook) => {
console.log(newBook);
if (newBook.id) {
this.updateBook(newBook);
}
else {
const newId = this.state.books.length + 1;
newBook.id = newId;
this.createBook(newBook);
}
}
Once we have finished updating we will need to reset our selectedBook
state. The last step is loadBooks
. We will add to the existing setState
call.
/src/App.js
loadBooks = () => {
console.log('loading books')
const url = 'http://localhost:5000/books';
fetch(url)
.then(resp => resp.json())
.then(data => {
console.log(data)
this.setState({ books: data, adding: false, selectedBook: null });
})
}
Re-using a form for both New and Create is a common pattern.
- The form is being re-used but we can make it clearer. Use conditionals to
- display either
Edit
orCreate New
in the form heading - display either
Update
orCreate New
on the current save button
- The
new
methods are being used to createAndUpdate.
- Change the name of the
App
component methodaddNewBookHandler
tocreateAndUpdateBookHandler
. - Rename the
BookForm
propaddNewBook
tocreateAndUpdateBook
.
- Improve the appearance of the book list by
- displaying the prices with two decimal points
- display a different background color for each genre
- make the lines larger so they are easier to click on
-
We can still see the number zero being displayed in the heading. Lets use a Loading component to display when the books array is empty.
-
The add button is displayed when the form is displayed. Change the code in
App
render so the button is not displayed at the same time as the form. -
The button default behaviour for
BookForm
cancel button is causing the page to refresh. ImplementhandleCancel
. It should be similar toaddNewBook
but not save a book. -
Add
Notifications
for
- Book saved
- book updated
- add / update canceled
- Refactor the code to pass a book rather then all the separate props when editing a book.
BookForm
will also need to be changed. eg:
<BookForm addNewBook={this.addNewBookHandler} book={this.state.editedBook}/>
- Add links to apply sorting to the book list. Sort by
- author
- title
- price
- Add a link to filter by genre
Add validation to the form. Make all the fields required. Make sure the price is not negative. Display error messages when fields are missing or invalid.
Add the ability to delete a book.