# I like the future of Next.js

# 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](https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering) and the new granular cache system.

It's lit.

# The Big Picture

```javascript
// 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`

```javascript
// 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

```javascript
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

```javascript
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:

```javascript
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

```javascript
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

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

## Caching Hierarchy

```javascript
"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:

```javascript
// 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:

```javascript
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; // ...
}
```
