« Previous Tutorial Next Tutorial »

In this tutorial we’re going to add functionality to those “remove from my list” buttons that we added last time. Well, functionality beyond console-logging, anyway, since the vast majority of users wouldn’t find that very useful. To do that, we’re going to need to create two API routes, some actions, some reducers, and some new code for our components. Busy as always! Let’s get started.

Open /routes/api/albums.js and head for line 142, which ends our getArtists method. Below it, add a padding line and the following route code:

// POST to /delete
router.post('/delete', (req, res, next) => {
  User.findOne({ username: req.user.username }, (err, foundUser) => {
    // Run filter against the array, returning only those that don't match the passed ID
    const newAlbums = foundUser.albums.filter(album => album !== req.body.albumId);
    foundUser.update({ $set: { albums: newAlbums } }, (error) => {
      if (error) {
        return res.json(JSON.stringify({ error: 'There was an error removing the album from the user\'s profile' }));
      }
      return res.json({ albums: newAlbums });
    });
  });
});

We use a nice built-in method for JavaScript Arrays here, Array.filter, which lets you designate a function to check each value against, and builds a new array only out of those values which your function returns. So in this case we run through the user’s list of album IDs and return all of them except the one we’re trying to delete. Then we update the user in the database with the new, truncated list of albums.

That’s it here. Save the file, open /routes/api/artists.js, find line 84, and below it add the same exact thing except, well, for artists. Here’s the code:

// POST to /delete
router.post('/delete', (req, res, next) => {
  User.findOne({ username: req.user.username }, (err, foundUser) => {
    // Run filter against the array, returning only those that don't match the passed ID
    const newArtists = foundUser.artists.filter(artist => artist !== req.body.artistId);
    foundUser.update({ $set: { artists: newArtists } }, (error) => {
      if (error) {
        return res.json(JSON.stringify({ error: 'There was an error removing the artist from the user\'s profile' }));
      }
      return res.json({ artists: newArtists });
    });
  });
});

Save this file and we’re done on the backend. Now we move on to actions, so open up /src/actions/albums.js. Below line 7 add these two new action creators:

export const albumDeleteFailure = error => ({ type: 'MUSIC_ALBUM_DELETE_FAILURE', error });
export const albumDeleteSuccess = json => ({ type: 'MUSIC_ALBUM_DELETE_SUCCESS', json });

And then add a padding line after line 56. We’re going to create a deleteAlbum function which is also going to repopulate the albums in the Store once it runs, in order to keep what we’re seeing on the page in line with what’s happening at the database level. Remember: the only thing stored in the user data is a list of album or artist IDs. We have to run a populate with those IDs to get a list full of actual information.

Here’s the function:

// Delete an album from user's list
export function deleteAlbum(albumId) {
  return async (dispatch) => {
    // clear the error box if it's displayed
    dispatch(clearError());

    // turn on spinner
    dispatch(incrementProgress());

    // Hit the API
    await fetch(
      '/api/albums/delete',
      {
        method: 'POST',
        body: JSON.stringify({ albumId }),
        headers: {
          'Content-Type': 'application/json',
        },
        credentials: 'same-origin',
      },
    )
    .then((response) => {
      if (response.status === 200) {
        return response.json();
      }
      return null;
    })
    .then((json) => {
      if (!json.error) {
        dispatch(populateAlbums(json.albums)); // eslint-disable-line
      }
      return json;
    })
    .then((json) => {
      if (!json.error) {
        return dispatch(albumDeleteSuccess(json));
      }
      return dispatch(albumDeleteFailure(new Error(json.error)));
    })
    .catch(error => dispatch(albumDeleteFailure(new Error(error))));

    // turn off spinner
    return dispatch(decrementProgress());
  };
}

As you can see, we have an extra .then in which we run the album population and just pass the json along down the chain to be handled … unless there’s an error, in which case we don’t bother to repopulate because nothing has changed. Also I disabled ESLint on that one line because it was complaining about populateAlbums coming after this function, but I prefer alphabetical order in my actions.

Save this file and open /src/actions/artists.js. Time to do the same exact thing here, except with artists. Start by adding these two action creators below line 7:

