React - Delete

This video is available to view for members only.

Click here to Join!

Already a member?

Login


In this video we are going to implement the DELETE functionality in our React CRUD application.

There are two steps to this process:

  1. We must send a DELETE request to our API - in order to actually remove the blog post from the system / database
  2. We must update the front end - without refreshing - in order to remove the blog post from the in-memory / local collection

We won't need a route for deleting blog posts. Instead, we will call a function that triggers the DELETE request, and then filters the deleted item from the in-memory collection.

Because we don't have a route, the first step is to figure out where this code would best be placed.

In my opinion, I believe this code is best placed on the List container. If the system were to grow further in size, maybe I would consider placing the delete logic into its own separate file. For now, that would be overkill.

The way in which we will implement this functionality is very similar to how we have seen in the create and update videos, whereby we declare a function inside our list container, and pass this function down to the Table component via props.

When the end user clicks on the Delete button, this will call a function local to the Table component, which in turn will call the function that has been passed in from 'above'.

If you are unfamiliar with this, I'd strongly recommend watching the previous two videos, where this concept is covered more thoroughly. In this video I am going to assume you have the pre-requisite knowledge - but again, if not, watch the previous two videos and / or read the write-ups.

The Delete Button

The first thing I am going to do is change the current <a href ... >Delete</a> link for a button:

/src/components/Table.js#L35

<btn onClick={this.deleteHandler.bind(this, i)} className="btn btn-danger btn-sm">Delete</btn>

This is going to require that we have a method on our Table class called deleteHandler, so let's also define that:

/src/components/Table.js

    deleteHandler(i, e) {
        e.preventDefault();
        this.props.onDelete(this.props.blogPosts[i].id);
    };

There's some interesting things happening here. Let's cover them in turn.

On Click

<btn onClick={this.deleteHandler.bind(this, i)}

We declared a button element which should run a function when clicked (onClick).

The first question I would have here is : why do we need to bind to this ? Why can't we just write:

<btn onClick={this.deleteHandler(i)} className="btn btn-danger btn-sm">Delete</btn>

Here's a fun one. If you try this, as soon as you load the page with the Table component, it will invoke the deleteHandler, and should your logic be correct, it will go ahead and delete every single post in your database. Nice.

This is perhaps the most confusing part of this whole setup.

What's happening here - to the best of my knowledge - is not a React 'problem'. It's not a problem at all. It's how JavaScript works!

Here's how I read it:

onClick={this.deleteHandler(i)}

On click, please run the method in this class called deleteHandler, and pass in the i variable.

Here's how JavaScript reads it:

onClick={this.deleteHandler(i)}

Call the method in this class called deleteHandler, and pass in the i variable. Then pass this value to the onClick function.

Yikes.

We can use bind to partially apply the function:

onClick={this.deleteHandler.bind(this, i)}

Which in turn will not actually run the function, but will pass a reference to the onClick handler of the as-yet-unrun function.

Subtle, but important.

We could also use an ES6 arrow function to achieve the same thing, in what I consider to be a more friendly way:

onClick={() => this.deleteHandler(i)}

Because in this format we are returning an anonymous function, which when invoked, will call this.deleteHandler(i).

Again, subtle, but hopefully not so crazy, once you understand why.

Delete Handler

The deleteHandler method is much more basic by comparison.

We want to preventDefault so as not to have a nasty jump-to-the-top-of-the-page-when-clicked experience.

Then, we use the passed in props to figure out what needs to happen.

The thing is, our Table is going to have a zero-indexed list of blog posts. This won't directly map to the ID of the blog post on the API.

Fortunately, we can take the passed in index (i), then use this to find the blog post object that is in our this.props.blogPosts array at that index, and from that object we can get the ID that will correspond to the blog post on our API.

We then pass this ID to the onDelete function - also passed in via props.

Defining The onDelete Function

We haven't actually defined the onDelete function that is expected to be passed in via props, so let's sort that out also:

// /src/containers/blogPosts/list.js

import React, { Component } from 'react';
import {fetchBlogPosts, deleteBlogPost} from '../../actions/blogPostActions';
import Table from '../../components/Table';

export default class List extends Component {

    // * snip *

