import { useMutation, useQueryClient } from '@tanstack/react-query'
import api from '@/services/api'
import { Post, PostId } from '@/types'
export function useLikePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (postId: PostId) => {
const { data } = await api.post(`/posts/${postId}/like`)
return data
},
onMutate: async (postId: PostId) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts'] })
// Snapshot previous value
const previousPosts = queryClient.getQueryData<Post[]>(['posts'])
// Optimistically update cache
queryClient.setQueryData<Post[]>(['posts'], (old) =>
old?.map((post) =>
post.id === postId
? { ...post, likes_count: post.likes_count + 1, liked_by_user: true }
: post
)
)
// Return snapshot for rollback
return { previousPosts }
},
onError: (err, postId, context) => {
// Rollback on error
if (context?.previousPosts) {
queryClient.setQueryData(['posts'], context.previousPosts)
}
},
onSettled: () => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})
}
import { PostId } from '@/types'
import { useLikePost } from '@/hooks/useLikePost'
interface LikeButtonProps {
postId: PostId
likesCount: number
likedByUser: boolean
}
export function LikeButton({ postId, likesCount, likedByUser }: LikeButtonProps) {
const likeMutation = useLikePost()
const handleLike = () => {
likeMutation.mutate(postId)
}
return (
<button
onClick={handleLike}
disabled={likeMutation.isPending}
className={`flex items-center gap-2 ${likedByUser ? 'text-red-600' : 'text-gray-600'}`}
>
<i className={`fas fa-heart ${likedByUser ? 'fas' : 'far'}`} />
<span>{likesCount}</span>
</button>
)
}
Optimistic updates make UIs feel instant by immediately showing the expected result before the server confirms. When a user likes a post, I update the local cache immediately, submit the request in background, and rollback if it fails. React Query's mutation callbacks (onMutate, onError, onSettled) orchestrate this flow. The onMutate function snapshots the current cache, updates it optimistically, and returns the snapshot for potential rollback. If the mutation fails, onError restores the snapshot. The onSettled callback runs regardless of success/failure to ensure cache consistency. This pattern dramatically improves perceived performance for actions like likes, follows, or simple edits where failures are rare.