Build an Optimistic UI in React using SWR with useMutation
One commont UX improvement when building User Interfaces is to add optimistic updates to the application.
Let's see how to do it easily with the SWR Hook and the useMutation hook.
We need to create a function where we will perform our mutation, let's say we are creating a new comment in an article.
async function createComment({
authorId,
articleId,
body,
}: {
authorId: number;
articleId: number;
body: string;
}) {
const response = await fetch("/api/comments", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ authorId, articleId, body }),
});
if (!response.ok) throw new Error(response.statusText);
return await response.json();
}
With this, we can create a useCreateComment
hook, this will let use re-use our mutation together with the optimistic update.
Here we will use the cache
object and mutate
function from SWR to optimistically update the data cachd by SWR.
import { cache, mutate } from "swr";
import useMutation from "use-mutation";
export function useCreateComment() {
return useMutation<{ authorId: number; articleId: number; body: string }>(
createComment,
{
onMutate(input) {
// first we keep the current data in a const
const currentData = cache.get(["article", input.articleId]);
mutate((cachedData) => {
// here we are adding the input data (the new comment) to the list of
// comments of the article
return { ...cachedData, comments: cachedData.comments.concat(input) };
}, false);
// this function is our rollback function, we will use it in case the
// mutation failed
return () => mutate(["article", input.articleId], currentData, false);
},
onSuccess(data) {
// if the mutation was a success, we update the cache of SWR to replace
// the comment to optimistically added and add the final one returned
// by the API
mutate(["article", data.articleId], cachedData => {
return {...cachedData, comments: cachedData.comments.map(comment => {
if (comment.id) return comment;
return data;
})}
});
}
onFailure(error, rollback) {
console.error(error);
// here we are calling the rallback fn returned by onMutate
rollback();
},
}
);
}
And lately, we can use it in a React component
import * as React from "react";
import { useCreateComment } from "mutations/use-create-comment";
function CommentForm({ articleId, userId }) {
const [mutate, { status }] = useCreateComment();
const [body, setBody] = React.useState("");
const handleSubmit = React.useCallback<React.FormEventHandler>(
function handleSubmit(event) {
event.preventDefault();
mutate({ articleId, userId, body });
},
[body]
);
return (
<form onSubmit={handleSubmit}>
<textarea
value={body}
onChange={(event) => setBody(event.target.value)}
/>
<button>Create comment</button>
</form>
);
}
With this, once the user submit the form, it will first add the new commet at the bottom of the list, and then, it will try to create it, and if for some reason the request failed it will automatically rollback to use th previous data.