Lesson 15 of 2224 min read

Client-Side Data Fetching with SWR and React Query in Next.js

Learn when and how to fetch data on the client in Next.js using SWR's useSWR and React Query's useQuery, plus client-side revalidation strategies.

Author: CodersNexus

Client-Side Data Fetching with SWR and React Query in Next.js

Every data-fetching example so far has happened on the server, inside async Server Components — and for most content, that's exactly the right approach: faster initial loads, better SEO, zero extra client JavaScript. But some data genuinely belongs on the client: content that needs to refresh repeatedly without a full page navigation, data tied to frequent user interaction (like live search-as-you-type results), or data that should keep itself in sync in the background while a user has a tab open.

For these cases, the React ecosystem offers dedicated client-side data-fetching libraries — most notably SWR (built by Vercel, the same company behind Next.js) and React Query (also known as TanStack Query). Both solve a similar set of problems: caching, automatic background revalidation, deduplication of requests, and loading/error state management — all from within Client Components. This lesson covers both libraries' core patterns and when reaching for client-side fetching makes more sense than the server-side approaches covered earlier.

Learning Objectives

  • Identify scenarios where client-side data fetching is more appropriate than server-side fetching.
  • Use SWR's useSWR hook to fetch, cache, and automatically revalidate data.
  • Use React Query's useQuery hook to achieve similar client-side data management.
  • Understand key client-side revalidation strategies like revalidate-on-focus and polling.
  • Compare SWR and React Query at a high level to make an informed library choice.

Core Definitions

  • SWR: A React data-fetching library (the name comes from 'stale-while-revalidate') built by Vercel, providing the useSWR hook for client-side caching and automatic revalidation.
  • React Query (TanStack Query): A React data-fetching and state-management library providing the useQuery hook, with a strong focus on caching, background updates, and mutation handling.
  • useSWR: SWR's primary hook, taking a cache key and a fetcher function, returning data, error, and loading state, with automatic revalidation built in.
  • useQuery: React Query's primary hook for fetching and caching data, taking a query key and a fetcher function, with extensive configuration for caching and refetch behavior.
  • Revalidate-on-focus: A client-side revalidation strategy where data is automatically refetched whenever the browser tab regains focus, ensuring freshness after a user returns to it.
  • Polling: A client-side revalidation strategy where data is automatically refetched at a fixed time interval, regardless of user interaction.

Detailed Explanation

Client-side data fetching means calling an API directly from a Client Component, in the browser, rather than fetching it ahead of time on the server. This is the traditional React data-fetching approach, and while the App Router shifts most data fetching to the server by default, client-side fetching still has clear, legitimate use cases: data that needs to refresh on an interval without navigation (a live notification count), data resulting from rapid user interaction (search-as-you-type), or data that should automatically re-sync when a user returns focus to a browser tab after being away.

SWR's useSWR hook takes two main arguments: a unique key (typically the API URL, used for caching and deduplication) and a fetcher function (typically a simple wrapper around fetch()). It returns an object containing the fetched data, any error, and loading state, and it automatically handles a set of smart revalidation behaviors out of the box: revalidating when the browser tab regains focus, revalidating on reconnect after a lost network connection, and deduplicating identical requests made in a short window — all without extra configuration, following the 'stale-while-revalidate' philosophy its name describes: showing cached data immediately while quietly checking for updates in the background.

React Query's useQuery hook follows a very similar shape: a unique query key (often an array, allowing more structured cache invalidation) and a fetcher function, returning data, error, isLoading, and additional granular status flags. React Query is generally considered slightly more feature-rich out of the box, particularly around mutations (a companion useMutation hook for creating/updating/deleting data with built-in cache invalidation), pagination helpers, and fine-grained cache configuration — making it a common choice for data-heavy applications with complex client-side state needs, while SWR is often preferred for its smaller size and simpler API when those advanced features aren't needed.

