NextjsFetching in Client Components

Fetching in Client Components

Server Components are the default and best place to fetch data in the App Router, but plenty of legitimate cases still need data fetching to happen in the browser — things like polling, search-as-you-type, or data that depends on client-only state such as scroll position or a browser API. This page covers how to fetch from a Client Component, and why you should almost always reach for a data-fetching library instead of raw useEffect.

The useEffect + useState approach

This is the classic pattern from pre-Server-Components React: track loading, error, and data manually, and fire the request inside a useEffect.

TSX
'use client'

import { useEffect, useState } from 'react'

interface Comment {
  id: string
  text: string
}

export default function CommentsWidget({ postId }: { postId: string }) {
  const [comments, setComments] = useState<Comment[] | null>(null)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    let cancelled = false

    fetch(`/api/posts/${postId}/comments`)
      .then((res) => res.json())
      .then((data) => {
        if (!cancelled) setComments(data)
      })
      .catch((err) => {
        if (!cancelled) setError(err.message)
      })

    return () => {
      cancelled = true
    }
  }, [postId])

  if (error) return <p>Failed to load comments.</p>
  if (!comments) return <p>Loading comments…</p>

  return (
    <ul>
      {comments.map((comment) => (
        <li key={comment.id}>{comment.text}</li>
      ))}
    </ul>
  )
}
Warning
This pattern works, but it re-implements caching, deduplication, race-condition handling (the cancelled flag above), retries, and revalidation from scratch every time you write it. It is also easy to get subtly wrong — forgetting the cleanup function is a very common source of "setState on unmounted component" bugs and stale data flashing on screen.
Prefer a dedicated data-fetching library

Libraries like SWR and TanStack Query (React Query) exist specifically to solve client-side fetching well: caching by key, automatic revalidation, deduplication of concurrent requests, retries, and built-in loading/error state — all with far less code than a hand-rolled useEffect.

TSX
'use client'

import useSWR from 'swr'

interface Comment {
  id: string
  text: string
}

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

export default function CommentsWidget({ postId }: { postId: string }) {
  const { data, error, isLoading } = useSWR<Comment[]>(
    `/api/posts/${postId}/comments`,
    fetcher,
  )

  if (error) return <p>Failed to load comments.</p>
  if (isLoading) return <p>Loading comments…</p>

  return (
    <ul>
      {data?.map((comment) => (
        <li key={comment.id}>{comment.text}</li>
      ))}
    </ul>
  )
}

SWR keys its cache by the URL (or key array) you pass in, so if two components request the same key at the same time, only one network request actually goes out. It also automatically revalidates on window focus and network reconnect by default, keeping data fresh without any extra code.

Tip
TanStack Query works on the same core ideas — query keys, automatic caching, background refetching — with a richer feature set for mutations, pagination, and infinite scrolling. Either library is a solid choice; pick one and use it consistently across the app.
When client-side fetching still makes sense
  • Data that depends on client-only state or interaction, like a search box that fetches results as the user types.

  • Data that needs to poll or refresh on an interval while the user has a tab open, such as a live status widget.

  • Data scoped to something that only exists in the browser, like the current viewport size or a value from localStorage.

  • Highly interactive widgets where you want optimistic UI updates driven from the client, independent of the page’s own render lifecycle.

Server Components vs. Client Components for fetching

Scenario

Recommended approach

Data needed to render the initial page

Fetch in a Server Component

Data behind a database or private API key

Fetch in a Server Component (keeps secrets off the client)

Data that changes based on user typing/interaction

Fetch in a Client Component with SWR/React Query

Data that needs to poll periodically

Fetch in a Client Component with SWR/React Query

Note
A common and effective pattern is hybrid: fetch the initial data in a Server Component for a fast first paint, then pass it into a Client Component as the initial value for a hook like SWR, which takes over subsequent refetching and revalidation.