Learning React & Redux in 2023 — Part 6
In part 5 we created a books app project, however the book object was non-persisted, which basically just means that if you refresh the browser, all the books you stored disappear. We’ll now look to re-factor that project with an outside API so that the books remain persisted.
We’ll be using JSON server which is good for development purposes.
We’ll install it with:
npm install json-server
once installed, we’ll create a db.json file in the root of our books project folder, and inside this file we’ll add the following:
{
"books": []
}
We’ll also update our package.json file with a new script:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"server": "json-server --watch db.json --port 3001 --host 127.0.0.1"
},
finally we’ll run the json server with the following command:
npm run server
Having created our API with a key of “books” and an empty array, we can now make a series of different requests to JSON server, and get back specific responses:
So for example, we can create a new book object with a POST method, or we can delete a book object with the DELETE method.
To test this we’ll need some kind of API client. If you’re using VSCode as your editor (highly recommended!) then you can install the “REST Client” extension in VSCode
To get it working, we need to create a setup file.
We’ll create a file called api.http and store it in the root of our project. Inside this file, we’ll type the exact text:
GET http://localhost:3001/books HTTP/1.1
Content-Type: application/json
If all went well (and REST Client was installed correctly) you should immediately see the words “Send Request” appear above what you typed..
You can now click on this to make a GET request to JSON server. The response will appear on the right of the screen:
So we can see we received back an empty array, which is what we expected as we created an emtpy array in db.json
Now let’s try adding a book object using a POST request…. we’ll create a brand new POST request, and use ### to act as a request sperator….
GET http://localhost:3001/books HTTP/1.1
Content-Type: application/json
###
POST http://localhost:3001/books HTTP/1.1
Content-Type: application/json
{
"title": "The Lord of the Rings"
}
once again we should see a “Send Request” link we can click on…
We can see from the response that the book object was added, and it was also given a unique id.
Let’s try editing this book with id of 1. We’ll change the title to something else….. let’s create another request using PUT and we’ll specify the id of the object we want to edit…
GET http://localhost:3001/books HTTP/1.1
Content-Type: application/json
###
POST http://localhost:3001/books HTTP/1.1
Content-Type: application/json
{
"title": "The Lord of the Rings"
}
###
PUT http://localhost:3001/books/1 HTTP/1.1
Content-Type: application/json
{
"title": "Harry Potter"
}
when we click Send Request, we get the following:
finally for completeness, we’ll test the DELETE method to delete an object…
GET http://localhost:3001/books HTTP/1.1
Content-Type: application/json
###
POST http://localhost:3001/books HTTP/1.1
Content-Type: application/json
{
"title": "The Lord of the Rings"
}
###
PUT http://localhost:3001/books/1 HTTP/1.1
Content-Type: application/json
{
"title": "Harry Potter"
}
###
DELETE http://localhost:3001/books/1 HTTP/1.1
Content-Type: application/json
Now in terms of interacting with our books project and that in turn working with JSON server, we’ll make use of Axios. First let’s install it into our project:
npm install axios
The plan will be to make our project make a request to JSON server, and then use the response to update our piece of state.
The first thing we’ll want to do is make some changes to our previous App.js file. Here’s a recap of what it was left at in the last project:
import { useState, useEffect } from "react";
import BookCreate from "./components/BookCreate";
import BookList from "./components/BookList";
function App() {
const [books, setBooks] = useState([]);
const handleEditBook = (id, newTitle) => {
const updatedBooks = books.map((book) => {
if (book.id === id) {
return { ...book, title: newTitle };
}
return book;
});
setBooks(updatedBooks);
};
const handleDeleteBook = (id) => {
const updatedBooks = books.filter((book) => book.id !== id);
setBooks(updatedBooks);
};
const handleCreateBook = (bookTitle) => {
const updatedBooks = [...books,
{
id: Math.round(Math.random() * 99999),
title: bookTitle
},
];
setBooks(updatedBooks);
console.log(bookTitle, books.length);
};
useEffect(() => {
console.log(books.length);
}, [books]);
return (
<div>
<BookList books={books} editBook = {handleEditBook} deleteBook = {handleDeleteBook}/>
<BookCreate onCreateBook={handleCreateBook}/>
</div>
);
}
export default App;
as we’ll be using axios, we’ll need to import it:
import { useState, useEffect } from "react";
import axios from "axios";
import BookCreate from "./components/BookCreate";
import BookList from "./components/BookList";
next, let’s change the handleCreateBook function so that when we create a book, the first thing that happens, is we make an axios POST request to JSON server, and then we’ll use the response we get back to update our state.
const handleCreateBook = async (bookTitle) => {
const response = await axios.post("http://localhost:3001/books", {
title: bookTitle,
});
const updatedBooks = [...books, response.data];
setBooks(updatedBooks);
console.log(response.data);
};
Because we’ll be waiting for a response, we need to mark our function with the async/await keywords.
We’re making a POST request with axios.post with the first arguement being the address and key of the JSON server object, and the next argument is what we want to send, which is a new object, with a title of the booktitle we pass into our handleCreateBook function.
We’ll then get back a response, which will show the book object from JSON server, which we can also view in the console.
so we can see our response.data is showing the new array object of a book with title and id.
Fetching the list of Books at the FIRST render
We’re updating db.json but if we refresh the screen we still don’t see out list of books at the FIRST render. We need to create a function that will retrieve the list of books from JSON server to render to the screen. Let’s first create that function.
const fetchBooks = async () => {
const response = await axios.get("http://localhost:3001/books");
setBooks(response.data);
};
this function would use a GET request to get all the books, and update our piece of state, but how do we call this function at first render?
what we DO NOT want to do is something like
fetchbooks();
if we call this function at first render, it will retrieve the books and update our piece of state using setBooks(response.data)
….but
if we update our piece of state, then our App function will RE-RENDER, and if we re-render, it will call fetchBooks() AGAIN
this will create an infinite fetch request, and cause our project to eventually crash!
useEffect !
This is where we use the useEffect feature of react. it is a feature designed to call a function ONCE and only ONCE.
the following code will tell react to call the fetchBooks() function ONCE during intial render:
const fetchBooks = async () => {
const response = await axios.get("http://localhost:3001/books");
setBooks(response.data);
};
useEffect(() => {
fetchBooks();
}, []);
The reason useEffect doesn’t call fetchBooks() on every subsequent RE-RENDER is because we’ve specified an empty array as a second arguement. If there was no empty array [] then it would also call this fetchBooks() function on every subsequent re-render.
If there is a variable inside the [] — for example [counter] …. if this variable changes, then fetchBooks() would also be called during a re-render IF the variable changed. So for example, if counter == 1 and then after a while counter == 2 then fetchBooks() would be called at the next re-render because counter changed.
Now that’s we’ve updated the handleCreateBook function, let’s do the same for handleEditBook and handleDeleteBook.
const handleEditBook = async (id, newTitle) => {
const response = await axios.put(`http://localhost:3001/books/${id}`, {
title: newTitle});
const updatedBooks = books.map((book) => {
if (book.id === id) {
return { ...book, ...response.data };
}
return book;
});
setBooks(updatedBooks);
};
const handleDeleteBook = async (id) => {
await axios.delete(`http://localhost:3001/books/${id}`);
const updatedBooks = books.filter((book) => book.id !== id);
setBooks(updatedBooks);
};
for editing the book, we use a PUT request. We want to pass in the book id, so to pass this into the URL we use back-ticks so that we can reference the value as the URL
`http://localhost:3001/books/${id}`
The second argument is the actual newTitle we’ll be sending.
notice in the return statement when we used the spreader, we also spread out the response.data response
return { ...book, ...response.data };
it makes sense to return the entire object from db.json rather than just the title: newTitle as we had before. After all, the db could start to include new objects with new key value pairs, so makes sense to return the entire object rather than just the book title.
Finally with the handleDeleteBook function, we added in a DELETE method, again using backticks.
Notice on this function we didn’t assign the response back to a variable called response?
const handleDeleteBook = async (id) => {
await axios.delete(`http://localhost:3001/books/${id}`);
const updatedBooks = books.filter((book) => book.id !== id);
setBooks(updatedBooks);
};
If you think about it, this would be totally unnecessary. We’re deleting an object from JSON server, and then immediately after we’re updating a piece of state with updatedBooks. At this point, both the db.json file AND the piece of state match because they’ve both just had the specific book object removed, first by the DELETE request, and then immediately after by using the React filter method. Because we’re changing the setBooks piece of state, it will also cause the App function to re-render (although NOT specifically the fetchBooks() function because of useEffect!), so the page will show the correct number of books, with the last one deleted.