# Data Fetching

> Read data in a Next.js 16 App Router app the canonical way — async Server Components first, URL `searchParams` for filter/tab/range state, `Promise<T>` + `use()` + `<Suspense>` when a Client Component genuinely needs server data, and Route Handlers + SWR/React Query only as a last resort (polling / focus refetch / third-party-mutated data). Server Actions are for mutations only — never reads. Use…

- **Type:** Skill
- **Install:** `agentstack add skill-lukedj78-dev-flow-data-fetching`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [lukedj78](https://agentstack.voostack.com/s/lukedj78)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [lukedj78](https://github.com/lukedj78)
- **Source:** https://github.com/lukedj78/dev-flow/tree/main/data-fetching

## Install

```sh
agentstack add skill-lukedj78-dev-flow-data-fetching
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# data-fetching — Server Components first, never `useEffect` for reads

This skill governs **where data reads land** in a Next.js 16 App Router app. The framework lets you call a `"use server"` function from a Client Component — that's a capability, not a license. Reading data via a Server Action in `useEffect` costs you SSR, streaming, request deduping, caching, and parallelism. The bug is silent: no error, no warning, just worse UX and wasted POSTs.

## When this skill applies

- The user is about to call a Server Action from a Client Component to load data.
- The user is about to add `useEffect` (at all — but especially to fetch).
- The user pastes `"use client" + useState + useEffect + fetch/getX` and asks for review.
- The user is about to convert a page to `"use client"` so it can host filter/tab state.
- The user adds a `"use server"` function whose only job is `SELECT` / read.
- The user asks to **audit** a Next.js codebase against the data-fetching rules.

## Contract

Follows the dev-flow contract — see `references/contracts.md`. Key facts:

- Reads `meta.json#stack.framework` and `stack.nextjs_version`. For `framework = "monorepo"`, reads `stack.monorepo.web.framework` and `stack.monorepo.web.nextjs_version`.
- **Refuses to apply** if:
  - `stack.framework ∉ {"next", "monorepo"}` — Server Components / Server Actions don't exist on RN, Remix, SvelteKit, Astro, plain React, etc.
  - `stack.nextjs_version != "16"` — `searchParams` is async in 16 (was sync in 15), `revalidatePath` import path moved, `refresh()` from `next/cache` is new. Do not silently translate the rules.
  - The project uses Pages Router (`pages/` directory). Different mental model (`getServerSideProps` / `getStaticProps` / API routes / SWR) — refuse rather than translate.
- Appends a `history` entry per refactor.
- Does **not** bump `phase`.

## Companion skills

- **`state-discipline`** (sibling in dev-flow) — owns the broader React-side rule (never bare `useEffect`; derive state, use a query lib, use event handlers, `key` to reset, `useMountEffect` for one-time external sync). Install and follow it alongside this skill.
- **`forms`** — for any UI that persists field values to the backend. Forms are mutation-heavy and have their own toolkit; reads inside a form (e.g. preloading the entity to edit) follow this skill's rules.

If a green example below *looks* like it would have been a `useEffect` in older code, that's the point — it isn't one anymore. The red ❌ blocks show `useEffect` only because that's what the anti-pattern looks like in the wild; never copy from a red block.

## The Rule

**Read data in Server Components. Mutate data with Server Actions. Never use `useEffect` (or `useState + useEffect`) in a Client Component to call a Server Action just to load data.**

Violating the letter is violating the spirit. The signal that you've drifted is not a runtime error (there is none), it's the patterns below: a `getX` action, a `useEffect` that fetches, a page newly converted to `"use client"`. **The absence of a stack trace is not the absence of a problem.**

## Why

> "Server Functions are designed for server-side **mutations**, and the client currently dispatches and awaits them **one at a time**. […] If you need parallel data fetching, use data fetching in Server Components."  
> — Next.js docs, `mutating-data.mdx`

> "Server Actions are queued, and using them for data fetching introduces sequential execution."  
> — Next.js docs, `backend-for-frontend.mdx`

Concretely, a `useEffect`-driven Server Action read costs:

- **No SSR** — the page paints empty, then fetches after hydration. Worst LCP.
- **Sequential queue** — every Server Action call waits on the previous one.
- **No request deduping / caching** — Server Actions always POST.
- **No streaming** — no progressive render with ``.
- **Double-fetch on mount** in Strict Mode dev.
- **Larger client bundle** — fetch logic, loading states, error states ship to the browser.

## Decision: how to load data

The first question is **not** "where does the data need to land?" — it's "**why is this a Client Component at all?**" Most reads belong on the server. If the answer is anything weaker than "polling, focus refetch, or a third party mutates the data without user intent," the fix is to lift the read to a Server Component, not to swap the transport.

```dot
digraph data_fetching {
  "Why is this a Client Component?" [shape=diamond];
  "Server Component, await directly" [shape=box];
  "URL searchParams; page stays Server Component" [shape=box];
  "Promise from Server Component, use() in Client leaf" [shape=box];
  "Route Handler GET + TanStack Query (last resort)" [shape=box];

  "Why is this a Client Component?" -> "Server Component, await directly" [label="It isn't / shouldn't be"];
  "Why is this a Client Component?" -> "URL searchParams; page stays Server Component" [label="Filter / tab / range state"];
  "Why is this a Client Component?" -> "Promise from Server Component, use() in Client leaf" [label="Genuine interactivity at the data boundary, initial data only"];
  "Why is this a Client Component?" -> "Route Handler GET + TanStack Query (last resort)" [label="Polling, focus refetch, or third-party mutates the data"];
}
```

**The branches are not peers.** Top to bottom: Server Component (default, ~90% of cases), URL state (most "I need filters" cases), `use()` + `` (rare), Route Handler + TanStack Query (last resort, narrow scope). Reaching for the bottom branch when an upper branch fits is the most common failure mode of this skill.

**Server Actions are for mutations only.**

## Migrating away from `useEffect` + Server Action — the ladder

If you're staring at `useState` + `useEffect` + a `"use server"` read in a Client Component, walk this ladder **top-down** and stop at the first rung that fits. It's almost always rung 1.

1. **Lift the read to a Server Component.** Convert the page to `async function Page({ searchParams })`, `await` the read at the top, pass data down. If the page has interactive state, ask rung 2 *before* deciding it has to stay client.
2. **Move state to URL `searchParams`.** Tabs, filters, ranges, pagination, sort, search query — all belong in the URL. The Client leaf calls `router.replace`; the Server Component re-renders with new data. Free streaming, free cache, shareable URL, back-button works.
3. **Pass `Promise` from Server Component, consume with `use()` + ``.** Only when a Client Component genuinely needs server data as props at mount (charting libs, third-party widgets expecting a synchronous data shape).
4. **`GET` Route Handler + TanStack Query** (recommended default for this rung — retries, request dedup, devtools, mutation helpers; SWR is an acceptable lighter-weight alternative for a single simple polling widget, but don't reach for a second data library once TanStack Query is already in the project). Reserved for: interval polling, focus revalidation, third-party mutates the data outside your app. **Not** for "I already have a Client Component and want to keep it."

### The lateral migration is the failure mode

`useEffect` + action → `useQuery`/`useSWR` + Route Handler in the same Client Component is the wrong refactor. It feels like progress — no more action-as-read — but:

- Page is still `"use client"`. No SSR, no streaming, same bad LCP.
- You traded a sequential POST queue for a sequential `fetch`. Same waterfall.
- "Route Handlers cache!" — not for per-user, per-org reads. Your `/api/cases` is `Cache-Control: private`; the CDN won't touch it.
- You added a network hop, a JSON serialization layer, a client library, an extra route file — for zero cache wins over the Server Component you should have written.

If you reached rung 4 without first asking "can this page simply be a Server Component?", back up.

## The four correct patterns

### 1. Async Server Component — the default

```tsx
// app/(app)/cases/page.tsx
import { listCases } from "@/lib/services/case.service";

export default async function CasesPage() {
  const cases = await listCases();
  return ;
}
```

No `"use client"`, no `useEffect`, no Server Action.

### 2. Stream a promise to a Client Component with `use()` + ``

```tsx
// app/(app)/cases/page.tsx — Server Component
import { Suspense } from "react";
import { listCases } from "@/lib/services/case.service";
import CasesTable from "./_components/cases-table"; // "use client"

export default function CasesPage() {
  const casesPromise = listCases(); // do NOT await
  return (
    }>
      
    
  );
}
```

```tsx
// _components/cases-table.tsx
"use client";
import { use } from "react";

export default function CasesTable({
  casesPromise,
}: {
  casesPromise: Promise;
}) {
  const cases = use(casesPromise); // suspends until resolved
  // …interactive UI
}
```

The Server Component starts the fetch, streams HTML as soon as it can, the Client Component hydrates with the resolved value. No client-side waterfall.

### 3. Server Action — only for mutations, invoked via `` or event handler after user intent

```ts
// lib/actions/cases.actions.ts
"use server";
import { revalidatePath } from "next/cache";

export async function archiveCase(id: string) {
  await caseService.archive(id);
  revalidatePath("/cases");
}
```

After the mutation, invalidate and let the Server Component re-render with fresh data. Don't return a list to refresh client state by hand.

**Variants — pick the narrowest:**

- `revalidatePath('/cases')` — invalidate by route segment.
- `revalidateTag('cases')` — invalidate by tag (when a service uses `fetch(..., { next: { tags: ['cases'] } })`, `unstable_cache`'s `tags` option, or — under Cache Components — `cacheTag('cases')` inside a `"use cache"` function/component).
- `refresh()` from `next/cache` — inside a Server Action, refresh the client router cache for the current route. Useful when the mutation happens on the same page. Does **not** revalidate tagged data by itself — pair it with `revalidateTag`/`updateTag` when the mutation also needs to invalidate a tag.

**`revalidateTag` signature depends on whether Cache Components (`cacheComponents: true`) is enabled** — see [Cache Components](#cache-components--use-cache-next-16-opt-in) below for the full breakdown:

- **Cache Components off** (default / pre-16 caching model): `revalidateTag('cases')` — single argument, expires the tag immediately. This is what the example above uses and it's correct for projects that haven't opted into Cache Components.
- **Cache Components on**: the single-argument form is **deprecated**. Use either:
  - `revalidateTag('cases', 'max')` in a Server Action or Route Handler — stale-while-revalidate: the current request still gets a fast (possibly stale) response, fresh data loads in the background.
  - `updateTag('cases')` — **Server Actions only**, immediate expiry, read-your-own-writes (the user who triggered the mutation sees the new value on the very next render, not a background refresh). Prefer this over `revalidateTag` inside Server Actions whenever the user needs to see their own change immediately (e.g. after creating or editing the record they're looking at).

### 4. Route Handler + TanStack Query (recommended) / SWR — last resort, narrow scope

Reach for this **only** when the data genuinely changes without user intent: interval polling, focus revalidation, third-party mutates the data outside your app. Anything else belongs in patterns 1–3.

**TanStack Query is the default** for this rung — it's what the project already has if `forms` scaffolded `stack.forms = "tanstack-form"`, and it gives you retries, request dedup, devtools, and mutation helpers for free. Reach for SWR only for a genuinely trivial one-off polling widget in a project that has no other client-side query library; never install both.

```ts
// app/api/dashboard/stats/route.ts — only because the dashboard polls every 5s
import { NextResponse } from "next/server";

export async function GET(req: Request) {
  const range = new URL(req.url).searchParams.get("range") ?? "30d";
  return NextResponse.json(await getDashboardStats(range));
}
```

```tsx
// TanStack Query — recommended default
"use client";
import { useQuery } from "@tanstack/react-query";

export function LiveStats({ range }: { range: string }) {
  const { data } = useQuery({
    queryKey: ["dashboard-stats", range],
    queryFn: () =>
      fetch(`/api/dashboard/stats?range=${range}`).then((r) => r.json()),
    refetchInterval: 5_000,
  });
  return /* … */;
}
```

```tsx
// SWR — acceptable for a single trivial polling widget, no other client query lib in the project
"use client";
import useSWR from "swr";

export function LiveStats({ range }: { range: string }) {
  const { data } = useSWR(`/api/dashboard/stats?range=${range}`, fetcher, {
    refreshInterval: 5_000,
  });
  return /* … */;
}
```

If your Client Component doesn't poll, doesn't refetch on focus, and isn't watching externally-mutated data — **you don't need this**.

## Cache Components / `use cache` (Next 16, opt-in)

Next 16 introduces **Cache Components**, the explicit opt-in caching model, enabled with `cacheComponents: true` in `next.config.ts`. Once on, nothing is cached unless you mark it with the **`"use cache"`** directive — the framework stops implicitly caching and you cache *deliberately*. This is orthogonal to the ladder above: you still default to async Server Components; `"use cache"` is for **expensive reads you want memoized across requests** (a slow aggregate query, a third-party API call, a rarely-changing config), not a replacement for RSC data fetching.

```ts
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = { cacheComponents: true };
export default nextConfig;
```

```ts
// lib/services/case.service.ts — cache an expensive read
export async function getCaseStats() {
  "use cache";                 // file/function/component-level directive
  cacheLife("hours");          // how long this stays fresh (built-in profile or custom)
  cacheTag("cases");           // tag so a mutation can invalidate it
  return db.select(/* expensive aggregate */);
}
```

- **`cacheLife(profile)`** — the freshness/expiry profile (`"seconds"` | `"minutes"` | `"hours"` | `"days"` | `"max"`, or a custom profile in `next.config.ts`).
- **`cacheTag(tag)`** — attaches a tag; a Server Action then invalidates it.

**Invalidation under Cache Components** (this is why the mutation section above branches on the flag):

- **`updateTag('cases')`** — Server Actions only, **immediate** expiry with read-your-own-writes. Prefer it when the user must see their own change on the next render (e.g. right after editing the record they're viewing).
- **`revalidateTag('cases', 'max')`** — the second argument (a cache profile) is **required** here; it's stale-while-revalidate (the triggering request may still see stale data while fresh loads in the background). The **single-argument** `revalidateTag('cases')` is the pre-Cache-Components form and is **deprecated** once `cacheComponents` is on.

If the project has **not** enabled `cacheComponents`, ignore this section: the single-argument `revalidatePath` / `revalidateTag` in the mutation examples above are correct as-is. `[VERIFY]` the exact `cacheLife` profile names and the `revalidateTag`/`updateTag` signatures against the installed Next version — this surface is new in 16 and still settling.

## Anti-pattern catalog — red ❌ → green ✅

The full red→green catalog (6 patterns: action-in-useEffect, useState-filter, manual-refetch-after-mutation, getX-in-actions, await-then-pass-to-client, optimistic-by-hand) is in `references/anti-patterns.md`. Brief index:

1. **Reading via Server Action in `useEffect`** → async Server Component.
2. **Filter / tab state in `useState`, refetched via action** → URL `searchParams` + Server Component re-render.
3. **Manual re-read after mutation** → `revalidatePath` / `revalidateTag` / `refresh` inside the actio

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [lukedj78](https://github.com/lukedj78)
- **Source:** [lukedj78/dev-flow](https://github.com/lukedj78/dev-flow)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-lukedj78-dev-flow-data-fetching
- Seller: https://agentstack.voostack.com/s/lukedj78
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
