typescript 82 lines · 3 tabs

React Query optimistic update for likes

Shared by codesnips Jan 2026
3 tabs
export interface Post {
  id: string;
  body: string;
  liked: boolean;
  likeCount: number;
}

export const postKey = (id: string) => ['post', id] as const;

export function toggleLike(post: Post): Post {
  const liked = !post.liked;
  return {
    ...post,
    liked,
    likeCount: post.likeCount + (liked ? 1 : -1),
  };
}

export async function sendLike(id: string, liked: boolean): Promise<Post> {
  const res = await fetch(`/api/posts/${id}/like`, {
    method: liked ? 'POST' : 'DELETE',
  });
  if (!res.ok) throw new Error(`Failed to update like: ${res.status}`);
  return res.json();
}
3 files · typescript Explain with highlit

This snippet demonstrates the optimistic-update pattern for a like/unlike toggle built on React Query (@tanstack/react-query). The core idea is that user interactions like tapping a heart should feel instant: rather than waiting for the server round-trip before updating the UI, the cache is patched immediately and only reconciled with the server afterward. If the request fails, the previous cache state is restored so the UI never lies for long.

The useLikePost hook tab wires this up through the four useMutation lifecycle callbacks. In onMutate, the hook first calls cancelQueries to abort any in-flight refetches that could overwrite the optimistic write with stale data — a subtle but important step, since a background fetch resolving after the mutation would clobber the local edit. It then snapshots the current cache with getQueryData and applies the toggle with setQueryData, computing the new liked flag and adjusting likeCount accordingly. That snapshot is returned as the mutation context.

If the network call rejects, onError receives that same context and rolls the cache back via setQueryData, guaranteeing the UI matches reality again. Regardless of success or failure, onSettled calls invalidateQueries so the post is eventually refetched and any drift (for example, a like count changed by another user) is corrected. This combination of optimistic write plus eventual invalidation is the canonical trade-off: maximum perceived speed with a self-healing fallback.

The PostQuery types tab defines the Post shape and a small updater helper so the cache math stays pure and testable. The LikeButton tab consumes the hook and shows how little the component needs to know — it renders from cache and calls mutate, letting isPending drive a disabled state to prevent double-taps. A key pitfall the code guards against is race conditions between rapid clicks; because each onMutate re-reads the freshest cache snapshot, sequential toggles compose correctly. Developers reach for this pattern whenever latency would otherwise make a common action feel sluggish, such as likes, follows, or bookmarks.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
swift
import SwiftUI

struct ContentView: View {
    @State private var username = ""
    @State private var isLoggedIn = false
    @StateObject private var viewModel = LoginViewModel()

SwiftUI declarative UI with state management

swift swiftui ios
by Sofia Martinez 2 tabs
javascript
import { Application } from "@hotwired/stimulus"
import FormSubmitController from "./controllers/form_submit_controller"

const application = Application.start()
application.debug = false

Disable submit button while Turbo form is submitting

rails hotwire stimulus
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

React Query optimistic update for likes — share card
Link copied