export const artistDeleteFailure = error => ({ type: 'MUSIC_ARTIST_DELETE_FAILURE', error });
export const artistDeleteSuccess = json => ({ type: 'MUSIC_ARTIST_DELETE_SUCCESS', json });
```

Then you’re adding the code below line 56 again, and here it is:

// Delete an artist from user's list
export function deleteArtist(artistId) {
  return async (dispatch) => {
    // clear the error box if it's displayed
    dispatch(clearError());

    // turn on spinner
    dispatch(incrementProgress());

    // Hit the API
    await fetch(
      '/api/artists/delete',
      {
        method: 'POST',
        body: JSON.stringify({ artistId }),
        headers: {
          'Content-Type': 'application/json',
        },
        credentials: 'same-origin',
      },
    )
    .then((response) => {
      if (response.status === 200) {
        return response.json();
      }
      return null;
    })
    .then((json) => {
      if (!json.error) {
        dispatch(populateArtists(json.artists)); // eslint-disable-line
      }
      return json;
    })
    .then((json) => {
      if (!json.error) {
        return dispatch(artistDeleteSuccess(json));
      }
      return dispatch(artistDeleteFailure(new Error(json.error)));
    })
    .catch(error => dispatch(artistDeleteFailure(new Error(error))));

    // turn off spinner
    return dispatch(decrementProgress());
  };
}

Not much to say that hasn’t already been said, so let’s move on! Save the file, and let’s do some reducing. Open /src/reducers/error.js first. Below line 13, add this code:

    case 'MUSIC_ALBUM_DELETE_FAILURE':

and below line 17, add this:

    case 'MUSIC_ARTIST_DELETE_FAILURE':

Save the file, and open /src/reducers/list.js. We need to catch our delete actions here, so below line 10, add this code:

    case 'MUSIC_ALBUM_DELETE_SUCCESS': {
      const newState = Object.assign({}, state);
      newState.albums = action.json.albums;
      return newState;
    }

And below line 25, add this:

    case 'MUSIC_ARTIST_DELETE_SUCCESS': {
      const newState = Object.assign({}, state);
      newState.artists = action.json.artists;
      return newState;
    }

Good! Rolling along. Save this file, and open /src/reducers/user.js. Remove the opening brace from line 15 and below it add this line:

    case 'MUSIC_ALBUM_DELETE_SUCCESS': {

Then remove the opening brace from line 21 and below it add this code:

    case 'MUSIC_ARTIST_DELETE_SUCCESS': {

While we’re here, let’s remove the spaces from the empty objects in lines 17 and 23. No particular reason … I just think it’s more aesthetically pleasing. Plus it matches up with all of our other code. Consistency is a good thing!

Save the file, and let’s make rocket go … open up /src/components/list/ListPageContainer.jsx. Import our new functions by changing lines 4 and 5 to this:

import { deleteAlbum, populateAlbums } from '../../actions/albums';
import { deleteArtist, populateArtists } from '../../actions/artists';

After that, head for line 61 and map those dispatches right to props, by adding this code just below the line. 61, that is.

  deleteAlbumFunction: deleteAlbum,
  deleteArtistFunction: deleteArtist,

Now that we have some proper props, head up to line 45, and make it look like this:

    const { authentication, deleteAlbumFunction, deleteArtistFunction, list } = this.props;

And then pass those funky functions—sorry, I really don’t know what’s wrong with me today—on down to our child component by adding these two lines below line 54:

        deleteAlbumFunction={deleteAlbumFunction}
        deleteArtistFunction={deleteArtistFunction}

All good here. Save the file, and open /src/components/list/ListPage.jsx. The main thing we’re doing here is making our delete function actually do what we want it to, so replace lines 16 to 18 with the following code:

  deleteItem(id, type) {
    const { deleteAlbumFunction, deleteArtistFunction } = this.props;
    if (type === 'album') {
      deleteAlbumFunction(id);
    }
    if (type === 'artist') {
      deleteArtistFunction(id);
    }
  }

And by “main thing” I actually meant “only thing.” We’re done. Save the file and head for a browser, refresh the page, make sure you’re logged in, and view your list. If you don’t have any albums or artists added, for some reason, go add some. Otherwise, remove a few to see what happens. It should work exactly as expected, which is how we like it!

In the next tutorial, we’re going to take a short break from writing component code to upgrade to the latest and greatest Node and React, as well as cleaning up our woefully neglected package.json file a bit. See you there!

« Previous Tutorial Next Tutorial »