How to Customize the React 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.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL with a single-time purchase.

[00:00 - 00:14] We've established a use query hook to help make a GraphQL query when a component first mounts. We'll now look to have a use mutation hook that helps prepare a function to make a GraphQL mutation.
[00:15 - 00:36] Our use mutation hook will behave differently. Since we don't want a mutation to run the moment the component mounts, our use mutation hook will simply receive the mutation query to be made and return the function to make the request, as well as the loading and error status of the request.
[00:37 - 00:54] Our use mutation hook will return an array of two values, the first being the request function and the second being an object that contains details of our request. We'll get a better understanding of how our use mutation hook is to behave once we start to create it.
[00:55 - 01:11] We'll create the hook in a file of its own within the lib API folder, and we'll label this file "use mutation". Similar to use query, we're going to need to keep track of some states, so we'll import the useState function from React.
[01:12 - 01:30] 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 . We'll create a state interface that describes the shape of the state object we 'll want to maintain in our hook.
[01:31 - 01:47] It'll have a data, loading and error fields. We don't know what the shape of data would be unless specified as a type variable, so we'll expect the state interface to accept a tdata type variable.
[01:48 - 01:55] The data field will either be of that shape or be null. Loading and error will simply be boolean values.
[01:56 - 02:06] This is very similar to how we've set it up in the use query hook. We'll export it and create a constant function called "use mutation".
[02:07 - 02:26] This use mutation function can accept two type variables, tdata and t variables. tdata is to represent the shape of data that can be returned from the mutation, while t variables is to represent the shape of variables the mutation is to accept.
[02:27 - 02:39] Both of these type variables will have a default type value of any. Our mutation function, however, will only accept a single required document query parameter.
[02:40 - 02:57] Our use mutation hook should be prepared to handle a variables object needed for certain mutations, but we haven't passed variables as a potential argument in our hook function. The reason being is how we actually want our hook to work.
[02:58 - 03:12] In our case, we won't pass in variables when we create our mutation function, but instead pass it in the request function the mutation is expected to return. Here's a pseudo-example of what we mean by this.
[03:13 - 03:26] Assume we wanted to return the fetch function and label it as requests. Only when we actually call this request function will we pass in the variables necessary for the mutation.
[03:27 - 03:46] Now, this is simply the way we're setting up our hook. It's totally possible to have variables be passed in in the mutation hook function in the request function and even both, but we'll just go with the approach of having just the variables only be passed in the actual request function that's being returned from the hook.
[03:47 - 04:03] Let's initialize the state object we'd want our hook to maintain. We'll initialize our state similar to how we've done in the use query hook by setting data to null, loading and error to false.
[04:04 - 04:26] Let's now create the fetch function that would actually be responsible in making our request. It'll be an async function that accepts a variables object and within will introduce a try catch block to help catch any errors that might arise.
[04:27 - 04:48] The type of variables will be equal to the t variables type variable that is to be specified when the hook is constructed. We're specifying the question mark here in the variables parameter to say that variables is optional, since their very well-me made be mutations that don't actually require any variables.
[04:49 - 05:01] In the beginning of our try statements, we'll set the loading status to true since when the function is run, the request would be in flight. We'll keep data and error as the original values.
[05:02 - 05:19] We'll then make our server fetch function and pass in the query and variables values the server fetch function can accept. We know the server fetch function will return an object of data and errors, so we'll destruct those values as well.
[05:20 - 05:38] We'll also pass along the type of variables of data and variables to ensure the information returned from the server fetch function is appropriately typed. We've seen that our GraphQL request could resolve, but have errors be returned from our resolver.
[05:39 - 06:00] In this case, we'll check to see if these errors exist, and if so, we'll throw an error and pass in the error message that can exist for the first error object in the errors array Apollo Server returns. If the request is successful and no errors exist, we'll set the returned data into our state object.
[06:01 - 06:16] In addition, we'll set loading back to false and ensure error is false. If an error arrived for either the request failing itself or the request is returning errors, we'll set the error value in our state to true.
[06:17 - 06:29] We'll also specify data should be null and loading is false. We'll capture the error message and look to console log the error message that 's "Rised" in a "Throw" statement.
[06:30 - 06:40] This will literally be the scope of our fetch function. Only thing remaining from our custom use mutation hook is to return the values we'd want our component to use.
[06:41 - 06:51] For this hook we'll return an array of two values. The first value will be the fetch function itself, and the second value will be the state object.
[06:52 - 07:07] We could have very well returned an object of key value pairs and had the fetch function as a property in this object. However, here when we were turning an array, when we destruct these values in our components, we can actually name the request function as we'd like.
[07:08 - 07:17] This is because arrays aren't mapped based on a key value pair, but instead on indexes. Our use mutation hook is now complete.
[07:18 - 07:28] Notice how our use mutation hook differs from use query. They're similar in how we've constructed a fetch function and the state object we're using to keep track of our request.
[07:29 - 07:41] In use mutation, however, we're not using a use effect hook since we want to be in control of when a request should be made. In addition, we destruct the values as an array instead of an object.
[07:42 - 08:07] Let's have our new hook exported from our API folder before we use it in our components. In our listings component, we'll import it, and just like how we've declared our use query hook at the top level, we'll declare the use mutation hook right after our use query hook.
[08:08 - 08:33] We'll pass in the mutation constant object, that is to say the actual query document, delete listing, and the type variables referencing the data and variables of this mutation. We'll destruct the values we want from our hook, we'll name the fetch function being destructured delete listing, and we'll also de-structure from our state object the loading and error state.
[08:34 - 08:42] We won't have the need for data here since we don't plan on presenting the deleted listing data in our UI. We now notice a few errors.
[08:43 - 08:53] One of the errors is due to the fact that we're using the delete listing name for two different functions. So let's just rename the old function we had to handle delete listing.
[08:54 - 09:08] The other errors are due to the fact that we're using the same name for both the loading and error properties for both the use query and use mutation hook. ES6 actually allows us to rename destructured variables.
[09:09 - 09:25] So what we can do is we can rename the loading and error properties for the delete listing mutation to delete listing loading and delete listing error. This should help avoid the name clash between these same properties for both the different hooks.
[09:26 - 09:34] We can still see some errors remaining. Well, one of the errors that is being shown up is essentially saying that the destructured elements are still unused.
[09:35 - 09:42] We can ignore that for now because we are going to use them. The other error can be seen when we try to inspect the loading and error properties.
[09:43 - 10:06] It tells us that property loading or error does not exist on type function that accepts variables and returns a promise or state. When we head over to the use mutation hook, in our return statement we can see that we're returning an array where fetch is the function that returns promise void and the state object is the object with type of state.
[10:07 - 10:18] The reason behind the error is that when we usually define arrays, we usually define arrays with a single type. In this case, we're returning an array that has multiple types.
[10:19 - 10:37] The error is essentially the TypeScript compiler unable to infer that the first value of the array is the function type and the second value of the array is the state object. It basically tells us that every value in the array could either be one or the other.
[10:38 - 10:55] This is where we have to tell the TypeScript compiler explicitly the individual type of each element in the array for the first index and the second index. With that said, we'll declare this return type above and we'll call it the mutation tuple type.
[10:56 - 11:23] It'll also accept the t data and t variables type variables and simply be an array with two type values, the first being the function and the second being the state objects. We'll then specify our function return type is the mutation tuple type we've just created while passing along the type variables.
[11:24 - 11:34] When we go back to our components, we'll see that particular error is now gone. What we've created is known as a tuple type in TypeScript.
[11:35 - 11:46] I never know if I'm pronouncing it right. I don't know if it's tuple or tuple. Either way, tuple types allow us to express arrays with a fixed number of elements whose types are known.
[11:47 - 12:13] In the TypeScript documentation, they specified a tuple type for this x variable in which the first item is a string and the second item is a number. In our example, for our use mutation hook, we specified a tuple type where the first item is a function that can accept a variables object and returns promise void and the second item is the state object specified in our hook.
[12:14 - 12:34] For consistency sake, though we don't really need it for our use query hook, we 'll also specify a return type there as well. We'll call it query results. The query result interface will create essentially contains all the fields in state while introducing a refetch function.
[12:35 - 12:49] This is where we can take advantage of the extends keyword to extend the state interface for query results and simply add in the refetch field. Refetch is simply be a function that returns void.
[12:50 - 13:05] Now note the ability to extend types can't be done with a traditional Type Script type, but it can be done for interfaces. This is one distinction between using types and interfaces in TypeScript.
[13:06 - 13:35] In our handle delete listing function, we'll remove the use of server fetch and simply call the delete listing mutation that's been destructured and pass in the ID variable it expects. Since delete listing is asynchronous, we'll await till it finishes before we make our query refetch.
[13:36 - 13:55] Since we don't actually interact with the server fetch in our components, we can remove the import of server in our component file as well. We'll now create a simple header tag that will declare a deletion in progress message when a deletion is in flight.
[13:56 - 14:07] We'll use a ternary statement to have this message kept in a constant variable only when delete listing loading is true. Otherwise, we'll set the value of the variable to null.
[14:08 - 14:17] We'll place this element variable at the bottom of our function return. If an error in our mutation ever occurs, we'd want to tell the user that.
[14:18 - 14:30] So we'll similarly create a variable that is essentially the warning message only when delete listing error is true. We'll have this rendered at the bottom of our return as well.
[14:31 - 14:46] Notice how we don't display a full page error when a mutation fails. Only when the query fails do we have no data to show the user, so we've resorted to showing a full page error.
[14:47 - 15:12] When a mutation fails, however, we've opted to not remove the existing UI from our application and instead simply display an error message at the bottom of our UI. If we now head to the server code and try to force an error be thrown in our delete listing resolver function, in the client when we try to delete a listing, we'll see that the error message is now shown.
[15:13 - 15:30] Notice how the refetch function doesn't actually execute when the mutation fails. This is due to us throwing a console error at the end of our fetch method in the catch statement in our mutation, which prevents any code after it to execute.
[15:31 - 15:38] Amazing. If we head back to the server application code, we can now remove the forced throwing of the error.
[15:39 - 15:54] And in the client we'll try to delete the listing we have here. If our call is successful, we'll very briefly see the loading message that is shown when a deletion is in progress, and when complete our refetch will be made successful.
[15:55 - 15:58] Awesome. And that brings us to the end of this lesson.
[15:59 - 16:16] We've successfully created a use mutation hook that helps abstract the server fetch functionality away from components. For any component that may need to trigger a mutation, they can just simply follow the pattern we've performed here in the listings components.
[16:17 - 16:44] [ Silence ]