A demo blog exploring Async React patterns with Next.js 16's Cache Components. Each post covers a real pattern used in the app, from caching and streaming to optimistic updates and transitions, focusing on crafting great UX in the in-between states.
Built with Next.js 16, React 19, Prisma, TailwindCSS v4, and shadcn/ui (Base UI).
bun install
bun run devOpen http://localhost:3000 in your browser.
This project uses Prisma Postgres. Set your connection string in .env:
DATABASE_URL="postgres://..."bun run prisma.generate # Generate the Prisma client
bun run prisma.push # Push schema to database
bun run prisma.seed # Seed blog posts
bun run prisma.studio # View data in Prisma StudioUsing SQLite instead: Change the provider in prisma/schema.prisma to sqlite, update db.ts to use @prisma/adapter-libsql, and set DATABASE_URL="file:./dev.db" in .env.
app/
[slug]/ # Public blog posts
dashboard/ # Admin dashboard
_components/ # Route-local components
[slug]/ # Post detail/edit
new/ # Create post
components/
design/ # Action prop components
ui/ # shadcn/ui primitives
data/
actions/ # Server Actions
queries/ # Data fetching with cache()
- components/ui — shadcn/ui components. Add with
bunx shadcn@latest add <component-name> - components/design — Components that expose Action props and handle async coordination internally
Every page folder should contain everything it needs. Components and functions live at the nearest shared space in the hierarchy.
Naming: PascalCase for components, kebab-case for files/folders, camelCase for functions/hooks. Suffix transition-based functions with "Action".
Cache Components: Uses cacheComponents: true to statically render server components that don't access dynamic data. Keep pages non-async and push dynamic data access into <Suspense> boundaries to maximize the static shell. Use "use cache" with cacheLife() to explicitly cache additional components or functions.
Async React: Replace manual isLoading/isError state with React 19's coordination primitives — useTransition for tracking async work, useOptimistic for instant feedback, Suspense for loading boundaries, and use() for reading promises during render. See AGENTS.md for detailed patterns and examples.
- Fetching data — Queries in
data/queries/, wrapped withcache(). Await in Server Components directly, or pass the promise to a client component and unwrap withuse(). - Mutating data — Server Actions in
data/actions/with"use server". Invalidate withrevalidateTag()+refresh(). UseuseTransition+useOptimisticfor pending state and instant feedback. - Navigation — Wrap route changes in
useTransitionto getisPendingfor loading UI. - Caching — Add
"use cache"withcacheLife()to pages, components, or functions to include them in the static shell. - Errors —
error.tsxfor boundaries,not-found.tsx+notFound()for 404s. Errors thrown inside transitions automatically reach the nearest error boundary.
Uses ESLint and Prettier with format-on-save in VS Code. Configuration in eslint.config.mjs and .prettierrc. Open the .code-workspace file to ensure correct extensions are set.
bun run buildDeploy to Vercel for the easiest experience with Prisma Postgres.
See the Next.js deployment docs for more details.