Your product page loads quickly but shows yesterday's prices. Your dashboard is always fresh but slow to render. The old mental model said "pick SSG or SSR." Cache Components instead lets a route combine a prerendered shell, cached shared content, and request-time content.
Outcome
A product page with an instant prefetched shell and cached data that revalidates on inventory updates, using Partial Prefetching, "use cache", cacheLife(), and cacheTag().
Fast Track
- Enable
cacheComponentsandpartialPrefetchinginnext.config.ts - Add "use cache" to expensive data functions so Next.js stores their return values
- Configure cacheLife() profile for time-based revalidation (e.g., stale 5min, revalidate 15min)
- Add
cacheTag()and chooseupdateTag()orrevalidateTag(tag, 'max')based on the freshness requirement
Prerendering with Cache Components
Cache Components is opt-in. After you enable cacheComponents: true, Next.js prerenders as much of a route as it can into HTML and an RSC payload (the serialized Server Component tree). The shell can include:
- Page layout and UI structure
- Non-dynamic content (text, images, static imports)
- Everything outside Suspense boundaries
Request APIs define request-time boundaries:
import { cookies, headers } from 'next/headers'
async function UserGreeting() {
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
// User-specific data, always fresh
const user = await fetchUser(userId)
return <h1>Welcome, {user.name}!</h1>
}Reading cookies(), headers(), or searchParams requires request-time work. Put that subtree behind <Suspense> so the surrounding shell can still prerender. Do not put request APIs inside a "use cache" scope; read them first and pass only the serializable value a cached function needs.
Suspense boundaries create "dynamic holes":
export default function ProductPage() {
return (
<div>
{/* Static shell - prerenders */}
<Header />
<ProductLayout>
{/* Dynamic hole - streams at runtime */}
<Suspense fallback={<Skeleton />}>
<ProductDetails />
</Suspense>
</ProductLayout>
<Footer />
</div>
)
}The static shell loads instantly. Dynamic content inside Suspense boundaries streams when ready. This is PPR (Partial Prerendering), a rendering strategy that combines static and dynamic content in a single route.
"use cache" Directive: Cache Expensive Data
The "use cache" directive stores the return value of async functions, components, or entire files. A cache miss still runs the function to produce a new value.
File-level caching:
"use cache"
// All exports in this file are cached
export async function getProducts() {
const res = await fetch('https://api.acme.com/products')
return res.json()
}
export async function getProductById(id: string) {
const res = await fetch(`https://api.acme.com/products/${id}`)
return res.json()
}Function-level caching:
import { cacheLife } from 'next/cache'
async function getProduct(id: string) {
"use cache"
cacheLife('products') // Use "products" profile from next.config.js
const res = await fetch(`https://api.acme.com/products/${id}`)
return res.json()
}
export default async function ProductPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const product = await getProduct(id)
return (
<div>
<h1>{product.name}</h1>
<p>Price: ${product.price}</p>
<p>Stock: {product.inventory}</p>
</div>
)
}Component-level caching:
async function ProductGrid({ category }: { category: string }) {
"use cache"
cacheLife('products')
const products = await fetchProductsByCategory(category)
return (
<div className="grid">
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
)
}cacheLife(): Time-Based Revalidation
Define cache profiles in next.config.js to control how long cached data stays fresh and when it revalidates:
import type { NextConfig } from 'next'
const config: NextConfig = {
cacheComponents: true,
partialPrefetching: true, // Enable Next.js 16.3 Instant Navigations
cacheLife: {
// Blog posts - longer cache, updates are rare
blog: {
stale: 3600, // 1 hour fresh
revalidate: 86400, // 24 hours before revalidation
expire: 604800, // 1 week max before forced regeneration
},
// Product catalog - moderate cache
products: {
stale: 300, // 5 minutes fresh
revalidate: 900, // 15 minutes before revalidation
expire: 3600, // 1 hour max
},
// Real-time data - minimal cache
social: {
stale: 60, // 1 minute fresh
revalidate: 300, // 5 minutes before revalidation
expire: 600, // 10 minutes max
},
},
}
export default configProfile semantics:
stale: Data is fresh for this duration (served from cache, no revalidation)revalidate: After this period, revalidate in the background using stale-while-revalidate (serve cached content while fetching fresh data in the background)expire: Maximum time before forced synchronous regeneration
Use profiles in functions:
import { cacheLife } from 'next/cache'
async function getBlogPost(slug: string) {
"use cache"
cacheLife('blog') // Use blog profile
const post = await fetchBlogPost(slug)
return post
}
async function getProducts() {
"use cache"
cacheLife('products') // Use products profile
const products = await fetchProducts()
return products
}cacheTag(), updateTag(), and revalidateTag()
Tag cache entries to invalidate them after mutations (create, update, delete):
import { cacheLife, cacheTag } from 'next/cache'
async function getProduct(id: string) {
"use cache"
cacheLife('products')
cacheTag('products', `product-${id}`) // Tag this cache entry
const res = await fetch(`https://api.acme.com/products/${id}`)
return res.json()
}Choose invalidation behavior deliberately:
updateTag(tag)immediately expires tagged data and is available only in Server Actions. Use it when the user must see their own write on the next render.revalidateTag(tag, 'max')marks tagged data stale and refreshes it on a later visit with stale-while-revalidate behavior. It works in Server Actions and Route Handlers and fits catalogs, webhooks, and background refreshes.- The single-argument
revalidateTag(tag)form is deprecated. It still represents the old immediate-expiry behavior; migrate that intent toupdateTag()in a Server Action or choose an explicit cache-life profile.
Immediately reflect a Server Action mutation:
"use server"
import { updateTag } from 'next/cache'
export async function updateProduct(id: string, data: ProductData) {
// Update in database
await db.products.update({ where: { id }, data })
// The author sees the updated detail and list immediately.
updateTag(`product-${id}`)
updateTag('products')
return { success: true }
}
export async function createProduct(data: ProductData) {
await db.products.create({ data })
// Only invalidate list, no specific product to invalidate
updateTag('products')
return { success: true }
}Call from client components:
"use client"
import { updateProduct } from '@/app/actions/products'
export function ProductForm({ product }: { product: Product }) {
async function handleSubmit(formData: FormData) {
const result = await updateProduct(product.id, {
name: formData.get('name') as string,
price: Number(formData.get('price')),
})
// The Server Action expires the affected tags with updateTag().
if (result.success) {
alert('Product updated!')
}
}
return (
<form action={handleSubmit}>
<input name="name" defaultValue={product.name} />
<input name="price" type="number" defaultValue={product.price} />
<button type="submit">Save</button>
</form>
)
}PPR: Place Suspense Close to Dynamic Content
PPR (Partial Prerendering) maximizes the static shell by placing Suspense boundaries close to dynamic content:
import { Suspense } from 'react'
import { cacheLife, cacheTag } from 'next/cache'
async function ProductDetails({ id }: { id: string }) {
"use cache"
cacheLife('products')
cacheTag('products', `product-${id}`)
const product = await fetchProduct(id)
return (
<div>
<h2>{product.name}</h2>
<p>${product.price}</p>
</div>
)
}
async function ProductReviews({ id }: { id: string }) {
// No cache - always fetch fresh reviews
const reviews = await fetchReviews(id)
return (
<div>
{reviews.map(r => (
<div key={r.id}>{r.comment}</div>
))}
</div>
)
}
export default async function ProductPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return (
<div>
{/* Static shell - prerenders instantly */}
<header>Product Page</header>
{/* Cached product data - loads fast from cache */}
<Suspense fallback={<div>Loading product...</div>}>
<ProductDetails id={id} />
</Suspense>
{/* Fresh reviews - streams after product */}
<Suspense fallback={<div>Loading reviews...</div>}>
<ProductReviews id={id} />
</Suspense>
<footer>Footer content</footer>
</div>
)
}Result:
- Static shell (header, layout, footer) loads instantly
- Cached product data loads from cache (fast)
- Fresh reviews stream when ready (dynamic)
Decision Framework
┌─────────────────────────────────────────────────────────────┐
│ CACHE DECISION TREE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Is the data user-specific? │
│ ├─ YES → Keep dynamic (no cache) │
│ └─ NO → Continue... │
│ │
│ Is staleness acceptable? │
│ ├─ NO → Keep dynamic │
│ └─ YES → Use "use cache" │
│ │
│ How should it revalidate? │
│ ├─ On schedule → cacheLife() with time profile │
│ └─ Own write → cacheTag() + updateTag() │
│ └─ Background refresh → revalidateTag(tag, 'max') │
│ │
└─────────────────────────────────────────────────────────────┘
Examples:
| Data Type | User-Specific? | Staleness OK? | Strategy |
|---|---|---|---|
| User dashboard | Yes | N/A | Dynamic (no cache) |
| Product catalog | No | Yes (5-15min) | "use cache" + cacheLife('products') |
| Blog posts | No | Yes (1-24hr) | "use cache" + cacheLife('blog') |
| Live sports scores | No | No | Dynamic (no cache) |
| Product detail | No | Yes, with invalidation | "use cache" + cacheTag() + updateTag() after an edit |
I'm implementing Cache Components in Next.js 16 and need help deciding on a caching strategy.
<context>
Next.js 16 Cache Components offer:
- `"use cache"` directive for shared caching
- `"use cache: private"` for per-user caching
- `cacheLife()` for time-based expiration (profiles: blog, products, social, etc.)
- `cacheTag()` + `updateTag()` for immediate mutation visibility
- `cacheTag()` + `revalidateTag(tag, 'max')` for stale-while-revalidate refreshes
</context>
<my-data>
**Data type:** _____
Example: "Product catalog", "User dashboard", "Blog posts", "Live sports scores"
**How often it changes:** _____
Example: "Every few hours", "When admin updates", "Real-time", "Once a day"
**Is it user-specific?**
- [ ] Same for all users (shared)
- [ ] Different per user (personalized)
**Staleness tolerance:** _____
Example: "5 minutes is fine", "Must be real-time", "1 hour acceptable"
**What triggers updates:** _____
Example: "Admin publishes", "User makes purchase", "External API webhook"
</my-data>
<current-implementation>
```tsx
// My current data fetching:
___PASTE_YOUR_DATA_FETCHING_CODE___
```
</current-implementation>
**Questions:**
1. Should I cache this data at all?
2. Which cacheLife profile fits? (blog, products, social, or custom)
3. Should I use time-based (cacheLife) or mutation-based (cacheTag) invalidation?
4. Shared cache or per-user cache?
Generate a complete caching implementation with the appropriate directives and invalidation strategy. Explain why each choice fits my data.Edge Cases and Gotchas
Understanding these edge cases prevents debugging headaches:
Edge Runtime Not Supported
"use cache" requires the Node.js runtime. Edge Runtime rejects the directive:
// ❌ FAILS - Edge Runtime doesn't support "use cache"
export const runtime = 'edge'
async function getData() {
"use cache" // Error: "use cache" is not supported in Edge Runtime
return fetch('https://api.example.com/data')
}Cache Components require the Node.js runtime. If a route must use the Edge Runtime, do not enable Cache Components for that application; use an external cache or provider-specific caching appropriate to that runtime instead.
Serialization Required
All arguments and return values must be serializable (convertible to JSON format, meaning only primitive values, arrays, and plain objects):
// ❌ FAILS - Functions aren't serializable
async function getData(callback: () => void) {
"use cache"
callback() // Error: callback is not serializable
return fetch('/api/data')
}
// ❌ FAILS - Symbols aren't serializable
async function getData(config: { [Symbol.for('key')]: string }) {
"use cache"
return fetch('/api/data')
}
// ✅ WORKS - Primitive arguments and plain objects
async function getData(id: string, options: { limit: number }) {
"use cache"
return fetch(`/api/data/${id}?limit=${options.limit}`)
}Nested Caching Behavior
When cached functions call other cached functions, the innermost cacheLife() wins unless the outer function also specifies one:
// Outer cache without cacheLife → inner controls timing
async function outer() {
"use cache"
return inner() // inner's cacheLife wins
}
async function inner() {
"use cache"
cacheLife('hours') // This controls the cache timing
return fetch('/api/data')
}
// Outer cache WITH cacheLife → outer wins
async function outerWithLife() {
"use cache"
cacheLife('minutes') // This overrides inner
return inner()
}map() Pitfall: Async Callbacks Don't Work
You cannot use "use cache" inside array callbacks like map():
// ❌ FAILS - "use cache" inside map callback
const results = items.map(async (item) => {
"use cache" // Error: Directive must be at function body start
return processItem(item)
})
// ✅ WORKS - Extract to named function
async function processItem(id: string) {
"use cache"
cacheTag(`item-${id}`)
return fetch(`/api/items/${id}`)
}
const results = await Promise.all(
items.map((item) => processItem(item.id))
)All "use cache" Functions Must Be Async
The "use cache" directive only works with async functions:
// ❌ FAILS - Synchronous function
function getConfig() {
"use cache" // Error: "use cache" functions must be async
return { theme: 'dark' }
}
// ✅ WORKS - Async function
async function getConfig() {
"use cache"
return { theme: 'dark' }
}cacheTag Limits
Tags have size and count limits:
- Maximum tag length: 256 characters
- Maximum tags per entry: 128 tags
// ❌ FAILS - Tag too long
cacheTag('a'.repeat(300)) // Error: Tag exceeds 256 characters
// ❌ FAILS - Too many tags
cacheTag(...Array(200).fill('tag')) // Error: Exceeds 128 tags
// ✅ WORKS - Use hierarchical tags
cacheTag('products', `product-${id}`, `category-${categoryId}`)When to Use connection()
┌─────────────────────────────────────────────────────────────┐
│ connection() DECISION TREE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Does the ENTIRE page need to be dynamic? │
│ ├─ NO → Use Suspense + "use cache" for partial caching │
│ └─ YES → Continue... │
│ │
│ Is there ANY cacheable content on the page? │
│ ├─ YES → Use Suspense to isolate dynamic parts │
│ └─ NO → Continue... │
│ │
│ Why is the entire page dynamic? │
│ ├─ CSP nonces → connection() ✓ │
│ ├─ All user-specific → connection() ✓ │
│ ├─ Realtime updates → Client Component ✓ │
│ └─ "It's easier" → Suspense + "use cache" ✓ │
│ │
└─────────────────────────────────────────────────────────────┘
Valid uses of connection():
- CSP nonces that must be unique per request
- Pages where 100% of content is user-specific
- Webhook handlers that must process every request
Anti-pattern uses of connection():
- "Quick fix" for Date.now() errors
- Pages with any cacheable content
- Avoiding Suspense boundaries
Cache Variants
Next.js 16 supports three cache variants for different use cases:
| Variant | Scope | Use Case |
|---|---|---|
"use cache" | Shared across all users | Public data (products, blog posts) |
"use cache: private" | Per-user cache | User-specific but cacheable (dashboard) |
"use cache: remote" | Remote cache (Vercel) | Distributed caching across regions |
// Public data - shared cache (default)
async function getProducts() {
"use cache"
return fetch('/api/products')
}
// User-specific but cacheable
async function getUserDashboard(userId: string) {
"use cache: private"
cacheTag(`user-${userId}`)
return fetch(`/api/users/${userId}/dashboard`)
}
// Distributed cache (Vercel infrastructure)
async function getGlobalConfig() {
"use cache: remote"
return fetch('/api/config')
}Hands-On Exercise 3.1
Requirements:
- Enable
cacheComponentsandpartialPrefetchinginnext.config.ts - Add "use cache" to a product data function in
apps/web/src/lib/products.ts - Configure a
productscacheLife profile innext.config.ts(stale: 5min, revalidate: 15min, expire: 1hr) - Add
cacheTag()to tag product cache entries withproduct-${id}andproducts - Create a Server Action in
apps/web/src/app/actions/products.tsthat updates a product and callsupdateTag() - Test cache and navigation behavior with the Network tab and Next.js DevTools
Implementation hints:
- Use
cacheLife('products')inside the cached function, after the "use cache" directive cacheTag()accepts multiple tags:cacheTag('products', 'product-123')- Server Actions must have
"use server"at the top - Call
updateTag(tag)after a user mutation when the next render must show the write immediately - Reserve
revalidateTag(tag, 'max')for stale-while-revalidate updates, including Route Handler webhooks - Verify with
curl -I http://localhost:3000/products/123to see cache headers
Try It
-
Verify static shell prerendering:
- Run
pnpm buildand check build output - Look for
○(static) vsƒ(dynamic) indicators - Static routes show prerendered HTML size
- Run
-
Test cache behavior:
- Load product page:
http://localhost:3000/products/123 - Open Network tab, check response headers for cache status
- Reload page, verify cached response (should be instant)
- Load product page:
-
Test cache invalidation:
- Trigger
updateTag('product-123')via the Server Action - Reload product page, verify fresh data appears
- Check Network tab for cache miss followed by new cache entry
- Trigger
-
Measure performance:
- Use Lighthouse to measure FCP (First Contentful Paint)
- Compare static shell (instant) vs dynamic content (streaming)
Commit & Deploy
git add -A
git commit -m "feat(advanced): implement Cache Components with cacheLife and cacheTag"
git push -u origin feat/cache-componentsDone-When
- Product page loads instantly (static shell prerendered, visible in build output)
- Next.js DevTools reports the product navigation as instant
- Product data shows cached values (verify cache headers in Network tab)
- After calling
updateTag(), the action's next render shows fresh data - Build output shows static routes with appropriate cache configuration
Troubleshooting
"Cannot use cookies() inside 'use cache'"
- Extract
cookies(),headers(), orsearchParamsoutside the cached function - Pass values as function arguments instead
// ❌ BAD
async function getData() {
"use cache"
const cookieStore = await cookies() // ERROR
return fetchData(cookieStore.get('token'))
}
// ✅ GOOD
async function getData(token: string) {
"use cache"
return fetchData(token)
}
// In page component
const cookieStore = await cookies()
const data = await getData(cookieStore.get('token')?.value!)"revalidateTag() requires 2 arguments" or "Expected 2 arguments, but got 1"
The single-argument form is deprecated. Choose the API that matches the desired user experience:
// Deprecated immediate-expiry form
revalidateTag('products')
// Stale-while-revalidate; valid in Server Actions and Route Handlers
revalidateTag('products', 'max')
// Immediate read-your-own-writes; Server Actions only
updateTag('products')revalidateTag(tag, 'max') marks cached data stale and refreshes it on the next visit. updateTag(tag) expires it immediately. An explicit { expire: 0 } profile remains available for advanced server cases, but it should not be taught as the default replacement for updateTag().
"Error: Route /path used Date.now() which is dynamic"
Using Date.now() or new Date() in Server Components causes prerender errors because timestamps are inherently dynamic:
// ❌ Causes prerender error
export default async function Page() {
const timestamp = Date.now() // ERROR during build
return <p>Generated at {timestamp}</p>
}Solutions:
- Wrap in "use cache" (caches the timestamp at cache creation time):
async function getTimestamp() {
"use cache"
return Date.now() // Cached when cache entry is created
}- Use
connection()to opt into dynamic rendering:
import { connection } from 'next/server'
export default async function Page() {
await connection() // Opts into dynamic rendering
const timestamp = Date.now() // Now allowed
return <p>Generated at {timestamp}</p>
}- Move to client component (if you need real-time updates):
"use client"
import { useState, useEffect } from 'react'
export function LiveTimestamp() {
const [time, setTime] = useState<number | null>(null)
useEffect(() => {
setTime(Date.now())
}, [])
return <p>Current time: {time}</p>
}"cacheComponents is not a valid experimental option"
In Next.js 16.3, cacheComponents remains a top-level config option:
// ❌ Old config (Next.js 16.0.x)
const config: NextConfig = {
experimental: {
cacheComponents: true, // No longer valid here
},
}
// ✅ Current config (Next.js 16.3)
const config: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
}"I still see stale data after revalidateTag()"
- Check tag spelling matches exactly (case-sensitive)
- Verify
cacheTag()was called in the cached function - Ensure
revalidateTag(tag, 'max')is called AFTER the mutation succeeds - Remember that
'max'intentionally permits stale content while refreshing on a later visit - If a Server Action must show its own write immediately, use
updateTag(tag)instead
"The route has no useful prerendered shell"
- Move
<Suspense>closer to the request-time content - Check whether
cookies(),headers(), orsearchParamsare read above the boundary - Move shared work into an explicit
"use cache"scope; do not combine Cache Components withdynamic = 'force-static' - Inspect the route with Next.js DevTools and production build output
"Cache not working in development"
- Cache behavior differs in
devvsbuildmode - Run
pnpm build && pnpm startto test production caching - Development mode bypasses some caching for fast refresh
Solution
Click to reveal solution
import type { NextConfig } from 'next'
const config: NextConfig = {
cacheComponents: true,
partialPrefetching: true, // Enable Next.js 16.3 Instant Navigations
cacheLife: {
blog: {
stale: 3600, // 1 hour
revalidate: 86400, // 24 hours
expire: 604800, // 1 week
},
products: {
stale: 300, // 5 minutes
revalidate: 900, // 15 minutes
expire: 3600, // 1 hour
},
social: {
stale: 60, // 1 minute
revalidate: 300, // 5 minutes
expire: 600, // 10 minutes
},
},
}
export default config"use cache"
import { cacheLife, cacheTag } from 'next/cache'
/**
* Fetch a single product by ID with caching.
* Cache invalidated via updateTag() after user mutations.
*/
export async function getProduct(id: string) {
cacheLife('products') // 5min fresh, 15min revalidate, 1hr expire
cacheTag('products', `product-${id}`) // Tag for invalidation
const res = await fetch(`https://api.acme.com/products/${id}`)
if (!res.ok) throw new Error('Failed to fetch product')
return res.json() as Promise<Product>
}
/**
* Fetch all products with caching.
*/
export async function getProducts() {
cacheLife('products')
cacheTag('products') // Invalidate when any product changes
const res = await fetch('https://api.acme.com/products')
if (!res.ok) throw new Error('Failed to fetch products')
return res.json() as Promise<Product[]>
}
interface Product {
id: string
name: string
price: number
inventory: number
}import { getProduct } from '@/lib/products'
import { Suspense } from 'react'
export default async function ProductPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return (
<div>
<header>
<h1>Product Details</h1>
</header>
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails id={id} />
</Suspense>
<footer>ACME Corp</footer>
</div>
)
}
async function ProductDetails({ id }: { id: string }) {
const product = await getProduct(id)
return (
<div>
<h2>{product.name}</h2>
<p>Price: ${product.price}</p>
<p>In Stock: {product.inventory}</p>
</div>
)
}
function ProductSkeleton() {
return (
<div className="animate-pulse">
<div className="h-8 w-48 bg-gray-200 rounded mb-4" />
<div className="h-6 w-32 bg-gray-200 rounded mb-2" />
<div className="h-6 w-24 bg-gray-200 rounded" />
</div>
)
}"use server"
import { updateTag } from 'next/cache'
import { db } from '@/lib/db'
export async function updateProduct(
id: string,
data: { name?: string; price?: number; inventory?: number }
) {
try {
// Update in database
await db.products.update({
where: { id },
data,
})
// The next render observes the successful write.
updateTag(`product-${id}`)
updateTag('products')
return { success: true }
} catch (error) {
console.error('Failed to update product:', error)
return { success: false, error: 'Failed to update product' }
}
}
export async function deleteProduct(id: string) {
try {
await db.products.delete({ where: { id } })
updateTag(`product-${id}`)
updateTag('products')
return { success: true }
} catch (error) {
console.error('Failed to delete product:', error)
return { success: false, error: 'Failed to delete product' }
}
}"use client"
import { updateProduct } from '@/app/actions/products'
import { useState } from 'react'
export function ProductForm({ product }: { product: Product }) {
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('')
async function handleSubmit(formData: FormData) {
setLoading(true)
setMessage('')
const result = await updateProduct(product.id, {
name: formData.get('name') as string,
price: Number(formData.get('price')),
inventory: Number(formData.get('inventory')),
})
setLoading(false)
if (result.success) {
setMessage('Product updated! Cache invalidated.')
} else {
setMessage(result.error || 'Failed to update')
}
}
return (
<form action={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
defaultValue={product.name}
className="w-full border rounded px-3 py-2"
/>
</div>
<div>
<label htmlFor="price">Price</label>
<input
id="price"
name="price"
type="number"
defaultValue={product.price}
className="w-full border rounded px-3 py-2"
/>
</div>
<div>
<label htmlFor="inventory">Inventory</label>
<input
id="inventory"
name="inventory"
type="number"
defaultValue={product.inventory}
className="w-full border rounded px-3 py-2"
/>
</div>
<button
type="submit"
disabled={loading}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
{loading ? 'Saving...' : 'Save Changes'}
</button>
{message && (
<p className={message.includes('success') ? 'text-green-600' : 'text-red-600'}>
{message}
</p>
)}
</form>
)
}
interface Product {
id: string
name: string
price: number
inventory: number
}References
Was this helpful?