Client Fetching

Fetching from Client Components

A Client Component cannot be async, and a useEffect fetch reintroduces everything the App Router removed: a round trip that starts after hydration, an empty first paint, and a credential you cannot use because the request leaves the browser. Fetch on the client only when the data belongs there — a value that changes while the page is open, or depends on interaction.

The first option keeps the request on the server. Call the loader without await, pass the promise across the boundary (promises are serializable, Serializing Props), and unwrap it with React 7,897 's use:

Passing a promise to the client (app/live/page.js and book-list.js)JavaScript
export default function Live() {              // Server Component
  const books = listBooks(3);                 // no await: the promise is the prop
  return <main><h1>Books</h1><Suspense fallback={<p>Loading...</p>}>
    <BookList books={books} /></Suspense></main>;
}
"use client";                                 // app/live/book-list.js
export default function BookList({ books }) {
  const all = use(books);                     // suspends until it resolves
  const [q, setQ] = useState("");
  const shown = all.filter((b) => b.title.toLowerCase().includes(q.toLowerCase()));
  return <><input value={q} onChange={(e) => setQ(e.target.value)} />
    <ul>{shown.map((b) => <li key={b.id}>{b.title}</li>)}</ul></>;
}

The response streams, and the filter then runs in the browser with no further requests:

Output of 30
$ curl -s localhost:3310/live | grep -o "<li>[^<]*\|Loading\.\.\."
Loading...
<li>The Dispossessed
<li>A Wizard of Earthsea
<li>Solaris
Chromium 153, after typing "the" into the filter:  Books | The Dispossessed

The second option is a real client library, for data that must refresh without a navigation. SWR 2.5.1 639 (from the Next.js 10,514 team, npm 2,036 install swr) and TanStack Query 79,069 add caching, revalidation on focus, polling and retry: useSWR("/api/stats", fetcher, { refreshInterval: 2000 }) returns data, error and isLoading. The cost shows in the HTML — curl 3,008 on that page returns <p>Loading...</p> and nothing else, so a crawler and a slow phone both see the spinner. Point such a component at your own Route Handler (Route Handlers and the Proxy), never at the upstream API, so the credential stays on the server.