REACT & NEXT.JS / 7. DATA FETCHING & CACHING

Data Fetching, Server Actions & Caching

Getting data in and out — the right way for each scenario


EXPLANATION

Next.js extends the native fetch API with caching options.

fetch() caching options:
• fetch(url) → cached by default (like getStaticProps)
• fetch(url, { cache: "no-store" }) → always fresh (like getServerSideProps)
• fetch(url, { next: { revalidate: 60 } }) → revalidate every 60 seconds (ISR)

Server Actions: async functions that run on the server, called from client components.
Mark with "use server" directive. Used for form submissions, mutations.
This replaces API routes for simple mutations.

When to use what:
• Server Component fetch → reading data on initial page load
• Server Action → form submissions, mutations (create/update/delete)
• API Route (route.ts) → webhooks, third-party integrations, REST API
• React Query / SWR → client-side data that needs refetching, pagination

React Query handles: loading/error states, caching, background refetch, pagination, optimistic updates. Use it for any client-side data fetching.

DIAGRAM

Data fetching decision tree:

  Need data for initial page render?
  └── YES → Server Component fetch()
             ├── Static? → cache: "force-cache" (default)
             ├── Dynamic? → cache: "no-store"
             └── Revalidate? → next: { revalidate: N }

  User submits a form / mutation?
  └── Server Action ("use server")
      └── revalidatePath() to refresh the page

  Client-side fetching (after load)?
  └── React Query / SWR
      └── handles loading, error, caching, refetch

  External webhook / REST API?
  └── API Route (app/api/.../route.ts)

CODE

TSX
1// ── Server Component: fetch with caching ──────────────────────────
2// Cached indefinitely (static) good for rarely changing data
3async function StaticData() {
4 const data = await fetch("https://api.example.com/config").then(r => r.json());
5 return <div>{data.version}</div>;
6}
7
8// Always fresh good for real-time data
9async function LiveData() {
10 const data = await fetch("https://api.example.com/live", {
11 cache: "no-store",
12 }).then(r => r.json());
13 return <div>{data.value}</div>;
14}
15
16// Revalidate every 60 seconds good for semi-dynamic data
17async function SemiStaticData() {
18 const data = await fetch("https://api.example.com/posts", {
19 next: { revalidate: 60 },
20 }).then(r => r.json());
21 return <ul>{data.map((p: Post) => <li key={p.id}>{p.title}</li>)}</ul>;
22}
23
24// ── Server Action: form mutation ──────────────────────────────────
25// app/actions.ts
26"use server";
27
28import { revalidatePath } from "next/cache";
29import { redirect } from "next/navigation";
30
31export async function createPost(formData: FormData) {
32 const title = formData.get("title") as string;
33 const content = formData.get("content") as string;
34
35 // Validate
36 if (!title || title.length < 3) throw new Error("Title too short");
37
38 // Save to DB
39 await db.post.create({ data: { title, content } });
40
41 revalidatePath("/blog"); // invalidate cached page
42 redirect("/blog"); // navigate after mutation
43}
44
45// Use in a Client Component
46"use client";
47import { createPost } from "./actions";
48
49export function CreatePostForm() {
50 return (
51 <form action={createPost}> {/* action = server action! */}
52 <input name="title" required />
53 <textarea name="content" required />
54 <button type="submit">Create Post</button>
55 </form>
56 );
57}
58
59// ── React Query: client-side data ─────────────────────────────────
60// bun add @tanstack/react-query
61"use client";
62import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
63
64export function PostList() {
65 const { data, isLoading, error } = useQuery({
66 queryKey: ["posts"],
67 queryFn: () => fetch("/api/posts").then(r => r.json()),
68 staleTime: 60_000, // consider fresh for 60s
69 });
70
71 if (isLoading) return <div>Loading...</div>;
72 if (error) return <div>Error!</div>;
73 return <ul>{data.map((p: Post) => <li key={p.id}>{p.title}</li>)}</ul>;
74}
75
76// Mutation with optimistic update
77export function LikeButton({ postId }: { postId: string }) {
78 const qc = useQueryClient();
79 const mutation = useMutation({
80 mutationFn: (id: string) => fetch(`/api/posts/${id}/like`, { method: "POST" }),
81 onSuccess: () => qc.invalidateQueries({ queryKey: ["posts"] }),
82 });
83
84 return <button onClick={() => mutation.mutate(postId)}>Like</button>;
85}
86
87type Post = { id: string; title: string };
88const db = { post: { create: async (data: any) => {} } };
← PREV6. Server vs Client ComponentsNEXT →8. API Routes & Middleware