If you haven’t already, take a look at Part 1 of this tutorial. It goes through the basics of setting up a React app and making GET requests to the dog.ceo API to pull pictures of random doggos!
In this part of the tutorial we’ll make POST requests to save which pictures are our favorites! The Dog API doesn’t actually have this ability so I’ve built out a small API that you can use to practice on.
Step 1. Updating our initial state
Since our app now saves favorites, we need to update our state object to include this. So simply enough, we’ll add a favorites key to our state with the initial value being an empty array. In addition, we will change the imageLoading key to something more generic so we can use it for the times new random images are loading as well as when we are saving a favorite. Let’s call it, isLoading. So with that said, here’s our new initial state object:
this.state = { image: null, isLoading: false, favorites: [] }
Once you’ve updated the inital state, be sure to replace any instances of imageLoading throughout your code to isLoading.
Step 2. Saving a favorite
Now that we’ve updated our state object to store favorites, we need to actually add that functionality to our app! We will be using the same fetch function that was used in Part 1, but instead of making requests to the Dog API, we’ll be making requests to an API I made just for this tutorial. Before making a request, you’ll need to get a token to identify you’re app.
Once you have your token, we can move on to making the API request. To save a favorite we will be making a POST request to the url: https://dogs.rojas.io/account/[YOUR_ACCOUNT_TOKEN]/favorite.
Making POST requests with the Fetch function is slightly different from making GET requests. We need to tell the Fetch function that we are making a POST request. To do that, you pass an object as a second parameter. The object will look like this:
{ method: 'POST' }
Besides telling the Fetch function that we are posting to a url, we also need to tell it what it is we are actually posting. To do that you need to tell it the content type as well as the actual content. The type of content we will be posting is JSON and it’s sent as part of the headers key. You pass the actual content to the body key. The body needs to be a JSON string. To save a favorite, the API is expecting a POST body containing a url key. So that’s what we will send! Here is an updated object with the content type and body included.
{ method: 'POST', headers: { 'Content-type': 'application/json' }, body: JSON.stringify({ url: this.state.image }) }
We will pass that object as the second parameter to the Fetch function. The rest of the code is pretty much the same as it was in Part 1. With that information, try adding a function to your App class called saveFavorite that uses the Fetch function to make an ajax call to https://dogs.rojas.io/account/[YOUR_ACCOUNT_TOKEN]/favorite. It should let the user know that there is a save in progress (Hint: Check your getRandomDogImage function as it’s similar). When an image is successfully saved as a favorite, the response will contain a url key with the one that you just saved. If it’s not there, an error occurred. Here is the code I came up with:
saveFavorite() { this.setState({ isLoading: true }); let self = this; fetch("https://dogs.rojas.io/account/[YOUR_TOKEN]/favorite", { method: 'POST', headers: { 'Content-type': 'application/json' }, body: JSON.stringify({ url: this.state.image }) }).then(function(response) { return response.json(); }) .then(function(jsonData) { if(typeof jsonData.url !== 'undefined') { let favorites = self.state.favorites.slice(); favorites.unshift(jsonData.url); self.setState({ isLoading: false, favorites: favorites }); } else { return Promise.reject(jsonData); } }) .catch((err) => { console.log(err); self.setState({ isLoading: false }); }); }
The first bit of code:
this.setState({ isLoading: true });
Sets the isLoading state to true and shows the loading message on the screen.
The next bit of code: let self = this; allows us to access the setState function while inside of the Fetch function. Like I said in Part 1, if you use arrow functions, you won’t have to do this part. But for this tutorial we won’t be using them.
The next block of code is us actually making the ajax call. In the second promise block, we check if jsonData.url is defined before accessing it. The next few lines:
let favorites = self.state.favorites.slice(); favorites.unshift(jsonData.url);
Prepares the new url to be added to our favorites state. The first line in that block makes a copy of the favorites array. The next line appends the url to the beginning of the array.
The final bit of code in that block sets the state of isLoading back to false and sets the favorites state to include our newly added url.
When jsonData.url is undefined, we return a rejected promise that gets caught in the catch block. There we just set the state of isLoading back to false. Normally we would display some kind of error message when it gets to this point, but for this tutorial we won’t be doing that.
The last thing you’ll need to do is bind the function to the class. If you remember from Part 1, you do this in the constructor function. Here’s the line of code you need to add:
this.saveFavorite = this.saveFavorite.bind(this);
Your constructor should now look like this:
constructor(props) { super(props); this.state = { image: null, isLoading: false, favorites: [] } this.getRandomDogImage = this.getRandomDogImage.bind(this); this.saveFavorite = this.saveFavorite.bind(this); }
Step 3. Adding the Favorite Button
Now that we’ve made a function to save a favorite, we need to add a button that triggers that function. We’ll add it next to the “Get New Random Image” button. After someone clicks on the favorite button, we should update the button to show that it’s been favorited. To do that, we need to have a condition wrapped around the button to check if the current image is in our favorites array. So with that information, try to code out the favorite button in the render function. I’ve written out my code below for how I’m showing the favorite button so if you get stuck feel free to take a look.
{ this.state.favorites.indexOf(this.state.image) === -1 ? ( // We haven't favorited this image <button type="button" onClick={this.saveFavorite}>Favorite</button> ) : ( // The image is already favorited <button type="button" disabled>Favorited!</button> ) }
There are a bunch of different ways you could have coded out how the favorite button will be displayed. I went with this way because it let’s you write just one condition rather than coding one button and wrapping the condition around each of the differences when an image is either favorited or not favorited. The first line in the code is the condition we’re checking. It basically says, “is the current image on the screen in the favorites array?”. indexOf, if you don’t know tries to find the index of the value you pass to it. So if the image is found, it will return the index i.e. 0,1,2 and so on. If it’s not found it returns -1.
The next few lines shows slightly different buttons based on if the image has been favorited or not. Also, I’m only triggering the saveFavorite function if someone clicks on the button when the image is not already favorited. Once you save your code, you’ll see that the favorite button appears on the page, though it is kind of brushing up against the original button that was on the page. Let’s add some CSS so it doesn’t look so cramped. While we’re at it, lets also change the color of the Favorite button to something other than the same color that the “Get New Random Image” button is. I’m going to make my favorite button orange and after someone has favorited an image, my button is going to be green. If you are comfortable with CSS feel free to change the colors to whatever you’d like. Here’s what I came up with.
.App button { background-color: #0080ff; padding: 10px 18px; color: white; font-size: 16px; font-weight: bold; border: 0px; border-radius: 3px; box-shadow: 0px 0px 5px 0px rgba(0,0,0,.25); cursor: pointer; margin: 0px 10px; } .App .btn-orange { background-color: #ff9600; } .App .btn-orange:hover { background-color: #ec8b00; } .App .btn-green { background-color: #0ec100; } .App .btn-green[disabled], .App .btn-green[disabled]:hover { background-color: #0ec100; cursor: default; }
The .App button definition is mostly the same from the Part 1 of the tutorial. However I’ve added margin: 0px 10px; to the bottom so that the buttons are separated.
The next classes .App .btn-orange and .App .btn-green are used to style the button so they are orange or green respectively. I’ve styled how the buttons look when hovered over and specifically added an extra disabled selector for the green button since my green button is always going to be disabled.
The next thing to do is add the classes to our button. After you’ve done that our code to render the button will look like this:
{ this.state.favorites.indexOf(this.state.image) === -1 ? ( // We haven't favorited this image <button type="button" onClick={this.saveFavorite} className="btn-orange">Favorite</button> ) : ( // The image is already favorited <button type="button" disabled className="btn-green">Favorited!</button> ) }
Try clicking on the favorite button. It should now turn green and say “Favorited!” once the save is complete!
Step 4. Displaying your favorite images
Now that we’ve saved our favorites, it would be nice to actually go back and view them right? To do that we’ll list out each url in our favorites array just below the two buttons we have so far. When someone clicks on one of the urls it should load up as the current image and have an indicator that it’s being shown. With that being said, there’s two things we need to do. The first is make a function that sets this.state.image to a specific favorite image. The next is adding a section to our render function that displays a list of our favorited images.
The function to show a favorite image is relatively straight forward. It should take in which favorite image to show. If you think you can write that bit of code on your own, go for it! If you’re stuck here’s what I came up with:
viewFavorite(imageURL) { this.setState({ image: imageURL }); }
At this point, you should be able to understand the above code. It sets this.state.image equal to imageURL.
Now to actually displaying the list. Just below the “Favorite” and “Get New Random Image” buttons, add a section titled “Your Favorites”. Underneath, show a list of all the images you favorited. When someone clicks on an image url, have that image show using the viewFavorite function we wrote above. This one might be a little more difficult to code out on your own if you are newer to React and Javascript in general, so don’t feel bad if you have to cheat a little on this one. But see if you can add it yourself before looking at my code.
<h3>Your Favorites</h3> <ul> {this.state.favorites.map(function(value, index) { return ( <li key={index}> <a onClick={this.viewFavorite.bind(this, value)}> {this.state.image === value ? (<span className="dot">·</span>) : ""} {value} </a> </li> ); }, this)} </ul>
To start, I put “Your Favorites” in a h3 tag to have it slightly separated from the buttons and then I start the list.
I’m using the map function to loop through each element in the favorites array. map is a native Javascript method specifically for arrays that lets you loop over each element. The first parameter it takes is a callback function. This function is called for each element in the array. When map runs the callback function for each element, it passes the value of the element as well as the index as parameters to the callback function. So that’s why you see value and index in the function declaration. We are not actually passing those values, it gets done automatically from the map function.
The next bit of code is returning the HTML markup that displays the url to the favorite image. React requires you to add a key attribute to the parent tag whenever you are looping through and displaying an array of items. For the value of the key we are using the index. This can be any value, but it must be unique for each list item so using the index is a safe way to do that.
Next up is the a tag with an onClick attribute to call the viewFavorite function. Though, we haven’t seen this way of calling a function yet. Calling the function this way allows us to both bind the function to the class (like we normally did in the constructor function) and pass parameters to the function we are calling.
The next line is a condition that checks whether value is equal to this.state.image. If it is, show a dot next to the url, otherwise show nothing. This is so when someone actually clicks on a favorite image or randomly stumbles upon it, you’ll be able to see specifically which url in the list we are viewing. Then finally, we are actually outputting the image url by displaying value.
The next bits of code close up the a and li tags. After the callback function we are adding a second parameter to the map function which is this. That basically tells the map function what to use when we call this from within the callback function.
Your render function should now look like this:
render() { return ( <div className="App"> {this.state.image ? (<img src={this.state.image} className="dog-image" />) : ""} { this.state.isLoading ? ( <p> <strong> Loading... </strong> </p> ) : "" } { this.state.favorites.indexOf(this.state.image) === -1 ? ( // We haven't favorited this image <button type="button" onClick={this.saveFavorite} className="btn-orange">Favorite</button> ) : ( // The image is already favorited <button type="button" disabled className="btn-green">Favorited!</button> ) } <button type="button" onClick={this.getRandomDogImage}>Get New Random Image</button> <h3>Your Favorites</h3> <ul> {this.state.favorites.map(function(value, index) { return ( <li key={index}> <a onClick={this.viewFavorite.bind(this, value)}> {this.state.image === value ? (<span className="dot">·</span>) : ""} {value} </a> </li> ); }, this)} </ul> </div> ); }
You should now be able to click on any of the urls in the favorites list and have it load!
Step 5. Update the Styling
Currently the Favorites list is kind of bland. Let’s update the CSS so it looks a little nicer. Again, if you are comfortable with CSS feel free to style it however you’d like. Here is what I came up with:
ul { margin: 0px; padding: 0px; list-style-type: none; } ul li { padding: 10px 0px; } a { cursor: pointer; color: #0080ff; } a:hover { color: #0071e0; } ul a .dot { color: #ff9600; font-size: 65px; margin-right: 3px; vertical-align: top; padding-top: 3px; line-height: 20px; } h3 { margin-top: 40px; margin-bottom: 0px; }
The first definition styles the ul tag. I’m removing any spacing on the outsides by setting the margin and padding to 0px. Then I’m making list-style-type equal to none to remove the standard black dot that appears when using the ul tag to make a list.
Next I’m adding spacing to the top and bottom of each list element with the ul li definition. Then the next two a definitions style how the links will show. I’m making it so a hand cursor is shown and that the links are a blue color.
The next section styles the little dot that appears next to the link when you are viewing one of your favorites. And the last section styles the h3 heading. It essentially is removing the spacing on the bottom of the heading and adding spacing to the top.
Step 6. Fetching your favorites
Up to this point, every time your page reloaded, you lost your list of favorites. When the page loads, we need to make an ajax call to pull the list of already saved favorites. This call is going to be similar to the call we made in Part 1. The url that has our list of favorites is https://dogs.rojas.io/account/[YOUR_ACCOUNT_TOKEN]/favorites. If you go to that url, you will see how the favorites are formatted when returned from the API. It returns an array of objects with the keys: url and created. We will be making a GET request, to that url, so we won’t need any of the extra options like we did when we made the POST request. See if you can write a function that pulls the list of favorites and updates the state to the favorites returned from the API. Here is my code:
getFavorites() { let self = this; fetch("https://dogs.rojas.io/account/[YOUR_TOKEN]/favorites").then(function(response) { return response.json(); }) .then(function(jsonData) { let favoriteImages = []; jsonData.map(function(value, index) { favoriteImages.push(value.url); }); self.setState({ favorites: favoriteImages }) }) .catch(function(err) { console.log(err); }); }
The first line sets a variable for this so we can use it within the promise blocks and map function later on. The next bit of code should look familiar. We make a GET request to the favorites url. In the second promise block we are looping through the jsonData using the map function and adding the url of each favorite to a favoriteImages array. Then we are setting the state of the favorites array eqaul to the favoriteImages array. If there was any errors, we would handle that in the catch block.
Now, if you remember from Part 1, we added a call to getRandomDogImage in the componentDidMount function. We’ll do the same thing with the getFavorites function. We also need to bind the function in the constructor.
Your constructor function should now look like this:
constructor(props) { super(props); this.state = { image: null, isLoading: false, favorites: [] } this.getRandomDogImage = this.getRandomDogImage.bind(this); this.saveFavorite = this.saveFavorite.bind(this); this.getFavorites = this.getFavorites.bind(this); }
Your componentDidMount function should look like this:
componentDidMount() { this.getRandomDogImage(); this.getFavorites(); }
Now when the page refreshes, your list of favorites will still be there!
Step 6. Deleting a favorite
What happens when you favorite something by mistake? Or change your mind? We need to add the ability to delete a favorite! Deleting a favorite will be similar to saving a favorite. It is almost entirely the same except for the request method. When deleting we will change POST to DELETE. The main thing that changes is what we do when handling the result once the favorite is deleted. Instead of adding it to the list we will be removing it. Also, we will pass the url to the favorite as a parameter to our delete function. Try to write it yourself before looking at my code below.
deleteFavorite(imageURL) { let self = this; fetch("https://dogs.rojas.io/account/"+TOKEN+"/favorite", { method: 'DELETE', headers: { 'Content-type': 'application/json' }, body: JSON.stringify({ url: imageURL }) }) .then(function(response) { return response.json(); }) .then(function(jsonData) { if(typeof jsonData.result !== 'undefined' && jsonData.result) { let updatedFavorites = self.state.favorites.slice(); let deletedIndex = updatedFavorites.indexOf(imageURL); if(deletedIndex !== -1) { updatedFavorites.splice(deletedIndex, 1); } self.setState({ favorites: updatedFavorites }); } else { return Promise.reject(jsonData); } }) .catch(function(err) { console.log(err); }); }
Since this is mostly the same as the saveFavorite function, I’m just going to explain whats happening in the second promise block. The first line checks to make sure that the result key is there and that it’s true. If it’s not, we force the catch block to trigger by returning a rejected promise. If the result is there and it’s true, then we remove the url from our favorites state.
The first line makes a copy of the favorites state. The next line finds the index to remove from the favorites array. Then we check if deletedIndex is not -1, meaning that we actually found the url in our favorites array. Then we update the state to our updatedFavorites array.
Now we need to add a way to trigger this delete function. To do that we will add a little × button next to each image url in our favorites list. Try it for yourself and then take a look at my updated favorites list code:
<h3>Your Favorites</h3> <ul> {this.state.favorites.map(function(value, index) { return ( <li key={index}> <a onClick={this.viewFavorite.bind(this, value)}> {this.state.image === value ? (<span className="dot">·</span>) : ""} {value} </a> <a className="delete-favorite" onClick={this.deleteFavorite.bind(this, value)}>×</a> </li> ); }, this)} </ul>
The only thing that changes here is the addition of:
<a className="delete-favorite" onClick={this.deleteFavorite.bind(this, value)}>×</a>
I gave the link a class so we can style it later on and similar to the viewFavorite function, we are calling the deleteFavorite the same way. We are binding the function in the onClick and passing the url as the second parameter.
You should now be able to delete favorites from your list by clicking on the little × next to each url. The last thing to do is style the delete button a little bit. Here is my CSS for the delete button:
ul li a.delete-favorite { color: red; padding-left: 10px; font-weight: bold; cursor: pointer; vertical-align: middle; }
In that block of CSS I’m making the color red, adding some padding to the left side so there’s some slight separation from the image url. Next I’m making it bold, having the cursor change to a hand when you hover over it and making it vertically aligned in the middle .
And that’s it! Our little app can now save and delete our favorite dog images! It’s not the prettiest thing in the world, but hopefully it gave you some good practice with making ajax calls within a React app. If you have any feedback or questions feel free to leave a comment below!