Sep 5,2026
React vs Vue in 2026 — An Honest Comparison
Component model, reactivity, ecosystem, and hiring — a practical comparison of React and Vue for teams choosing in 2026, without the tribalism.
React
Vue
Frontend
Comparison
Published September 22, 2026
Everything you need to actually use TanStack Query in production: queries, mutations, caching, invalidation, optimistic updates, and the pitfalls that trip everyone up.
This is a crash course, not a reference doc — every section is runnable code you can paste and adapt. By the end you'll understand queries, mutations, cache invalidation, and the three bugs everyone hits in their first week.
Before it, "data fetching" in React meant useEffect + useState +
manually tracking loading/error/stale state, times every component that
needed the same data. TanStack Query replaces all of that with a
declarative cache: you describe what you need, it handles when to
fetch, refetch, and dedupe.
Install it:
npm install @tanstack/react-queryWrap your app in a provider:
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
export function QueryProvider({ children }: { children: React.ReactNode }) {
const [client] = useState(() => new QueryClient());
return (
<QueryClientProvider client={client}>{children}</QueryClientProvider>
);
}import { useQuery } from "@tanstack/react-query";
function Profile({ userId }: { userId: string }) {
const { data, isPending, error } = useQuery({
queryKey: ["user", userId],
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error("Failed to fetch user");
return res.json();
},
});
if (isPending) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <h1>{data.name}</h1>;
}The queryKey array is the cache key. ["user", userId] means every
distinct userId gets its own cache entry — call this hook twice with
the same userId anywhere in your tree, and only one network request
fires.
import { useMutation, useQueryClient } from "@tanstack/react-query";
function EditProfileForm({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (data: { name: string }) => {
const res = await fetch(`/api/users/${userId}`, {
method: "PATCH",
body: JSON.stringify(data),
});
return res.json();
},
onSuccess: () => {
// invalidate so the profile query refetches fresh data
queryClient.invalidateQueries({ queryKey: ["user", userId] });
},
});
return (
<button onClick={() => mutation.mutate({ name: "New Name" })}>
{mutation.isPending ? "Saving..." : "Save"}
</button>
);
}The pattern that makes UIs feel instant — update the cache before the server confirms, then roll back on failure:
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ["todos"] });
const previous = queryClient.getQueryData(["todos"]);
queryClient.setQueryData(["todos"], (old: Todo[]) =>
old.map((t) => (t.id === newTodo.id ? newTodo : t)),
);
return { previous };
},
onError: (_err, _newTodo, context) => {
// roll back on failure
queryClient.setQueryData(["todos"], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});Always cancel in-flight queries with cancelQueries before writing
optimistic data — otherwise a slow, stale fetch can resolve after your
optimistic update and silently overwrite it.
useEffect for fetching?No. If you're fetching data in useEffect + useState in 2026, TanStack
Query (or your framework's built-in loader, if using Next.js Server
Components) should replace it — you get caching, deduping, retries, and
refetch-on-focus for free.
useQuery reads, useMutation writes.queryKey is your cache identity — include every input.invalidateQueries is how you tell the cache "this is stale."onMutate + onError rollback + onSettled
refetch, in that order.