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><Modaltitle="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}><GenerateInputname="title"value={inputs.title}handleInputChange={handleInputChange}/><GenerateInputname="description"value={inputs.description}handleInputChange={handleInputChange}/><GenerateTextInputname="content"value={inputs.content}handleInputChange={handleInputChange}/><GenerateIngredientsnames={['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>);};