ā¤ Like
šŸ”– Save
šŸ”— Share
Eric Hu
Eric Hu

Data Fetching & Caching

@thedevspaceio

šŸ† Your Next.js app is slower than it could be. This is why

It's not your database, it's not your API. It because you're not unlocking the full power of Next.js data fetching and caching patterns.

I built this cheatsheet to help you fetch and cache data in Next.js with the best practices.

āœ… Fetching in server components āœ… Parallel and sequential fetching āœ… Fetching in client components āœ… Enabling Cache Components āœ… use cache āœ… cacheLife āœ… cacheTag āœ… On-demand revalidation āœ… Server actions āœ… Streaming uncached data

#nextjs #react #datafetching #caching #usecache #serveractions #webdev #frontend #coding #tips


Fetching in server components

Fetch directly in an async server component. No useEffect needed.

tsx
export default async function Posts() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();
 
  return <PostList posts={posts} />;
}

Requests to the same URL are automatically deduplicated during a render pass.


Parallel and sequential fetching

Start requests in parallel to avoid waterfalls.

tsx
// āœ… Parallel: start both, then await both
const userPromise = getUser(id);
const postsPromise = getPosts(id);
 
const [user, posts] = await Promise.all([userPromise, postsPromise]);
tsx
// āŒ Sequential: posts waits for user
const user = await getUser(id);
const posts = await getPosts(id);

Fetching in client components

Use React's use API to stream data from the server. Fetch in a server component and pass the promise down without awaiting it.

Server component:

tsx
import { Suspense } from "react";
 
export default function Page() {
  const posts = getPosts(); // don't await
 
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Posts posts={posts} />
    </Suspense>
  );
}

Client component:

tsx
"use client";
 
import { use } from "react";
 
export default function Posts({ posts }) {
  const allPosts = use(posts); // unwraps the promise
 
  return <PostList posts={allPosts} />;
}

For polling, mutations, and client-side cache management, use a community library like SWR or React Query.


Enabling Cache Components

Opt in via next.config.ts.

ts
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  cacheComponents: true,
};
 
export default nextConfig;

use cache

Cache the return value of an async function or component. Arguments become part of the cache key.

tsx
import { cacheLife } from "next/cache";

Data-level: cache a function.

tsx
export async function getUsers() {
  "use cache";
  cacheLife("hours");
 
  return db.query("SELECT * FROM users");
}

UI-level: cache an entire component.

tsx
export default async function Page() {
  "use cache";
  cacheLife("hours");
 
  const users = await getUsers();
  return <UserList users={users} />;
}

Add "use cache" at the top of a file to cache all its exports.


cacheLife

Set the cache lifetime with a profile or a custom config.

tsx
"use cache";
 
cacheLife("hours");
ProfileStaleRevalidateExpire
seconds30s1s60s
minutes5m1m1h
hours5m1h1d
days5m1d1w
weeks5m1w30d
max5m30d1y

Fine-grained control with an object.

tsx
"use cache";
 
cacheLife({
  stale: 3600, // fresh for 1 hour
  revalidate: 7200, // revalidate after 2 hours
  expire: 86400, // expire after 1 day
});

cacheTag

Tag cached data so it can be invalidated on demand.

tsx
import { cacheLife, cacheTag } from "next/cache";
 
export async function getPosts() {
  "use cache";
  cacheLife("max");
  cacheTag("posts");
 
  return db.query("SELECT * FROM posts");
}

Reuse the same tag across functions to invalidate them together.


On-demand revalidation

Invalidate tags from a server action or route handler.

tsx
"use server";
 
import { revalidateTag, updateTag, revalidatePath } from "next/cache";
 
export async function createPost(formData) {
  const post = await db.post.create({ ... });
 
  updateTag("posts"); // user sees their change immediately
  revalidatePath("/blog"); // or invalidate by path
}

FunctionBehaviorWhere
updateTag()Expires immediatelyServer actions only
revalidateTag()Stale-while-revalidateServer actions, route handlers
revalidatePath()Invalidates a whole pathServer actions, route handlers
tsx
revalidateTag("posts", "max"); // longest stale window

Prefer tags over paths. They are more precise and avoid over-invalidating.


Server actions

Mutate data with functions marked "use server". Call them from forms or events.

app/actions.ts

tsx
"use server";
 
import { updateTag } from "next/cache";
 
export async function createPost(formData) {
  const title = formData.get("title");
  await db.post.create({ data: { title } });
  updateTag("posts");
}

app/blog/new/page.tsx

tsx
import { createPost } from "../actions";
 
export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <button type="submit">Create</button>
    </form>
  );
}

Forms work without JavaScript thanks to progressive enhancement.


Streaming uncached data

Skip use cache for data that must be fresh on every request. Wrap it in Suspense instead.

tsx
import { Suspense } from "react";
 
async function LatestPosts() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();
  return <PostList posts={posts} />;
}
 
export default function Page() {
  return (
    <>
      <h1>My Blog</h1> {/* static shell */}
      <Suspense fallback={<p>Loading posts...</p>}>
        <LatestPosts /> {/* streams at request time */}
      </Suspense>
    </>
  );
}

Runtime APIs like cookies(), headers(), and searchParams also stream behind Suspense.

Full-Stack AI Developer Roadmap

From HTML & CSS to working with AI models, all in one structured roadmap.

@thedevspaceio
www.thedevspace.io