Client-Side Mutations

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. 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!

Slides

The mutations can also be called from client. The tanstack query library provides efficient means of handling such mutations. Let's try it!


In our blog app, users read posts tagged 'tech' or 'travel,' and admins add new ones. Normally, liking a post or adding one means waiting for the server—boring! In the Fetching Data lecture, we introduced TanStack Query, which lets us update the UI instantly on the client and then sync with the server. It’s fast and fun!


First, we define a simple REST API endpoint that either gets all the current posts or increases likes of an existing post.


The likePost function POSTs a new request to our post API to increase the number of likes.


The mutation executes this asynchronous function and awaits its result


The onMutate function executes when the mutation starts. In this case, we perform an optimistic update, replacing the existing post's likes with more likes.


If the mutation fails, we revert to original post with the original number of likes.


If the mutation succeeds, we re-confirm that the number of likes was saved, taking the value from the returned saved post.


The like button calls this mutation with the post id.


The list component uses tanstack query to load the posts and stores them in the cache under key "posts", which we are updating in our mutation. In general, tanstack query provides more granular control over the state of the cache and the way to update it.


Please see the documentation of the stack query for all the options, as covering them falls out of the scope of this class.