Both libraries support explicit control over revalidation strategy beyond their smart defaults: polling (refetching automatically every N milliseconds, useful for near-real-time data like a live dashboard counter) and manual revalidation (triggering a refetch programmatically, for example immediately after a user submits a form that changes the underlying data). Choosing between server-side fetching (covered in earlier lessons) and one of these client-side libraries comes down to a simple question: does this specific piece of data need to live and refresh independently within an already-loaded page, or is it fine to be fetched once as part of the initial page load? The former calls for SWR or React Query; the latter is almost always better served by server-side fetching for its performance and SEO advantages.

Diagram Description

Visualize when to choose server-side vs client-side fetching:

[Does the data need to refresh WITHOUT a full page navigation?]
├── NO → Fetch on the SERVER (async Server Component, as in earlier lessons)
└── YES → Fetch on the CLIENT (SWR or React Query)
├── Needs polling / live updates? → configure refetchInterval / refreshInterval
├── Needs refresh on tab refocus? → default behavior in both libraries
└── Needs manual refetch after a mutation? → call mutate() (SWR) or invalidateQueries() (React Query)

Comparison Table

FeatureSWRReact Query (TanStack Query)
Primary hookuseSWR(key, fetcher)useQuery({ queryKey, queryFn })
Revalidate on window focusEnabled by defaultEnabled by default
Built-in mutation handlingBasic, via mutate()Dedicated useMutation hook with rich options
Bundle sizeSmallerLarger, but more feature-rich
Best fitSimpler apps, smaller data needsData-heavy apps with complex caching/mutation needs

Next.js Practical Example

// components/NotificationBadge.tsx — SWR example
'use client';

import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then((res) => res.json());

export default function NotificationBadge() {
  const { data, error, isLoading } = useSWR('/api/notifications/count', fetcher, {
    refreshInterval: 15000, // poll every 15 seconds
  });

  if (isLoading) return <span>...</span>;
  if (error) return <span>—</span>;
  return <span className="badge">{data.count}</span>;
}

// components/NotificationBadgeReactQuery.tsx — React Query equivalent
'use client';

import { useQuery } from '@tanstack/react-query';

export default function NotificationBadgeReactQuery() {
  const { data, error, isLoading } = useQuery({
    queryKey: ['notifications', 'count'],
    queryFn: () => fetch('/api/notifications/count').then((res) => res.json()),
    refetchInterval: 15000,
  });

  if (isLoading) return <span>...</span>;
  if (error) return <span>—</span>;
  return <span className="badge">{data.count}</span>;
}

Both NotificationBadge components solve the identical problem — showing a live, self-updating unread notification count — using their respective library's core hook. useSWR takes '/api/notifications/count' as its cache key and the fetcher function, automatically caching the result and re-fetching it every 15 seconds via refreshInterval, plus automatically on window refocus. useQuery achieves the same outcome with a structured queryKey array (useful for more precise cache invalidation elsewhere in a larger app) and an equivalent refetchInterval option. Notice both components require 'use client', since these hooks rely on client-side state and effects — this kind of self-refreshing badge is exactly the case where client-side fetching, rather than a one-time server fetch, is the correct approach, since the count needs to update repeatedly without the user navigating anywhere.

Industry Examples

  • Social media platforms use SWR or React Query for live notification badges, unread message counts, and like/comment counts that need to refresh without a full page reload.
  • Project management tools like task boards use React Query's mutation handling to optimistically update a task's status on drag-and-drop, then reconcile with the server in the background.
  • Financial trading platforms use polling with SWR or React Query to keep displayed prices reasonably current within an already-loaded dashboard, without requiring a full page refresh.
  • E-commerce sites use SWR for a 'live' cart item count in the header that updates instantly across tabs/components when an item is added, without needing a server round-trip and page reload.
  • Admin dashboards with complex, interrelated data (users, permissions, billing) often choose React Query specifically for its structured cache invalidation, letting a single mutation correctly refresh multiple related, already-loaded views.

Interview Questions and Answers

