Full Stack Development

Client-Side Mutations



by Tomas Trescak t.trescak@westernsydney.edu.au

Next.js

/

@tanstack/query

    // app/api/posts.ts (Server simulation)
function getPosts() {
  NextResponse.json([{id: 1}, ...]) 
}

function likePost(req: NextRequest) {
  const { id } = req.body();
  const savedPost = db.update({
    where: { id },
    update: { likes: { inc: 1 } }
  });
  return savedPost;
}

export { getPosts as GET, savePost as POST }

// app/components/LikeButton.tsx
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';

type Post = { id: number; title: string; likes: number };

function likePost(id: string) {
  return fetch('/api/posts', {
    method: 'POST',
    body: JSON.stringify({ id })
  })
}

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<Post>(['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>
  );
}

// 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[]> {
  const result = await fetch('/api/posts');
  return result.json();
}

export default function PostsPage() {
  const { data: posts } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });

  return (
    <QueryClientProvider client={queryClient}>
      {posts?.map(post => (
        <div key={post.id}>
          {post.title} - <LikeButton post={post} />
        </div>
      ))}
    </QueryClientProvider>
  );
}
  

Next.js

/

@tanstack/query