Custom useMutation Hook
In this lesson, we'll create a custom useMutation Hook which will abstract the server fetch functionality needed to conduct a mutation from a component.
📝 This lesson's quiz can be found - here. 🗒️ Solutions for this lesson's quiz can be found - here.
We've created a useQuery
Hook to help make a GraphQL query request when a component first mounts. In this lesson, we'll look to create a useMutation
Hook that helps prepare a function to make an API mutation request.
Gameplan#
Our useMutation
Hook will behave differently since we won't want a mutation to run the moment a component mounts by default. Our useMutation
Hook will simply receive the mutation query document to be made.
export const Listings = ({ title }: Props) => {
useMutation<DeleteListingData, DeleteListingVariables>(DELETE_LISTING);
// ...
};
And return an array of two values - the first being the request function and the second being an object that contains detail of the request.
export const Listings = ({ title }: Props) => {
const [deleteListing, { loading, error }] = useMutation<
DeleteListingData,
DeleteListingVariables
>(DELETE_LISTING);
// ...
};
We'll get a better understanding of how our useMutation
Hook is to behave once we start to create it.
useMutation
#
We'll create our useMutation
Hook in a file of its own in the src/lib/api
folder. We'll label this file useMutation.ts
.
src/
lib/
api/
index.ts
server.ts
useMutation.ts
useQuery.ts
// ...
Similar to the useQuery
Hook, we're going to need to keep track of some state within our useMutation
Hook so we'll import the useState
Hook. Since we're going to be interacting with the server as well, we'll import the server
object which will help us make the server.fetch()
request.
import { useState } from "react";
import { server } from "./server";
We'll create a State
interface that describes the shape of the state object we'll want to maintain. The State
interface will have the data
, loading
, and error
fields. The type of data
will either be equal to a type variable passed in the interface (TData
) or null
. The loading
and error
fields will be of type boolean
.
interface State<TData> {
data: TData | null;
loading: boolean;
error: boolean;
}
We'll export and create a const
function called useMutation
. The useMutation
function will accept two type variables - TData
and TVariables
. TData
is to represent the shape of data that can be returned from the mutation while TVariables
is to represent the shape of variables the mutation is to accept. Both of the TData
and TVariables
type variables will have a default type value of any
.
Our mutation function, however, will only accept a single required document query
parameter.
import { useState } from "react";
import { server } from "./server";
interface State<TData> {
data: TData | null;
loading: boolean;
error: boolean;
}
export const useMutation = <TData = any, TVariables = any>(query: string) => {};
The term
query
here is used to reference the GraphQL request that is to be made. One can rename thequery
parameter tomutation
to be more specific.
variables
#
We expect variables necessary for our request to be used in our useMutation
Hook but we haven't specified variables
as a potential argument in our function. The reason being is how we want our Hook to work. In our use case, we won't want to pass in the variables when we use the useMutation
Hook but instead pass it in the request function the mutation is expected to return.
Let's go through an example of what we intend to do. Assume the useMutation
Hook when used in a component is to return a fetch function that we'll label as request
.
export const Listings = ({ title }: Props) => {
const [request] = useMutation<DeleteListingData, DeleteListingVariables>(
DELETE_LISTING
);
};
Only when the request
function is called, will we pass in the variables necessary for the mutation.
export const Listings = ({ title }: Props) => {
const [request] = useMutation<DeleteListingData, DeleteListingVariables>(
DELETE_LISTING
);
const deleteListing = (id: string) => {
await request({ id }); // variables is passed in
};
};
This is simply how we want to set up our useMutation
Hook. It's possible to also pass in variables when we run the Hook function as well.
useMutation
#
At the beginning of the useMutation
Hook, we'll initialize the state object we'll want our Hook to maintain. We'll initialize our state similar to how we've done in the useQuery
Hook by setting data
to null
and the loading
and error
fields to false
.
import { useState } from "react";
import { server } from "./server";
interface State<TData> {
data: TData | null;
loading: boolean;
error: boolean;
}
export const useMutation = <TData = any, TVariables = any>(query: string) => {
const [state, setState] = useState<State<TData>>({
data: null,
loading: false,
error: false
});
};
We'll now create a fetch()
function in our useMutation
Hook that will be responsible for making our request. fetch()
will be an asynchronous function that accepts a variable
object that will have a type equal to the TVariables
type variable. We'll also state the variables
parameter is an optional parameter since there may be mutations we can create that don't require any variables.
We'll introduce a try/catch
statement within the fetch()
function.
import { useState } from "react";
import { server } from "./server";
interface State<TData> {
data: TData | null;
loading: boolean;
error: boolean;
}
export const useMutation = <TData = any, TVariables = any>(query: string) => {
const [state, setState] = useState<State<TData>>({
data: null,
loading: false,
error: false
});
const fetch = async (variables?: TVariables) => {
try {
// try statement
} catch {
// catch statement
}
};
};
At the beginning of the try
block, we'll set the state loading
property to true
since at this point the request will be in-flight. We'll keep data
and error
as the original values of null
and false
respectively.
We'll then conduct our server.fetch()
function, and pass in the query
and variables
values the server.fetch()
function can accept. The server.fetch()
function will return an object of data
and errors
so we'll destruct those values as well. We'll also pass along the type variables of data
and variables
to ensure the information returned from the server.fetch()
function is appropriately typed.
import { useState } from "react";
import { server } from "./server";
interface State<TData> {
data: TData | null;
loading: boolean;
error: boolean;
}
export const useMutation = <TData = any, TVariables = any>(query: string) => {
const [state, setState] = useState<State<TData>>({
data: null,
loading: false,
error: false
});
const fetch = async (variables?: TVariables) => {
try {
setState({ data: null, loading: true, error: false });
const { data, errors } = await server.fetch<TData, TVariables>({
query,
variables
});
} catch {
// catch statement
}
};
};
In the last lesson, we observed how GraphQL requests could resolve but have errors be returned from our resolver. These errors will be captured in the errors
array we'retrieving from the server.fetch()
function. We'll check to see if these errors exist and if so - throw an Error
and pass in the error message from the first error object in the errors
array.
import { useState } from "react";
import { server } from "./server";
interface State<TData> {
data: TData | null;
loading: boolean;
error: boolean;
}
export const useMutation = <TData = any, TVariables = any>(query: string) => {
const [state, setState] = useState<State<TData>>({
data: null,
loading: false,
error: false
});
const fetch = async (variables?: TVariables) => {
try {
setState({ data: null, loading: true, error: false });
const { data, errors } = await server.fetch<TData, TVariables>({
query,
variables
});
if (errors && errors.length) {
throw new Error(errors[0].message);
}
} catch {
// catch statement
}
};
};
If the request is successful and no errors exist, we'll set the returned data
into our state. We'll also set the loading
and error
properties to false.
If an error arises for either the request failing or errors being returned from the API, we'll set the error
value in our state to true
. We'll also specify data
should be null
and loading
is false. We'll capture the error
message and look to log the error
message in the browser console.
import { useState } from "react";
import { server } from "./server";
interface State<TData> {
data: TData | null;
loading: boolean;
error: boolean;
}
export const useMutation = <TData = any, TVariables = any>(query: string) => {
const [state, setState] = useState<State<TData>>({
data: null,
loading: false,
error: false
});
const fetch = async (variables?: TVariables) => {
try {
setState({ data: null, loading: true, error: false });
This page is a preview of TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL