Remove next/cache usage (use cache, cacheLife, cacheTag, cacheComponents)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F22Ms5Cxam5UHyxaCrtZzu
This commit is contained in:
Rami Bitar
2026-07-29 11:26:23 -04:00
co-authored by Claude Fable 5
parent f23332e42c
commit 1e58ba4ca4
15 changed files with 24 additions and 81 deletions
+1 -2
View File
@@ -37,8 +37,7 @@ type AccountPageProps = {
/**
* `/account` — core Customer Accounts surface (name + email only).
*
* No `export const dynamic` / `export const fetchCache`: under
* `cacheComponents: true`, `force-dynamic` is not allowed. The page relies on
* No `export const dynamic` / `export const fetchCache`: the page relies on
* the `AppShell` dynamic context (`await connection()` in the root layout's
* shell), which opts the whole subtree into dynamic rendering — any
* `headers()`/`cookies()` read inside (e.g. `getCustomerAccessToken()` →
+3 -3
View File
@@ -17,9 +17,9 @@ import { Providers } from "./providers";
/**
* Async server shell that owns the per-request (dynamic) reads: the cart seed
* + the shop analytics GID. With `cacheComponents: true`, uncached/dynamic data
* accessed in a Server Component must sit inside a `<Suspense>` boundary so the
* static HTML shell prerenders and the per-buyer parts stream
* + the shop analytics GID. The dynamic data access in this Server Component
* sits inside a `<Suspense>` boundary so the static HTML shell prerenders and
* the per-buyer parts stream
* (`next/server` `connection()` + `headers()`/`cookies()` are per-request).
*
* Rendered inside `<Suspense>` from the root layout. `await connection()`
+3 -3
View File
@@ -26,9 +26,9 @@ export const metadata: Metadata = {
* `CartProvider` would create a divergent inner store vs the root-rendered
* `CartDrawer` — mutations would not reflect across them.
*
* `force-dynamic` is not allowed with `cacheComponents: true`; the root layout
* already makes every route dynamic via `cookies()`/`headers()` in the cart
* seed. Reachable via the footer `/cart` link (no-JS fallback).
* No `force-dynamic` needed; the root layout already makes every route
* dynamic via `cookies()`/`headers()` in the cart seed. Reachable via the
* footer `/cart` link (no-JS fallback).
*/
export default function CartPage() {
return (
+1 -8
View File
@@ -1,7 +1,6 @@
import { parseCollectionParams, type AvailableFilter } from "@shopify/hydrogen";
import type { ProductFilter as StorefrontApiProductFilter } from "@shopify/hydrogen/storefront-api-types";
import type { Metadata } from "next";
import { cacheLife, cacheTag } from "next/cache";
import { notFound } from "next/navigation";
import { CollectionBrowser } from "@/components/CollectionBrowser";
@@ -43,13 +42,7 @@ async function fetchCollection(
availableFilters: AvailableFilter[];
pageInfo: { hasNextPage: boolean; endCursor?: string | null };
}> {
"use cache";
cacheLife("minutes");
cacheTag("collections", "products");
// Reconstruct URLSearchParams from the serialized search string. `use cache`
// serializes arguments, so a URLSearchParams passed in loses `.get` — pass a
// plain string across the cache boundary and parse inside.
// Reconstruct URLSearchParams from the serialized search string.
const browse = parseCollectionParams(new URLSearchParams(searchString));
const result = await query(handle, after, browse.filters, browse.sortKey, browse.reverse);
return result;
-5
View File
@@ -1,5 +1,4 @@
import type { Metadata } from "next";
import { cacheLife, cacheTag } from "next/cache";
import Link from "next/link";
import { Breadcrumbs } from "@/components/Breadcrumbs";
@@ -21,10 +20,6 @@ export const metadata: Metadata = {
};
async function fetchCollections(after?: string) {
"use cache";
cacheLife("hours");
cacheTag("collections");
const { data, errors } = await staticStorefrontClient.graphql(COLLECTIONS_QUERY, {
variables: { first: 24, after },
});
+2 -2
View File
@@ -8,8 +8,8 @@ import { SITE_ORIGIN } from "@/lib/site";
import { AppShell } from "./app-shell";
/**
* Root layout (engineering.md F1, F4, F7, F10). With `cacheComponents: true`,
* the layout is a **static shell** — it prerenders the `<html>`/`<body>` +
* Root layout (engineering.md F1, F4, F7, F10). The layout is a
* **static shell** — it prerenders the `<html>`/`<body>` +
* announcement bar, then wraps the per-request (dynamic) `AppShell` (cart seed
* + analytics shop + chrome) in `<Suspense>` so the dynamic parts stream while
* the static shell serves immediately. `AppShell` calls `connection()` to opt
+2 -2
View File
@@ -13,8 +13,8 @@ import { getStorefrontClient } from "@/lib/storefront";
* `references/nextjs.md`). `proxy.ts` cannot inspect the routed response, so
* Storefront URL redirects run here, after the framework returns a 404.
*
* With `cacheComponents: true`, the per-request reads (`headers()` +
* `getStorefrontClient()` + `redirect()`) must sit inside a `<Suspense>`
* The per-request reads (`headers()` + `getStorefrontClient()` +
* `redirect()`) sit inside a `<Suspense>`
* boundary. The static 404 shell prerenders; the `<RedirectChecker>` streams
* and either `redirect()`s to a matching Shopify URL redirect or renders
* nothing (leaving the 404 shell visible).
+1 -7
View File
@@ -1,5 +1,4 @@
import type { Metadata } from "next";
import { cacheLife, cacheTag } from "next/cache";
import Link from "next/link";
import { CollectionCard } from "@/components/CollectionCard";
@@ -22,13 +21,8 @@ export const metadata: Metadata = {
twitter: { card: "summary_large_image" },
};
/** Home catalog fetch — cached at the data boundary (F2: dynamic root layout
* precludes page-level prerender, so the cache-point is the fetch function). */
/** Home catalog fetch. */
async function fetchHome() {
"use cache";
cacheLife("minutes");
cacheTag("products", "collections");
const { data, errors } = await staticStorefrontClient.graphql(HOME_QUERY);
if (errors) {
console.error("[hydrogen] Home query failed", errors);
+1 -14
View File
@@ -1,6 +1,5 @@
import { getSelectedProductOptions } from "@shopify/hydrogen";
import type { Metadata } from "next";
import { cacheLife, cacheTag } from "next/cache";
import { notFound } from "next/navigation";
import { Suspense } from "react";
@@ -19,8 +18,6 @@ type Props = {
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { handle } = await params;
// Re-fetch the product for metadata. It hits the same `use cache` cache-point
// as the page render, so this is a cache hit in practice.
const { product } = await fetchProduct(handle, "");
const title = product?.title ?? "Product";
return {
@@ -41,15 +38,9 @@ async function fetchProduct(
handle: string,
searchString: string,
): Promise<{ product: ProductData | null }> {
"use cache";
cacheLife("minutes");
cacheTag("products");
// Read variant selection from URL option params (F4: a no-JS GET to
// `?Size=Large&Color=Green` resolves the variant server-side). Do NOT pass
// `optionNames: []` — an empty allow-list filters out every param.
// Reconstruct URLSearchParams from the serialized search string — `use cache`
// serializes arguments, so a URLSearchParams passed in loses `.get`.
const selectedOptions = getSelectedProductOptions(new URLSearchParams(searchString));
const { data, errors } = await staticStorefrontClient.graphql(PRODUCT_QUERY, {
@@ -89,13 +80,9 @@ export default async function ProductPage({ params, searchParams }: Props) {
/**
* Related products (`notes/product.md` "you may also like"). Best-effort: any
* error/timeout returns an empty list so the PDP never breaks. Fetched via the
* shared `staticStorefrontClient` inside a `use cache` cache-point.
* shared `staticStorefrontClient`.
*/
async function RelatedProducts({ handle }: { handle: string }) {
"use cache";
cacheLife("minutes");
cacheTag("products");
let related: ProductCardData[] = [];
try {
const { data, errors } = await staticStorefrontClient.graphql(RELATED_PRODUCTS_QUERY, {
+1 -8
View File
@@ -2,7 +2,6 @@ import { parseCollectionParams, type StorefrontApi } from "@shopify/hydrogen";
import type { AvailableFilter } from "@shopify/hydrogen";
import type { ProductFilter as StorefrontApiProductFilter } from "@shopify/hydrogen/storefront-api-types";
import type { Metadata } from "next";
import { cacheLife, cacheTag } from "next/cache";
import { CollectionBrowser } from "@/components/CollectionBrowser";
import { SEARCH_QUERY } from "@/lib/queries";
@@ -37,10 +36,6 @@ type SearchNode = NonNullable<
type SearchProductNode = Extract<SearchNode, { __typename: "Product" }>;
async function fetchSearch(term: string, searchString: string): Promise<SearchResult> {
"use cache";
cacheLife("minutes");
cacheTag("products");
if (!term) {
return {
term: "",
@@ -51,9 +46,7 @@ async function fetchSearch(term: string, searchString: string): Promise<SearchRe
};
}
// Reconstruct URLSearchParams from the serialized search string. `use cache`
// serializes arguments, so a URLSearchParams passed in loses `.get` — pass a
// plain string across the cache boundary and parse inside.
// Reconstruct URLSearchParams from the serialized search string.
const searchParams = new URLSearchParams(searchString);
const browse = parseCollectionParams(searchParams);
// parseCollectionParams returns a ProductCollectionSortKeys; search only
+2 -8
View File
@@ -1,5 +1,4 @@
import type { MetadataRoute } from "next";
import { cacheLife, cacheTag } from "next/cache";
import { SITEMAP_QUERY } from "@/lib/queries";
import { SITE_ORIGIN } from "@/lib/site";
@@ -7,9 +6,8 @@ import { staticStorefrontClient } from "@/lib/storefront-static";
/**
* `/sitemap.xml` (engineering.md F10). Lists product + collection URLs with
* `updatedAt` timestamps, fetched via the shared `staticStorefrontClient`
* inside a `use cache` cache-point (catalog, not personalized). mock.shop
* fallback works (queries mock.shop).
* `updatedAt` timestamps, fetched via the shared `staticStorefrontClient`.
* mock.shop fallback works (queries mock.shop).
*/
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const entries = await fetchSitemap();
@@ -23,10 +21,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
type SitemapEntry = { loc: string; lastmod?: string };
async function fetchSitemap(): Promise<SitemapEntry[]> {
"use cache";
cacheLife("hours");
cacheTag("products", "collections");
const { data, errors } = await staticStorefrontClient.graphql(SITEMAP_QUERY);
if (errors) {
console.error("[hydrogen] Sitemap query failed", errors);
+1 -1
View File
@@ -21,7 +21,7 @@ import { FilterGroup } from "@/lib/filters";
/**
* Shared collection/search browser (`hydrogen-collection-browser` /
* `references/nextjs.md`). The server page fetches the product + filter
* snapshot via `staticStorefrontClient` inside a `use cache` cache-point and
* snapshot via `staticStorefrontClient` and
* passes it here. This client component owns the `CollectionProvider`,
* `useCollection`/`useCollectionForm`, URL sync via `useRouter`/`useSearchParams`,
* and the filter/sort/grid UI.
+4 -11
View File
@@ -1,16 +1,14 @@
import "server-only";
import { analyticsShop as analyticsShopConfig, shop as shopConfig } from "@shared/config";
import { cacheLife, cacheTag } from "next/cache";
import { SHOP_ANALYTICS_QUERY } from "@/lib/queries";
import { staticStorefrontClient } from "@/lib/storefront-static";
/**
* Resolve the shop analytics GID best-effort + non-blocking (engineering.md F1).
* The query runs inside a `'use cache'` cache-point (`cacheLife("hours")`,
* `cacheTag("shop")`) so warm requests resolve instantly. On timeout/error we
* fall back to the config-derived shop GID/name (avoids drift — `@shared/config`
* `analyticsShop.shopId` is already `gid://shopify/Shop/${shop.shopId}`).
* On timeout/error we fall back to the config-derived shop GID/name (avoids
* drift — `@shared/config` `analyticsShop.shopId` is already
* `gid://shopify/Shop/${shop.shopId}`).
*/
export type AnalyticsShop = {
shopId: string;
@@ -24,12 +22,7 @@ const SHOP_FALLBACK: AnalyticsShop = {
shopDescription: null,
};
/** Cache the shop query result for hours (it almost never changes). */
async function fetchShopAnalytics(): Promise<AnalyticsShop> {
"use cache";
cacheLife("hours");
cacheTag("shop");
const { data, errors } = await staticStorefrontClient.graphql(SHOP_ANALYTICS_QUERY);
if (errors) {
console.error("[hydrogen] Root shop query failed", errors);
@@ -42,7 +35,7 @@ async function fetchShopAnalytics(): Promise<AnalyticsShop> {
}
/**
* Best-effort, non-blocking shop analytics resolution. Races the cached query
* Best-effort, non-blocking shop analytics resolution. Races the query
* against a 2000ms timeout; on timeout/error falls back to `@shared/config`.
* Merges the resolved GID/name with the config-derived `analyticsShop`
* metadata (acceptedLanguage/currency/hydrogenSubchannelId).
+2 -3
View File
@@ -14,9 +14,8 @@ import { resolveStorefrontConfig } from "./storefront-config";
* sitemap, related products, shop analytics GID) fetch through this client.
* Only the cart seed uses the per-buyer `getStorefrontClient()`.
*
* Caching lives at the `use cache` boundary (cache-points keyed by serializable
* inputs). The `cache:` option is never passed to `graphql()` — Next native
* data cache + `cacheLife`/`cacheTag` replace the Oxygen sub-request LRU.
* The `cache:` option is never passed to `graphql()` (no Oxygen sub-request
* LRU in this Next.js port).
*/
const requestContext = createShopifyRequestContext({
request: { headers: new Headers() },
-4
View File
@@ -3,14 +3,10 @@ import type { NextConfig } from "next";
/**
* Next.js 16 config for the Hydrogen example.
*
* `cacheComponents: true` enables Cache Components / `use cache` (engineering.md
* F2 native Next caching instead of the Oxygen sub-request LRU).
*
* No `next/image` remote patterns — we use plain `<img>` + the `hydrogen-image`
* helper (F12; Hydrogen ships no Image component and we size CDN URLs ourselves).
*/
const nextConfig: NextConfig = {
cacheComponents: true,
// React Strict Mode is disabled because `@shopify/hydrogen/react`'s
// `PredictiveSearchProvider` destroys its store in the Strict Mode effect
// cleanup (double-invoke), and `useMemo` returns the same (now-destroyed)