    onDelete(id) {
        deleteBlogPost(id)
            .then((data) => {
                let blogPosts = this.state.blogPosts.filter((post) => {
                    return id !== post.id;
                });

                this.setState(state => {
                    state.blogPosts = blogPosts;
                    return state;
                });
            })
            .catch((err) => {
                console.error('err', err);
            });
    }

    render() {
        return (
            <div>
                <Table blogPosts={this.state.blogPosts}
                       onDelete={this.onDelete.bind(this)}
                />
            </div>
        );
    }
}

Again, plenty going on here, so let's review.

Firstly, we need to import the deleteBlogPost function from our blogPostActions. That's not defined yet, so let's do so:

// /src/actions/blogPostActions.js

export function deleteBlogPost(id) {
    return fetch('http://api.symfony-3.dev/app_dev.php/posts/' + id, {
        method: 'DELETE',
        mode: 'CORS'
    }).then(res => res)
    }).catch(err => err);
}

Really straightforward, send in a HTTP DELETE request to our API passing in the id to be deleted. If it goes well we expect a 204 - no content response.

Back to our list container:

onDelete(id) {

We've declared our method, and we expect to be given an id parameter.

As soon as we have this id field we can make the 'real' request to the API, via that newly imported deleteBlogPosts function.

If that all blows up then we want to catch the error and log it into the console.

If it goes well then we need to filter out the deleted blog post from the in-memory collection. We can do this with the filter method which is available on this.state.blogPosts as this will be a JavaScript array, so we get filter on the prototype:

let blogPosts = this.state.blogPosts.filter((post) => {
    return id !== post.id;
});

The filter function expects to be given a function as the first argument. That function will be given the element, index, and array as its three arguments. These arguments are passed in as part of the call - you don't need to worry about providing them, or how they get there. This confused me like crazy when first encountering the filter function.

In this instance, post would be our element, and as I'm not using the other two, I don't bother to explicitly define them on the function.

Once we have the element / post, we can run a simple function - called a predicate - which must return either true or false.

The filter function will run through every item in the array (this.state.blogPosts, in our case), and run this predicate function on a per-item basis.

If the function returns true, we want to keep the item.

If the function returns false, we want to discard the item.

The result is an array that contains only the things that we wanted to keep.

In our case we say we only want to keep anything item that has an id that did not match the id we just deleted. In other words, keep every item that wasn't the one we just asked to delete.

We must then explicitly update the state, or React won't re-render the Table for us:

this.setState(state => {
    state.blogPosts = blogPosts;
    return state;
});

This ensures everything behaves as expected. But we must explicitly pass this new function down to the Table component, and we do this as a prop:

    render() {
        return (
            <div>
                <Table blogPosts={this.state.blogPosts}
                       onDelete={this.onDelete.bind(this)}
                />
            </div>
        );
    }

In this case we bind to this to ensure that the value of this when onDelete is called from the Table component actually refers to the List container, and not the Table component.

And that is the gist of the Delete functionality.

In truth, this is one of those pieces of code that needs to be seen and played with to start making more sense. So I strongly advise you to clone the code and have a play around.

Feel free to ask questions, or leave feedback on how I could improve this implementation - I'm always keen to learn, but this definitely does work... which is something :)

Code For This Episode

Get the code for this episode.

Share This Episode

If you have found this video helpful, please consider sharing. I really appreciate it.


Episodes in this series

# Title Duration
1 Intro with Twig CRUD - List 06:03
2 Twig CRUD - Create 03:25
3 Twig CRUD - Update (Edit) 02:43
4 Twig CRUD - Delete 02:36
5 Simple Symfony 3 RESTful API Setup 05:32
6 API - GET a Single BlogPost 04:51
7 API - GET a Collection of BlogPosts 02:20
8 API - POST to Create New Data 06:30
9 API - PUT and PATCH to Update 04:11
10 API - DELETE 03:34
11 Angular - Setup, Styling, and GET All 09:19
12 Angular - Refactoring 10:23
13 Angular - Create (POST) 06:54
14 Angular - Update (PUT) 08:41
15 Angular - Remove (DELETE) 06:44
16 React - Intro, Setup, and GET all 06:54
17 React - Refactoring 09:14
18 React Router 04:41
19 React - Create (POST) 13:13
20 React - Update (PUT / PATCH) 12:40
21 React - Delete 11:00
22 React - Tidy Up, and Finish 04:14