Skip to main content

Command Palette

Search for a command to run...

I like the future of Next.js

It's actually getting super cool.

Updated
4 min read
I like the future of Next.js
T

Just a guy who loves to write code and watch anime.

Introduction

I'm vibing with the future of Next.js.

Not sure if they're gonna change the story around server actions. I’ve not been the biggest fan. I’m not alone though, plenty of people aren’t too happy about them (see Twitter/X). I love how Remix does it with useFetcher .

React 19 makes the experience better for sure with useOptimistic and useFormStatus. But I believe a final piece is missing here for the optimal DX.

But I love PPR and the new granular cache system.

It's lit.

The Big Picture

// next.config.js
const config = {
  experimental: {
    // 1. Enable new caching system
    // You can now use `use cache` in your code
    dynamicIO: true,
    // 2. Optional: Define cache profiles
    cacheLife: {
      blog: {
        stale: 3600, // Client cache: 1 hour
        revalidate: 900, // Server refresh: 15 mins
        expire: 86400, // Max life: 1 day
      },
    },
  },
};

Basic Usage with use cache

// 1. File-level caching
"use cache";
export default function Page() {
  return <div>Cached Page</div>;
}

// 2. Component-level caching
export async function PriceDisplay() {
  "use cache";
  const price = await fetchPrice();
  return <div>${price}</div>;
}

// 3. Function-level caching
export async function getData() {
  "use cache";
  return await db.query();
}

Smart Caching with Tags

import { unstable_cacheTag as cacheTag } from 'next/cache'
import { revalidateTag } from 'next/cache'

// Tag cached data
export async function ProductList() {
  'use cache'
  cacheTag('products')  // Tag this cache entry
  const products = await fetchProducts()
  return <div>{products}</div>
}

// Invalidate when needed
export async function addProduct() {
  'use server'
  await db.products.add(...)
  revalidateTag('products')  // Clear cached data
}

Custom Cache Profiles

import { unstable_cacheLife as cacheLife } from "next/cache";

export async function BlogPosts() {
  "use cache";
  cacheLife("blog"); // Use the blog profile we defined
  return await fetchPosts();
}

Non-Obvious But Important Points

Cache Keys

  • Props and arguments automatically become part of the cache key.

  • Non-serializable values, like functions, become "unmodifiable references."

Example:

export async function UserCard({ id, onDelete }) {
  "use cache";
  // id becomes part of cache key
  // onDelete is passed through but doesn't affect caching
  const user = await fetchUser(id);
  return <div onClick={onDelete}>{user.name}</div>;
}

Interleaving Dynamic & Cached Content

export async function CachedWrapper({ children }) {
  "use cache";
  const header = await fetchHeader();
  return (
    <div>
      <h1>{header}</h1>
      {children} {/* Dynamic content passed through */}
    </div>
  );
}

Multiple Cache Tags

export async function ProductPage({ id }) {
  "use cache";
  cacheTag(["products", `product-${id}`, "featured"]);
  // Now can invalidate with any of these tags
}

Caching Hierarchy

"use cache";
export default async function Page() {
  // This whole page is cached EXCEPT dynamic part:
  return (
    <div>
      <CachedHeader />
      <div>
        {/* Dynamic section within cached page */}
        {/* Marked with Suspense */}
        <Suspense fallback={<Loading />}>
          <DynamicFeed />
        </Suspense>
      </div>
    </div>
  );
}

Type safety

I would probably have a constant for cache keys and life.

For cache life, I'd probably do something like this:

// You can then use this in your code
// Keeping it all type safe and avoiding magic strings
export const CACHE_LIFE_KEYS = {
  blog: "blog",
} as const;

const config = {
  experimental: {
    cacheLife: {
      [CACHE_LIFE_KEYS.blog]: {
        stale: 3600, // Client cache: 1 hour
        revalidate: 900, // Server refresh: 15 mins
        expire: 86400, // Max life: 1 day
      },
    },
  },
};

For tags, I'd go with something like the factory pattern when working with React Query:

export const CACHE_TAGS = {
  blog: {
    all: ["blog"] as const,
    list: () => [...CACHE_TAGS.blog.all, "list"] as const,
    post: (id: string) => [...CACHE_TAGS.blog.all, "post", id] as const,
    comments: (postId: string) =>
      [...CACHE_TAGS.blog.all, "post", postId, "comments"] as const,
  },
} as const;

function tagCache(tags: string[]) {
  // We need to spread because tags is an array and cacheTag expects
  // multiple arguments as plain strings
  cacheTag(...tags);
}

// Then use it like this:
export async function BlogList() {
  "use cache"; // Still need this
  tagCache(CACHE_TAGS.blog.list()); // This works for the spreading
  return; // ...
}
M

good article, love it.

one thing I don't like it, if I use a function that has "use cache" in the layout page, I need to wrap the layout page with suspense but on one upper level, why? page/component having the "use cache" function have to be wrapped with suspense boundary before it renders something like that and default loading page doesnt cover that for layout page.

faced it in the current canary version,

1