Hoot Front-End Delete a Hoot
Learning objective: By the end of this lesson, students will be able to implement the functionality for deleting a hoot.
Overview
In this lesson, we’ll implement the following user story:
👤 As the author of a hoot, I should see a button to ‘Delete’ a hoot on the ‘Details’ page. Clicking the button should delete the hoot and redirect me back to the ‘List’ page.
When implementing delete functionality, it’s important to ensure that only the author of a given resource can delete it. Our application should take measures to prevent users from deleting hoots that do not belong to them.
We should address this need in both the back-end and front-end. We’ve already included a check for this in our back-end app:
// controllers/hoots.js
if (!hoot.author._id.equals(req.user._id)) {
return res.status(403).send("You're not allowed to do that!");
}
In this lesson, we will focus on restricting access in the front-end app.
Based on our user story, we’ll need to conditionally render the delete button based on the author of the hoot. We can accomplish this using the UserContext. This makes the signed-in user object easily accessible throughout our component tree. We’ll make use of this user object when we render the delete button in the HootDetails component.
Build the UI
-
At the top of the
HootDetailscomponent, add imports for importUserContextanduseContext:// src/components/HootDetails/HootDetails.jsx // Add useContext to the existing import statement for react import { useState, useEffect, useContext } from 'react'; import { useParams } from 'react-router'; import CommentForm from '../CommentForm/CommentForm'; import * as hootService from '../../services/hootService'; // Import the UserContext import { UserContext } from '../../contexts/UserContext'; -
Within the component function, create the following
userconstant:// src/components/HootDetails/HootDetails.jsx const HootDetails = () => { const { hootId } = useParams(); // Access the user object from the UserContext const { user } = useContext(UserContext); const [hoot, setHoot] = useState(null); // useEffect, handleAddComment, and return statements here };Time to add some conditional rendering for our button.
For our conditional rendering, we’ll use the Logical AND ( && ) operator.
If the
hoot.author._idmatchesuser._id, this piece of UI should be visible. If not, the UI should not be rendered. This means only the author of this particularhootcan access the UI to update or delete ahoot. -
Modify the contents of the
<header>in the main return for theHootDetailscomponent to conditionally render the delete button:// src/components/HootDetails/HootDetails.jsx <header> <p>{hoot.category.toUpperCase()}</p> <h1>{hoot.title}</h1> <p> {`${hoot.author.username} posted on ${new Date(hoot.createdAt).toLocaleDateString()}`} </p> {/* Add the following */} {hoot.author._id === user._id && ( <> <button>Delete</button> </> )} </header>💡 Notice the use of a React fragment (
<> </>) here. While we don’t need a fragment now, we’ll add another element alongside the delete button soon.
Build the handleDeleteHoot() function
-
Stub up the
handleDeleteHoot()function in theAppcomponent:// src/App.jsx const handleDeleteHoot = async (hootId) => { console.log('hootId', hootId); }; -
Next, pass the function down to
HootDetails:// src/App.jsx <Route path='/hoots/:hootId' element={<HootDetails handleDeleteHoot={handleDeleteHoot}/>} /> -
The
HootDetailscomponent will need to receive thehandleDeleteHoot()function as a prop. It currently doesn’t have any props, so let’s add thepropsparameter to the component function:// src/components/HootDetails/HootDetails.jsx // The HootDetails component function needs to receive props const HootDetails = (props) => { // HootDetails function code here }; -
In the
HootDetailscomponent, let’s update the delete button we added earlier. We’ll attach anonClickevent handler that triggers theprops.handleDeleteHoot(hootId)function when the button is clicked.Update your button with the following:
// src/components/HootDetails/HootDetails.jsx {hoot.author._id === user._id && ( <> {/* Modify the button */} <button onClick={() => props.handleDeleteHoot(hootId)}> Delete </button> </> )}🚨 Be sure to pass in
hootIdas an argument when you call the function. We won’t know which hoot to delete without it. -
In your browser, try deleting a hoot. You should see a
console.log()originating fromApp.jsxconfirming that thehootIdis being passed up the component tree. -
With the
hootIdaccessible inhandleDeleteHoot(), let’s confirm that we canfilter()state using this value:// src/App.jsx const handleDeleteHoot = async (hootId) => { console.log('hootId', hootId); setHoots(hoots.filter((hoot) => hoot._id !== hootId)); navigate('/hoots'); };Remember, the array’s filter() method returns a shallow copy of the array, excluding all elements that do not pass the test implemented by the provided callback function.
In the code block above, our
filter()method returns only thehootobjects whose_idvalues do not match thehootId. This effectively excludes the hoot we want to delete from the array used to update state.
Try deleting a hoot. After clicking the delete button, you should be redirected to the list page where the hoot is no longer visible. However, if you refresh the browser, you’ll see the hoot reappear. This happens because we are currently only managing local state.
No changes have been made to the database, so when the browser refreshes, hootService.index() runs again, loading hoots from the database.
Managing local state is useful for providing immediate visual updates. However, for changes to persist beyond the current session, we need to update both the local state and the database. We’ll address this in the next step!
Build the service function
Let’s finish up our delete functionality by adding the deleteHoot() service function to the hoot service:
// src/services/hootService.js
const deleteHoot = async (hootId) => {
try {
const res = await fetch(`${BASE_URL}/${hootId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
return res.json();
} catch (error) {
console.log(error);
}
};
export {
index,
show,
create,
createComment,
// Add export:
deleteHoot,
};
Call the service
Now that we have our service function, we’ll add it to handleDeleteHoot(), along with one other minor change.
In our back-end, you might recall that the delete hoot controller function responds with a deletedHoot:
res.status(200).json(deletedHoot);
If we call hootService.deleteHoot(), what we get back is this deletedHoot object:
const deletedHoot = await hootService.deleteHoot(hootId);
The deletedHoot object contains the _id (ObjectId) of the hoot removed from the database. With this in mind, when we use the filter() method inside handleDeleteHoot(), we can use deletedHoot._id instead of the current hootId.
This approach assures us that the deletion was successfully processed on the back-end and database before we update the front-end.
Back in src/App.jsx, update handleDeleteHoot() with the following:
// src/App.jsx
const handleDeleteHoot = async (hootId) => {
const deletedHoot = await hootService.deleteHoot(hootId);
// Filter state using deletedHoot._id:
setHoots(hoots.filter((hoot) => hoot._id !== deletedHoot._id));
navigate('/hoots');
};
Try it out! You should now be able to delete hoots.