Manually managing loading states, caching, and refetching with useEffect got old years ago, and both React Query and SWR solve that problem well. We built the same feature twice, once with each library, on a project with real server-state complexity to see which one actually fit our team's habits rather than relying on blog-post comparisons.
React Query's devtools made debugging cache behavior significantly easier during development, which mattered more than we expected once the team started actually using them day to day. Being able to see exactly which queries were stale, which were refetching in the background, and why a particular component re-rendered turned a category of "why is this showing old data" bugs from a debugging session into a quick glance at a panel. React Query's mutation API, with its built-in support for optimistic updates and cache invalidation on success, also removed a fair amount of hand-rolled state-syncing code we'd previously written by hand for anything involving a form submission that needed to update a list elsewhere on the page.
SWR's smaller bundle size and simpler API were genuinely appealing for smaller projects where React Query's mutation and query-invalidation features were more machinery than we needed. A marketing site with a handful of read-only data fetches doesn't need a full mutation and cache-invalidation system; it needs stale-while-revalidate behavior and not much else, and SWR delivers exactly that with a noticeably lighter footprint in both bundle size and API surface area to learn.
Server-state complexity is the variable that actually predicts which library fits, more than team size or project age. A project with a handful of independent read-only queries barely stresses either library's design; the differences only really show up once queries start depending on each other, once a mutation needs to invalidate three unrelated queries at once, or once you need fine-grained control over retry behavior and stale time on a per-query basis. Our comparison project deliberately included a multi-step checkout flow with exactly that kind of interdependency, since a simpler feature wouldn't have surfaced any real difference between the two libraries at all.
Bundle size differences are real but smaller in practice than the raw numbers suggest once a project is already pulling in a reasonably sized dependency tree elsewhere. For a marketing site or a small internal tool where every kilobyte matters more, SWR's lighter footprint is a genuine advantage worth choosing for on its own. For a larger application where the data-fetching library is a small fraction of total bundle size regardless of which one is picked, the feature and tooling differences matter more than the size difference does.
Migration between the two, should a project's needs change, turned out to be less painful than we expected going in. Both libraries share a similar mental model, a hook that returns data, loading, and error state keyed by a query identifier, so a project that outgrows SWR's simpler feature set can move to React Query without restructuring how components consume server state, just how the underlying fetching and caching is configured.
We've settled on React Query as our default for anything with meaningful server-state complexity: multiple related queries, mutations that need to invalidate several caches at once, or optimistic UI. SWR remains our pick for smaller projects where a lighter footprint matters more than advanced cache controls, and we haven't found a project yet where we regretted either choice once we'd sorted it by that criterion instead of by preference.