LECTURE: CLIENT-SIDE MUTATIONS WITH TANSTACK QUERY
WELCOME TO CLIENT-SIDE MAGIC
Hey everyone! Today, we’re exploring something exciting in web development:
Client-Side Mutations with TanStack Query [https://tanstack.com/query/latest].
If you’ve ever clicked "Like" on a post or added a comment and wondered how the
app updates so smoothly, this is the trick behind it. We’ll use TanStack Query—a
powerful library—to manage data changes right in the browser, making apps feel
fast and responsive.
Why should you care? Picture our blogging app, Blog, where users read posts,
filter them by tags like "tech" or "travel," and admins add or edit posts.
Normally, updating data might mean waiting for a server response, but with
TanStack Query, we can handle changes on the client-side first, then sync with
the server. By the end, you’ll know how to make Blog snappy—like adding a post
or liking it—without lag.
Let’s grab your attention: Ever thought updating an app always needs a server
roundtrip? A common misconception is that every change requires hitting the
server first. Nope—TanStack Query lets us update the UI instantly and sync
later! Or maybe you think client-side means insecure? Surprise—it’s safe when
paired with a server. Let’s dive in and bust these myths!
--------------------------------------------------------------------------------
SECTION 1: WHAT ARE CLIENT-SIDE MUTATIONS?
So, what’s a client-side mutation? It’s when we change data—like adding a post
or liking something—right in the browser, before or while talking to the server.
TanStack Query helps us do this by managing data fetching, caching, and
mutations in a neat package.
In Blog, imagine a user clicks "Like" on a post titled "Why AI is Awesome."
Without TanStack Query, we’d send a request to the server, wait, and then update
the like count. With TanStack Query, we update the count instantly in the UI,
then let the library handle the server sync. It’s called an optimistic update—we
assume it’ll work and fix it if it doesn’t.
Here’s a sneak peek at Blogs simplest mutation:
import { useMutation } from '@tanstack/react-query';
function LikeButton() {
const mutation = useMutation({
mutationFn: (postId: number) => fetch(`/api/like/${postId}`, { method: 'POST' }),
});
return <button onClick={() => mutation.mutate(1)}>Like</button>;
}
This button triggers a "Like" action. TanStack Query’s useMutation handles the
server call, but we’ll make it fancier soon. The surprise? You might think
mutations are all server-side—TanStack Query shifts control to the client,
making things feel instant.
--------------------------------------------------------------------------------
SECTION 2: SETTING UP BLOG WITH TANSTACK QUERY
Let’s build Blog's client-side mutations step-by-step, starting with liking a
post, then adding a new post.
Simple Example: Liking a Post
Imagine users browsing Blog’s /posts page, seeing a post with a "Like" button.
We want the like count to jump up instantly when clicked. Here’s how:
// app/api/like.ts (Server API simulation)
async function likePost(postId: number): Promise<{ likes: number }> {
// Pretend database update
return { likes: Math.floor(Math.random() * 100) + 1 }; // Random likes for demo
}
// app/components/LikeButton.tsx
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
type Post = { id: number; title: string; likes: number };
export default function LikeButton({ post }: { post: Post }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (postId: number) => likePost(postId),
onSuccess: (data) => {
queryClient.setQueryData(['posts', post.id], { ...post, likes: data.likes });
},
});
return (
<button onClick={() => mutation.mutate(post.id)}>
Like ({post.likes})
</button>
);
}
// app/posts/page.tsx
'use client';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
import LikeButton from '@/app/components/LikeButton';
const queryClient = new QueryClient();
async function fetchPosts(): Promise<Post[]> {
return [
{ id: 1, title: 'Why AI is Awesome', likes: 10 },
{ id: 2, title: 'Travel Tips', likes: 5 },
]; // Pretend API
}
export default function PostsPage() {
const { data: posts } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
return (
<QueryClientProvider client={queryClient}>
<ul>
{posts?.map(post => (
<li key={post.id}>
{post.title} - <LikeButton post={post} />
</li>
))}
</ul>
</QueryClientProvider>
);
}
What’s going on? The useQuery hook fetches posts, and useMutation handles the
"Like" action. When clicked, mutation.mutate calls the server, and onSuccess
updates the cached post data. The UI reflects the new likes right after the
server responds.
How It Works
TanStack Query caches data (like posts) with a queryKey. Mutations let us update
that cache manually or automatically. Here, we update the specific post’s likes,
keeping Blog in sync without a full reload.
--------------------------------------------------------------------------------
SECTION 3: OPTIMISTIC UPDATES IN BLOG
Now, let’s make Blog feel really fast with optimistic updates. Why wait for the
server? We’ll bump the like count instantly and roll back if it fails.
// app/components/LikeButton.tsx
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
type Post = { id: number; title: string; likes: number };
export default function LikeButton({ post }: { post: Post }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (postId: number) => likePost(postId),
onMutate: async (postId) => {
await queryClient.cancelQueries(['posts', postId]);
const previousPost = queryClient.getQueryData(['posts', postId]);
queryClient.setQueryData(['posts', postId], { ...post, likes: post.likes + 1 });
return { previousPost };
},
onError: (err, postId, context) => {
queryClient.setQueryData(['posts', postId], context?.previousPost);
},
onSuccess: (data) => {
queryClient.setQueryData(['posts', postId], { ...post, likes: data.likes });
},
});
return (
<button onClick={() => mutation.mutate(post.id)}>
Like ({post.likes})
</button>
);
}
How It Works
* onMutate: Before the server call, we optimistically increase the likes by 1
and save the old data.
* onError: If the server fails (e.g., network issue), we revert to the previous
data.
* onSuccess: When the server responds, we update with the real like count.
In Blog, clicking "Like" updates the count instantly. If the server says "20
likes," it syncs; if it fails, it rolls back. Users get instant feedback—super
smooth!
Advantages:
* Feels instant—great for engagement.
* Handles errors gracefully.
Disadvantages:
* More code to manage rollbacks.
* Assumes success, so server reliability matters.
--------------------------------------------------------------------------------
SECTION 4: ADDING POSTS WITH MUTATIONS
Let’s let admins add posts in Blog. We’ll use a form, optimistically show the
post, and sync it with the server.
// app/api/posts.ts (Server API simulation)
async function addPost(newPost: { title: string; tag: string }): Promise<Post> {
return { id: Date.now(), title: newPost.title, likes: 0, tag: newPost.tag };
}
// app/components/AddPost.tsx
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
type Post = { id: number; title: string; likes: number; tag: string };
export default function AddPost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newPost: { title: string; tag: string }) => addPost(newPost),
onMutate: async (newPost) => {
await queryClient.cancelQueries(['posts']);
const previousPosts = queryClient.getQueryData<Post[]>(['posts']);
const optimisticPost = { id: Date.now(), title: newPost.title, likes: 0, tag: newPost.tag };
queryClient.setQueryData(['posts'], (old: Post[] = []) => [...old, optimisticPost]);
return { previousPosts };
},
onError: (err, newPost, context) => {
queryClient.setQueryData(['posts'], context?.previousPosts);
},
onSuccess: (data) => {
queryClient.setQueryData(['posts'], (old: Post[] = []) =>
old.map(post => (post.id === data.id ? data : post))
);
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
mutation.mutate({
title: formData.get('title') as string,
tag: formData.get('tag') as string,
});
}}
>
<input name="title" placeholder="Post Title" />
<input name="tag" placeholder="Tag (e.g., tech)" />
<button type="submit">Add Post</button>
</form>
);
}
// app/posts/page.tsx (Updated)
'use client';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
import AddPost from '@/app/components/AddPost';
const queryClient = new QueryClient();
async function fetchPosts(): Promise<Post[]> {
return [
{ id: 1, title: 'Why AI is Awesome', likes: 10, tag: 'tech' },
{ id: 2, title: 'Travel Tips', likes: 5, tag: 'travel' },
];
}
export default function PostsPage() {
const { data: posts } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
return (
<QueryClientProvider client={queryClient}>
<AddPost />
<ul>
{posts?.map(post => (
<li key={post.id}>{post.title} - {post.tag} (Likes: {post.likes})</li>
))}
</ul>
</QueryClientProvider>
);
}
How It Works
* The form submits a new post.
* onMutate adds it to the post list optimistically.
* onError reverts if the server fails.
* onSuccess updates with the server’s final data.
In Blog, the admin sees their post—“AI Rocks,” tag “tech”—appear instantly. It’s
seamless and keeps the app lively.
--------------------------------------------------------------------------------
CONCLUSION
Let’s wrap up! TanStack Query makes client-side mutations in Blog fast and fun.
We liked posts and added new ones, all with instant UI updates and safe server
syncs. It’s about control—updating the client first, then confirming with the
server.
Key takeaways:
* Use useMutation for client-side changes.
* Optimistic updates make apps feel instant.
* Cache with queryClient to keep data fresh.
* Handle errors to avoid broken UIs.
Common pitfalls:
* Don’t forget rollbacks—server failures happen.
* Test network issues; optimism needs a safety net.
* Keep mutations simple—complex logic can trip you up.
Next time you build an app, ask: Can TanStack Query speed this up? For Blog, it
turned slow updates into a snappy experience. Go play with it—make something
cool!
Maggie is a generative AI that can help you understand the course content better. You can ask her questions about the lecture, and she will try to answer them. You can also see the questions asked by other students and her responses.
Join the discussion to ask questions, share your thoughts, and discuss with other learners