Q1. When should you use client-side data fetching (SWR/React Query) instead of server-side fetching in Next.js?

Client-side fetching is appropriate when data needs to refresh independently within an already-loaded page without a full navigation — such as live counters, polling dashboards, or data resulting from rapid user interaction like search-as-you-type. For most initial page content, server-side fetching remains preferable for performance and SEO.

Q2. What does the 'stale-while-revalidate' name behind SWR actually mean in practice?

It means SWR shows cached ('stale') data to the user instantly if available, while simultaneously checking the network for fresh data in the background, then updating the UI once the fresh data arrives — prioritizing perceived speed without sacrificing eventual accuracy.

Q3. What are the two main arguments passed to useSWR and useQuery?

Both take a unique cache key (a URL string for useSWR, or often an array for useQuery's queryKey) and a fetcher function responsible for actually retrieving the data, typically a simple wrapper around fetch().

Q4. How do SWR and React Query differ in their approach to mutations?

SWR provides a simpler mutate() function for manually triggering revalidation or updating cached data after a change. React Query provides a dedicated useMutation hook with more built-in features for handling creates/updates/deletes, including optimistic updates and structured cache invalidation via query keys.

Q5. What is polling in the context of client-side data fetching, and how is it configured?

Polling means automatically refetching data at a fixed time interval regardless of user interaction, useful for near-real-time data. It's configured via a refreshInterval option in SWR or a refetchInterval option in React Query, both specified in milliseconds.

MCQs With Answers

1. Which scenario is the best fit for client-side data fetching rather than server-side fetching?

  1. A blog post's main content
  2. A marketing homepage's hero section
  3. A live notification count that updates without page navigation
  4. A company's About Us page

Answer: C. A live notification count that updates without page navigation

Explanation: Data that must refresh independently within an already-loaded page, without a full navigation, is the classic use case for client-side libraries like SWR or React Query.

2. What does the name SWR stand for?

  1. Server Web Requests
  2. Stale-While-Revalidate
  3. Simple Web Router
  4. Static Website Refresh

Answer: B. Stale-While-Revalidate

Explanation: SWR is named after the caching pattern it implements: showing cached (stale) data instantly while revalidating with fresh data in the background.

3. What are the two primary arguments passed to useSWR?

  1. A component and a prop
  2. A cache key and a fetcher function
  3. A URL and an HTTP method only
  4. A loading state and an error state

Answer: B. A cache key and a fetcher function

Explanation: useSWR takes a unique key (typically the request URL) used for caching, and a fetcher function responsible for actually retrieving the data.

4. Which React Query hook is specifically designed for handling data mutations like creates and updates?

  1. useQuery
  2. useMutation
  3. useEffect
  4. useFetch

Answer: B. useMutation

Explanation: React Query provides a dedicated useMutation hook, offering built-in features for handling data changes, including optimistic updates and cache invalidation.

5. What is 'polling' in the context of SWR or React Query?

  1. Asking the user for permission before fetching
  2. Automatically refetching data at a fixed time interval
  3. A way to cancel a fetch request
  4. A method for compressing API responses

Answer: B. Automatically refetching data at a fixed time interval

Explanation: Polling refetches data on a regular schedule (configured via refreshInterval or refetchInterval), useful for near-real-time data that should update without requiring user interaction.

Common Mistakes to Avoid

  • Using client-side fetching (SWR/React Query) for a page's main initial content, when server-side fetching would offer better performance and SEO for that same data.
  • Forgetting that hooks like useSWR and useQuery require a Client Component, and omitting the necessary 'use client' directive.
  • Not providing a stable, unique cache key, causing unnecessary refetching or cache collisions between unrelated data.
  • Overusing aggressive polling intervals for data that doesn't actually need near-real-time freshness, creating unnecessary network and server load.
  • Assuming SWR and React Query behave identically in every respect, when their defaults, mutation handling, and feature sets differ in meaningful ways worth evaluating for a specific project's needs.

Interview Notes

  • Client-side fetching (SWR/React Query) is best for data that must refresh independently within an already-loaded page, without full navigation.
  • useSWR(key, fetcher) and useQuery({ queryKey, queryFn }) are the primary hooks for SWR and React Query respectively.
  • Both libraries revalidate on window focus by default, following a stale-while-revalidate caching philosophy.
  • React Query offers a more feature-rich useMutation hook for handling data changes; SWR offers a simpler mutate() function.
  • Polling (refreshInterval/refetchInterval) automatically refetches data at a fixed time interval for near-real-time needs.

Key Takeaways

  • Client-side data fetching complements, rather than replaces, the server-side rendering strategies covered in earlier lessons — each has a clear, distinct use case.
  • SWR and React Query both solve caching, revalidation, and loading/error state management, sparing you from reimplementing these patterns manually with useEffect and useState.
  • The choice between SWR and React Query often comes down to project complexity: SWR for simplicity and smaller bundle size, React Query for richer mutation handling and larger, data-heavy applications.
  • Understanding when data belongs on the server versus the client is one of the most important architectural decisions in a modern Next.js application.

Summary

While server-side fetching in async Server Components is the right default for most content in the Next.js App Router, some data genuinely needs to live and refresh on the client — live counters, polling dashboards, or search-as-you-type results. SWR and React Query (TanStack Query) are the two leading libraries for this, each providing a primary hook (useSWR and useQuery respectively) that takes a cache key and a fetcher function, returning data, error, and loading state with automatic caching and revalidation built in, following a stale-while-revalidate philosophy. Both revalidate automatically on window refocus by default and support explicit polling via a refresh/refetch interval for near-real-time needs. React Query offers a more feature-rich approach to mutations via its useMutation hook, making it a common choice for data-heavy applications, while SWR's smaller size and simpler API suit projects with more modest client-side data needs. Choosing correctly between server-side and client-side fetching for each piece of data is a key architectural skill in building performant, well-structured Next.js applications.

Frequently Asked Questions

Server-side fetching happens once, ahead of time, as part of rendering a page on the server (covered in earlier lessons). Client-side fetching with SWR or React Query happens in the browser, inside a Client Component, and can refresh independently while the page is already loaded, without requiring navigation.

Choose client-side fetching when data needs to update repeatedly without a full page navigation — such as live notification counts, polling dashboards, or search-as-you-type results — rather than for a page's main initial content, which is usually better served on the server.

Yes. Both hooks rely on client-side React state and effects internally, so any component using them must be a Client Component, marked with the 'use client' directive.

It means that both libraries automatically refetch data whenever the browser tab regains focus (for example, when a user switches back to that tab after being on another one), helping ensure the displayed data isn't stale after periods of inactivity — this behavior is enabled by default in both libraries.

SWR is generally simpler and smaller, ideal for straightforward client-side data needs. React Query offers a richer feature set, particularly around mutations (via useMutation) and structured cache invalidation, making it well-suited to larger, data-heavy applications with more complex client-side state requirements.

Use SWR's refreshInterval option or React Query's refetchInterval option, both specified in milliseconds, to enable polling — automatically refetching the data on a fixed schedule regardless of user interaction.

Yes, and this is a very common and recommended pattern — use server-side fetching for a page's main initial content, and layer in SWR or React Query for specific, smaller pieces of UI that need independent, ongoing client-side updates.

SWR automatically deduplicates requests sharing the same key, meaning both components share the same cached data and only one underlying network request is made, even though multiple components are 'subscribed' to that same data.

It can, if used for content that search engine crawlers need to see, since client-fetched data isn't present in the initial server-rendered HTML. This is exactly why client-side fetching is best reserved for non-critical, interactive, or frequently-updating data rather than a page's core content.

Yes. React Query was renamed to TanStack Query as the library expanded to support frameworks beyond React (such as Vue and Svelte), but the React-specific package and its useQuery hook function the same way and are still commonly referred to as 'React Query' by developers.