React - Delete
In this video we are going to implement the DELETE
functionality in our React CRUD application.
There are two steps to this process:
- We must send a
DELETE
request to our API - in order to actually remove the blog post from the system / database - 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 :)