Frontend Serverless with React and GraphQL

Start at the beginning and work your way through this project. The code for each step as well as the finished project can be found in the Github repository.


39. Add Delete Recipe Button

Objective: Allow a user to delete their own recipes

We will now create a delete recipe button. How it will work is that when the button is clicked, it will pop up a modal dialog box and ask for them to confirm the delete. If they press confirm it will run a delete mutation and then after it finishes, we will re-direct them back to the my-recipes page.

Everything is pretty similar to the other mutations we've used except for the modal. How the modal works is it is a separate react component that sits next to the button in a hidden state. We create functions called handleHide and handleShow which will toggle a state variable called isModalVisible to be true or false. We set the visibility of the modal based on that state variable.

components/DeleteButton.tsx

import { Button, Modal } from 'antd';
import { useMutation } from '@apollo/react-hooks';
import { deleteRecipeGraphQL } from '../graphql/mutations/deleteRecipe';
import { useState } from 'react';
import { recipesGraphQL } from '../graphql/queries/recipes';
import Router from 'next/router';
export const DeleteButton = ({
id,
disabled,
}: {
id: string;
disabled: boolean;
}) => {
const [deleteRecipeMutation, { loading: deleteRecipeLoading }] = useMutation(
deleteRecipeGraphQL,
);
const [isModalVisible, setModalVisibility] = useState(false);
const handleOk = async () => {
if (!deleteRecipeLoading) {
await deleteRecipeMutation({
refetchQueries: [{ query: recipesGraphQL }],
variables: {
where: { id },
},
});
}
setModalVisibility(false);
Router.replace('/my-recipes');
};
const handleShow = () => setModalVisibility(true);
const handleHide = () => setModalVisibility(false);
return (
<>
<Button block type="danger" disabled={disabled} onClick={handleShow}>
Delete Recipe
</Button>
<Modal
title="Confirm Delete"
visible={isModalVisible}
onOk={handleOk}
onCancel={handleHide}
>
<p>Are you sure that you want to delete this recipe?</p>
</Modal>
</>
);
};

Now that we've created the button, let's import it into the <UpdateRecipe> component. We will make sure to disable it either when the recipe query itself is loading or when the delete mutation is currently firing to prevent accidental double clicking of the delete button.

components/UpdateRecipe.tsx

import { useQuery, useMutation } from '@apollo/react-hooks';
import { recipeGraphQL } from '../graphql/queries/recipe';
import { submitForm } from '../utils/submitForm';
import { useState } from 'react';
import * as _ from 'lodash';
import { Form, Row, Col, Button } from 'antd';
import { GenerateInput, GenerateTextInput } from './GenerateFields';
import { GenerateIngredients } from './GenerateIngredients';
import { Loading } from './notify/Loading';
import { createUpdateObj } from '../utils/createUpdateObj';
import { updateRecipeGraphQL } from '../graphql/mutations/updateRecipe';
import { DeleteButton } from './DeleteButton';
export const UpdateRecipe = ({ id }) => {
const { loading: isQueryLoading, data, error } = useQuery(recipeGraphQL, {
variables: { where: { id } },
});
const [updateRecipeMutation, { loading: updateRecipeLoading }] = useMutation(
updateRecipeGraphQL,
);
const [recipeState, setRecipeState] = useState({ isQueryLoading: true });
const initiateUpdateRecipe = () => {
const updateObj = createUpdateObj(data, inputs);
return updateRecipeMutation({
refetchQueries: [{ query: recipeGraphQL, variables: { where: { id } } }],
variables: {
data: {
...updateObj,
},
where: { id },
},
});
};
const {
inputs,
handleInputChange,
handleAddIngredient,
handleDeleteIngredient,
handleDropdownChange,
handleUpdate,
setInputs,
} = submitForm(
{
title: '',
description: '',
content: '',
ingredients: [],
},
initiateUpdateRecipe,
);
if (!isQueryLoading && recipeState.isQueryLoading) {
const { __type, ...loadedRecipe } = _.get(data, 'recipe');
setInputs((state) => ({ ...state, ...loadedRecipe }));
setRecipeState((state) => ({ ...state, isQueryLoading }));
}
if (!data) return <Loading />;
const disabled = isQueryLoading || updateRecipeLoading;
return (
<Form onFinish={handleUpdate}>
<GenerateInput
name="title"
value={inputs.title}
handleInputChange={handleInputChange}
/>
<GenerateInput
name="description"
value={inputs.description}
handleInputChange={handleInputChange}
/>
<GenerateTextInput
name="content"
value={inputs.content}
handleInputChange={handleInputChange}
/>
<GenerateIngredients
names={['amount', 'unit', 'type']}
values={inputs.ingredients}
handleAddIngredient={handleAddIngredient}
handleDeleteIngredient={handleDeleteIngredient}
handleInputChange={handleInputChange}
handleDropdownChange={handleDropdownChange}
/>
<Row>
<Col span={16} />
<Col span={4}>
<Form.Item label="Action">
<Button block disabled={disabled} type="primary" htmlType="submit">
Update Recipe
</Button>
<DeleteButton id={id} disabled={disabled} />
</Form.Item>
</Col>
</Row>
</Form>
);
};