2024-05-27
We will be exploring how GraphQL mutattions work and I will be doing so by adding a button that will use the useAcceptInvitationMutation function.
Blog post by MF Mabala
Accepting an Invitation with a GraphQL mutation
Accepting an Invite with GraphQL Mutations
In this blog we will be doing a couple of tasks so you will need to have knowledge on react and typescript. Below are things that we will be creating.
- Accept Invite Mutation
- Accept Invitation hook
- Accept Invite Dialog
- Accept Invite Component
- The Invitation page
Writing a mutation.
With everything that is written in this blog there is a backend to support it. Please see my previous blog to be able to write the api schema. This mutation that I am about to show you is for the APP(frontend). The file is named invitation.graphql
mutation AcceptInvitation(
$id: ID!
$firstName: String!
$lastName: String!
) {
acceptInvitation(
id: $id
firstName: $firstName
lastName: $lastName
) {
status
}
}
The above code represents the Accept Invitation Mutation. The first round brackets represent the arguments that the mutation takes. We return the status from the mutation.
After writing the mutation you will run the following commands. Open your terminal and run the relevant command to start up your server. The command depends on how you set up your project . For my project I run
yarn dev
After the server has started running open another terminal and run
yarn schema
The commands will generate a schema.graphql file for you and also generate code for the mutation. If you search useAcceptInvitationMutation in the generated file you should be able to see it. We will be using the generated file to write code in the other files.
Accept Invitation hook
To be able to understand the hooks you should know react and typescript . I will guide you step by step. Name the file accept-invitation.tsx.
lets start by creating the function
export function useAcceptInvitation({
}){
}
The dialog will contain a form where the user is able to edit the details and and accept the invitation. Next we will add the form schema that we will use and import the useAcceptInvitation to be able to use it. Remember it should be from the generated file.
import { useAcceptInvitationMutation } from '@gen/graphql';
import * as z from 'zod';
const formSchema = z.object({
email: z.string().email(),
firstName: z.string().min(2),
lastName: z.string().min(2),
});
export function useAcceptInvitation({}){
const [acceptInvitation] = useAcceptInvitationMutation();
}
The useAcceptInvitationMutation takes in the accept invitation. The form schema contains all the variables or parameters you want to show on your form or want to use in your project.
Now lets define the props and use them accordingly.
import { useAcceptInvitationMutation } from '@gen/graphql';
import * as z from 'zod';
const formSchema = z.object({
firstName: z.string().min(2),
lastName: z.string().min(2),
});
interface UseAcceptInvitationProps {
onCompleted: () => void;
firstName: string;
lastName: string;
id: string;
}
export function useAcceptInvitation({
onCompleted,
firstName,
lastName,
id,
}): UseAcceptInvitationProps {
const [acceptInvitation] = useAcceptInvitationMutation();
}
We will be using a form .We need to add the from constant that takes the form Schema and displays the values . We will also need to add a onSubmit function that handles the submission of the form.
import { useAcceptInvitationMutation } from '@gen/graphql';
import * as z from 'zod';
const formSchema = z.object({
firstName: z.string().min(2),
lastName: z.string().min(2),
});
interface UseAcceptInvitationProps {
onCompleted: () => void;
firstName: string;
lastName: string;
id: string;
}
export function useAcceptInvitation({
onCompleted,
firstName,
lastName,
id,
}): UseAcceptInvitationProps {
const [acceptInvitation] = useAcceptInvitationMutation();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
mobileNumber,
firstName,
lastName,
email,
},
});
const onSubmit = useCallback(
(values: z.infer<typeof formSchema>) => {
acceptInvitation({
variables: {
id,
firstName: values.firstName,
lastName: values.lastName,
mobileNumber: values.mobileNumber,
},
onCompleted,
},
});
},
[acceptInvitation, id, onCompleted]
);
}
The reason we use useCallback function is that it lets us memoize a callback function by preventing it from being recreated on every render. This means it caches the call Back function so that it is not defined with every render.
The onSubmit function is saying when we accept our invitation, the information in our form must be the one defined in the formSchema hence they are described as variables. Then After a user accepts an invitation complete the action.
Now that we have all we need we have to to return some data in our function so that we are able to use the hook on other pages.
import { useAcceptInvitationMutation } from '@gen/graphql';
import * as z from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useCallback} from 'react';
import { useForm } from 'react-hook-form';
const formSchema = z.object({
firstName: z.string().min(2),
lastName: z.string().min(2),
});
interface UseAcceptInvitationProps {
onCompleted: () => void;
firstName: string;
lastName: string;
id: string;
}
export function useAcceptInvitation({
onCompleted,
firstName,
lastName,
id,
}): UseAcceptInvitationProps {
const [acceptInvitation] = useAcceptInvitationMutation();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
firstName,
lastName,
email,
},
});
const onSubmit = useCallback(
(values: z.infer<typeof formSchema>) => {
acceptInvitation({
variables: {
id,
firstName: values.firstName,
lastName: values.lastName,
},
onCompleted,
},
});
},
[acceptInvitation, id, onCompleted]
);
return {
onSubmit,
form,
loading,
formFields: ['firstName', 'lastName', 'email'] as const,
};
}
Accept Invite component
This component will have the form description and title. We will be using the hook in here and the purpose for this page is to be able to accept the invite. We can easily make all this files into one file but we will not do that because we want our code to be systematic and manageable. Name a file AcceptInvite.tsx
import { Invitation, UserRole } from '@gen/graphql';
import { useRouter, usePathname } from 'next/navigation';
import { useAcceptInvitation, useInvitation } from '../hooks';
import { AcceptInviteDialog } from './acceptInviteDialog';
const InviteTypes = {
Member: {
role: UserRole.Member,
title: 'Accept Invitation',
description: "Edit your details and click accept when you're done.",
},
} as const;
export function AcceptInvite({
type,
id,
email,
firstName,
lastName,
}: AcceptInviteProps) {
const { title, description } = InviteTypes[type];
const router = useRouter();
const pathname = usePathname();
const {
form,
onSubmit,
formFields,
loading: loadingAccepting,
} = useAcceptInvitation({
id,
email,
firstName,
lastName,
onCompleted: () => {
const route = '/invitation';
if (pathname === route) {
// client-side has no reload function that works
return window.location.reload();
}
return router.push(route);
},
});
return (
<AcceptInviteDialog
title={title}
description={description}
form={form}
onSubmit={onSubmit}
formFields={formFields}
loading={loadingAccepting}
isDisabled={isDisabled}
/>
);
}
interface AcceptInviteProps {
type: 'Member';
firstName: string;
lastName: string;
email: string;
id: string;
}
When you are setting up the invite types it can be a variety of types and can be defined how they will be used. We have a type member that has its unique description and if a user is a member then the description and title will be as defined. The const in the AcceptInvite has to accept then properties that are returned in the UseAcceptInvite Hook. The UseAcceptInvite must accept the props defines in the hook.
The AcceptInvite function will return a dialog from the AcceptInvite that I will explain next.
Explaining the Accept Invite Dialog
In my project I want to use a dialog for the users to accept an invite that has been sent to them. When they click the acceptInvite button it will open a dialog that will display pre-populated details of the user.
Most of the components that I use are from a library called shadcn/ui . The library makes things easier and to familiarise your self with the components that you can use you can visit their website on: https://ui.shadcn.com/
The dialog that we will be using is a shadcn/ui component and this is how I used it. Firstly name your file AcceptInviteDialog.tsx
import { useState } from 'react';
import { FieldValues, UseFormReturn } from 'react-hook-form';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
Button,
Input,
DialogFooter,
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
Loader,
} from '~/components/ui';
import { camelToTitleCase } from '~/lib/utils';
export function AcceptInviteDialog<T extends FieldValues>({
title,
description,
form,
formFields,
onSubmit,
loading,
}: AcceptInviteDialogProps<T>) {
return (
<div>
<Dialog>
<DialogTrigger>
<Button variant="outline" rounded="full">
{title}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col w-full gap-8">
<div className="flex flex-col gap-2 w-full">
{formFields.map((key: any) => (
<FormField
key={key}
control={form.control}
name={key}
render={({ field }) => {
return (
<FormItem>
<FormLabel>{camelToTitleCase(key)}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
))}
</div>
<DialogFooter>
<Button type="submit" >
{loading ? 'Submitting..' : 'Accept Invite'}
</Button>
</DialogFooter>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
</div>
);
}
interface AcceptInviteDialogProps<T extends FieldValues> {
title: string;
description: string;
loading: boolean;
form: UseFormReturn<T>;
formFields: readonly (keyof T)[];
onSubmit: (value: T) => void;
The dialog that you see above is the one returned in the AcceptInvite.tsx.
Invitation page
Start by naming your page as Invitation.tsx
import { useInvitationQuery } from '@gen/graphql';
export function useInvitation() {
const { loading, data, refetch } = useInvitationQuery({
variables: {
id: '',
},
});
return {
data,
loading,
refetch,
};
}
This is the invitation page. We will query the invitation to return the data that is being queried. The the variable of the invitation will be the id. We want to get the invitation using its id and return the data in the invitation.
Page displayed on the web
After writing the code we want to see some functionality in the web. We have written a lot of code and we have to somehow display the things that we want to be shown on the web. Here is how you do it.
'use client';
import { AcceptInvite} from '~/users/components';
import { useInvitation } from '~/users/hooks';
export function InvitationPage() {
const { data, loading} = useInvitation();
console.log({ invitation, loading });
return (
<div>
{ Loading? !data:<Loader> <AcceptInvite type={'Member'} {...data.?invitation}} />
</div>
);
}