Update Requests with React Query
This is another post in a series of posts using React Query with TypeScript.
Previous posts:
The post covers how to use React Query with requests that make data updates. We will build a form that when submitted will make a PUT HTTP request.
A person form
Our form is as follows:
export function App() {
return (
<div>
<form>
<p>ID:</p>
<fieldset>
<div>
<label htmlFor="firstName">First name</label>
<input
type="text"
id="firstName"
name="firstName"
/>
</div>
<div>
<label htmlFor="lastName">Last name</label>
<input
type="text"
id="lastName"
name="lastName"
/>
</div>
</fieldset>
<button type="submit">Save</button>
</form>
</div>
);
}
The form contains fields to capture a first name and last name.
We want the form to contain existing data so that the user can make any necessary changes. We will use React Query to manage the request that fetches the existing data. Here’s the query:
export function App() {
const { status: queryStatus, error: queryError, data } = useQuery<
Person,
Error
>(["person", { id: 1 }], getPerson);
if (queryStatus === "loading") {
return <div>...</div>;
}
if (queryStatus === "error") {
return <div>{queryError!.message}</div>;
}
...
}
We alias the state variables so that they won’t clash with state variables that will come into play later. The type for a person is:
type Person = {
id: number;
firstName: string;
lastName: string;
};
We are requesting person with an id
of 1
from the fetching function getPerson
. Here is the fetching function:
async function getPerson({ queryKey }: { queryKey: [string, { id: number }] }) {
const [, { id }] = queryKey;
const response = await fetch(`http://localhost:3004/people/${id}`);
if (!response.ok) {
throw Error("Problem fetching person");
}
const data = await response.json();
assertIsPerson(data);
return data;
}
fetch
is used to make the request, and an error is thrown if the response isn’t successful.
The fetching function is expected to return the response data that we want to use in our component. We use a type assert function called assertIsPerson
to ensure the data is correctly typed:
function assertIsPerson(data: any): asserts data is Person {
if (!("id" in data && "firstName" in data && "lastName" in data)) {
throw new Error("Not a person");
}
}
We can use the state variables from the query as follows:
export function App() {
...
return (
<div>
{data && ( <form>
<p>ID: {data.id}</p> <fieldset>
<div>
<label htmlFor="firstName">First name</label>
<input
type="text"
id="firstName"
name="firstName"
defaultValue={data.firstName} />
</div>
<div>
<label htmlFor="lastName">Last name</label>
<input
type="text"
id="lastName"
name="lastName"
defaultValue={data.lastName} />
</div>
</fieldset>
<button type="submit">Save</button>
</form>
)} </div>
);
}
We show the person’s id and default the input values with the person’s first and last name.
Let’s start to implement the form submission:
export function App() {
...
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); const firstName = formData.get("firstName") as string; const lastName = formData.get("lastName") as string; } return (
<div>
{data && (
<form onSubmit={handleSubmit}> ...
</form>
)}
</div>
);
}
The submit handler extracts the first and last name values from the form. We will finish this implementation later.
Declaring a mutation
React Query can manage a request that updates data using a useMutation
hook.
Let’s declare our mutation below the query:
export function App() {
...
const { mutate, status: mutateStatus, error: mutateError } = useMutation<
Person, // return type
Error,
Person // params type
>(updatePerson);
...
}
We have aliased the destructured state variables so that they don’t clash with those from the query.
The destructured mutate
variable is what we will eventually use to trigger the update.
useMutation
takes in a mutation function that will make the request, which is updatePerson
in our example. We will look at this a little later.
useMutation
accepts generic type parameters. There are various overloads, but we have passed in the following:
- The return type from the mutation function.
- The error type from the mutation function.
- The parameter type of the mutation function.
Using mutation state variable
We can use the mutation state variables in the form as follows:
<form onSubmit={handleSubmit}>
<p>ID: {data.id}</p>
<fieldset disabled={mutateStatus === "loading"}> ...
</fieldset>
<button type="submit">Save</button>
{mutateStatus === "success" && <p>Change successfully saved</p>} {mutateStatus === "error" && <p>{mutateError!.message}</p>}</form>
We disable the update while it is in progress. We also display success/error messages when the update has been completed.
Triggering a mutation
We can trigger the mutation as follows in the submit handler:
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const firstName = formData.get("firstName") as string;
const lastName = formData.get("lastName") as string;
mutate({ id: data!.id, firstName, lastName });}
If we give the form a try, the update is successfully made:
Nice. 😊
The code in this post is available in the following gist: https://gist.github.com/carlrip/c048b9cad627fde1836a55ac24a31474
If you to learn more about using TypeScript with React, you may find my course useful: