AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Data Fetching

skill-lukedj78-dev-flow-data-fetching · by lukedj78

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…

No reviews yet
0 installs
10 views
0.0% view→install

Install

$ agentstack add skill-lukedj78-dev-flow-data-fetching

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-lukedj78-dev-flow-data-fetching)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
16d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Data Fetching? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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.

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

// 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() + ``

// 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 (
    }>
      
    
  );
}
// _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

// 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.

// 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));
}
// 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 /* … */;
}
// 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.

// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = { cacheComponents: true };
export default nextConfig;
// 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 mutationrevalidatePath / 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.

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

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.