Archived student lesson
Add delete button
Part of End to End React with Prisma 2
Objective: Create a delete button for deleting feeds and bundles.
Now that we can create and update feeds and bundles, let’s work on the delete button. Let’s first build out the graphical elements and then we’ll hook it all up to the mutations to make it work.
components/itemDelete.tsx
import { BundleObject, FeedObject, ItemType } from '../utils/types'
import { Delete, Spin } from './svg'
export const ItemDelete = ({
item,
type,
}: {
item: FeedObject | BundleObject
type: ItemType
}) => {
return (
<>
<div className="flex col-span-1 py-2 px-1 z-10">
{false ? (
<Spin className="h-6 w-6 text-gray-500 animate-spin" />
) : (
<Delete className="h-6 w-6 text-red-500" />
)}
</div>
</>
)
}
Then add Delete svg to the svg file:
components/svg.tsx
export const Delete = ({ className }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
/>
</svg>
)
Then we will use this ItemDelete component in the OneListItem component right next to the ItemEdit component that we’ve already made.
components/oneListItem.tsx
import { ItemDelete } from './itemDelete'
// more of the react component
;<div className="col-span-2 flex justify-end">
{canManipulate ? (
<ItemEdit
item={item}
type={type}
selected={selected}
setSelected={setSelected}
/>
) : null}
{canManipulate ? <ItemDelete item={item} type={type} /> : null}
</div>
Now we should see a delete button but it doesn’t work yet. The operation of the button will be as follows: a user clicks on the button, it pops up a modal dialog box which prompts them to confirm that they want to delete the item in question. If they click confirm it gets deleted otherwise if they click cancel, the dialog box goes away with no change made.
Let’s now build the dialog button. We’ll use a state react hook for managing the boolean that controls whether it is showing or not. We’ll also take advantage of some lovely styling with Tailwind to make sure this modal looks in tip-top shape. You’ll see a lot of classes set for a variety of divs and buttons, but this is only for visually setting all the details- it is all cosmetic.
components/itemDelete.tsx
import { useState } from 'react'
import { BundleObject, FeedObject, ItemType } from '../utils/types'
import { Delete, Spin } from './svg'
export const ItemDelete = ({
item,
type,
}: {
item: FeedObject | BundleObject
type: ItemType
}) => {
const isFeed = type === ItemType.FeedType
const __typename = isFeed ? 'Feed' : 'Bundle'
const [modalVisibility, setVisibility] = useState(false)
return (
<>
{modalVisibility ? (
<div className="fixed z-20 inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 transition-opacity">
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen"></span>
<div
className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<h3 className="text-lg font-medium">
Are you sure you want to delete this{' '}
{isFeed ? 'feed?' : 'bundle?'}
</h3>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<span className="flex w-full rounded-md sm:ml-3 sm:w-auto">
<button
onClick={e => {
e.preventDefault()
setVisibility(false)
}}
type="button"
className="font-bold py-2 px-6 rounded text-white bg-red-500 text-white w-full"
>
Delete
</button>
</span>
<span className="mt-3 flex w-full rounded-md sm:mt-0 sm:w-auto">
<button
onClick={e => {
e.preventDefault()
setVisibility(false)
}}
type="button"
className="font-bold py-2 px-6 rounded bg-white text-gray-700 rounded border-4 border-gray-300 w-full"
>
Cancel
</button>
</span>
</div>
</div>
</div>
</div>
</div>
) : null}
<div
onClick={e => {
e.preventDefault()
setVisibility(true)
}}
className="flex col-span-1 py-2 px-1 z-10"
>
{false ? (
<Spin className="h-6 w-6 text-gray-500 animate-spin" />
) : (
<Delete className="h-6 w-6 text-red-500" />
)}
</div>
</>
)
}
You can see that the div which has the Delete component now has an onClick handler which will set the visibility to true. If it is true, then we show the whole modal dialog box which has two buttons on it Delete and Cancel. Right now either button will set the visibility to false which makes the dialog box go away- but we will now load down the Delete button with a mutation that will actuall execute the mutation.
The first step is to add a deleteItemMutation handler. We also want to use the useFetchUser hook so we can be sure that we won’t let someone click the delete button if they aren’t a user or if their information is loading.
components/itemDelete.tsx
import { useMutation } from '@apollo/client';
import { useState } from 'react';
import { DELETE_BUNDLE_MUTATION, DELETE_FEED_MUTATION } from '../utils/api/graphql/mutations';
import { BUNDLES_QUERY, FEEDS_QUERY } from '../utils/api/graphql/queries';
import { BundleObject, FeedObject, ItemType } from '../utils/types';
import { useFetchUser } from '../utils/user';
import { Delete, Spin } from './svg';
export const ItemDelete = ({ item, type }: { item: FeedObject | BundleObject; type: ItemType }) => {
const isFeed = type === ItemType.FeedType;
const [modalVisibility, setVisibility] = useState(false);
const [deleteItemMutation, { loading: deleteItemLoading }] = useMutation(isFeed ? DELETE_FEED_MUTATION : DELETE_BUNDLE_MUTATION);
const { user, loading } = useFetchUser();
// more of the react component
<button
onClick={e => {
e.preventDefault();
deleteItemMutation({
variables: {
data: {
id: item.id,
},
},
optimisticResponse: {
__typename: 'Mutation',
[`delete${__typename}`]: {
id: item.id,
__typename,
},
},
update: (store, { data: { deleteFeed, deleteBundle } }) => {
try {
const data = store.readQuery({ query: isFeed ? FEEDS_QUERY : BUNDLES_QUERY });
const currentItems = data[isFeed ? 'feeds' : 'bundles'];
const deleteItem = isFeed ? deleteFeed : deleteBundle;
const updatedArray = currentItems.filter(o => o.id !== deleteItem.id);
const newData = { [isFeed ? 'feeds' : 'bundles']: updatedArray };
store.writeQuery({ query: isFeed ? FEEDS_QUERY : BUNDLES_QUERY, data: newData });
} catch (e) {}
},
});
setVisibility(false);
}}
type="button"
className="font-bold py-2 px-6 rounded text-white bg-red-500 text-white w-full"
>
Delete
</button>
// more of the react component
{deleteItemLoading || loading || !user ? (
<Spin className="h-6 w-6 text-gray-500 animate-spin" />
) : (
<Delete className="h-6 w-6 text-red-500" />
)}
Our deleteItemMutation takes the id of the item and we handle the response by filtering out the item from the list that we get from running a feeds/bundles query. We can also run an easy optimisticResponse by simply returning the deleted item’s id and __typename.
Now create some feeds and bundles and watch that when you delete them, they delete almost instantaneously- sweet!