<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Safdar Ali</title>
    <description>The latest articles on DEV Community by Safdar Ali (@safdarali25).</description>
    <link>https://dev.to/safdarali25</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2398981%2F54da62cd-54f1-41f2-8343-10121c73537c.jpg</url>
      <title>DEV Community: Safdar Ali</title>
      <link>https://dev.to/safdarali25</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/safdarali25"/>
    <language>en</language>
    <item>
      <title>Prisma + PostgreSQL + Next.js — Full Stack Setup Guide</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Fri, 30 Oct 2026 13:40:37 +0000</pubDate>
      <link>https://dev.to/safdarali25/prisma-postgresql-nextjs-full-stack-setup-guide-26en</link>
      <guid>https://dev.to/safdarali25/prisma-postgresql-nextjs-full-stack-setup-guide-26en</guid>
      <description>&lt;p&gt;I'm &lt;a href="https://safdarali.in/about" rel="noopener noreferrer"&gt;Safdar Ali&lt;/a&gt;. When a Next.js project needs real persistence — users, posts, orders — I default to Prisma over PostgreSQL. Not because ORMs are magic, but because typed queries, migrations, and App Router server components fit together cleanly. This prisma nextjs tutorial walks from zero to production queries, the same stack I use when a client needs a full product, not just a marketing shell on &lt;a href="https://safdarali.in/" rel="noopener noreferrer"&gt;safdarali.in&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="prerequisites"&gt;Prerequisites and project bootstrap&lt;/h2&gt;
&lt;p&gt;Node 20+, a Next.js 15 App Router app, and PostgreSQL — local Docker, Neon, or Supabase free tier all work. India-friendly tip: Neon and Supabase both have free tiers with Mumbai-adjacent regions for lower latency than US-only hosts.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx create-next-app@latest my-app --typescript --app --eslint&lt;br&gt;
cd my-app&lt;br&gt;
npm install prisma @prisma/client&lt;br&gt;
npx prisma init
&lt;h1&gt;
  
  
  .env
&lt;/h1&gt;

&lt;p&gt;DATABASE_URL="postgresql://user:pass@localhost:5432/myapp?schema=public"&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I use Docker Compose for local Postgres when clients want offline development — one docker compose up and DATABASE_URL points at localhost. For solo projects I often skip local Postgres entirely and use Neon's free branch per developer; connection string in .env.local, no Docker overhead on a laptop that already runs Next.js, Prisma Studio, and Chrome with twelve tabs. Pick the path that gets you to a working schema fastest; you can always dockerize later.&lt;/p&gt;
&lt;p&gt;TypeScript strict mode pairs well with Prisma — generated types flow into Server Components without manual interfaces. Enable strict in tsconfig.json before writing models so nullable fields and relation includes fail at compile time instead of in production when a client reports undefined author name on a blog post.&lt;/p&gt;
&lt;p&gt;prisma nextjs tutorial students often skip .env discipline — commit .env.example, never .env.local. Add DATABASE_URL to Vercel before the first deploy that touches the database, or builds succeed and runtime crashes on the first Server Component query. I test locally with npx prisma studio to verify relations before writing UI — faster than debugging empty author names in JSX when the include was wrong.&lt;/p&gt;
&lt;h2 id="schema"&gt;Schema design — models that match your UI&lt;/h2&gt;
&lt;p&gt;Start from screens, not tables. A blog needs Post and User; a SaaS needs Organization and Member. Keep relations explicit in Prisma schema — your Server Components will thank you for include types.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// prisma/schema.prisma&lt;br&gt;
generator client {&lt;br&gt;
  provider = "prisma-client-js"&lt;br&gt;
}

&lt;p&gt;datasource db {&lt;br&gt;
  provider = "postgresql"&lt;br&gt;
  url      = env("DATABASE_URL")&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;model User {&lt;br&gt;
  id        String   @id @default(cuid())&lt;br&gt;
  email     String   @unique&lt;br&gt;
  name      String?&lt;br&gt;
  posts     Post[]&lt;br&gt;
  createdAt DateTime @default(now())&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;model Post {&lt;br&gt;
  id        String   @id @default(cuid())&lt;br&gt;
  title     String&lt;br&gt;
  slug      String   @unique&lt;br&gt;
  published Boolean  @default(false)&lt;br&gt;
  authorId  String&lt;br&gt;
  author    User     @relation(fields: [authorId], references: [id])&lt;br&gt;
  createdAt DateTime @default(now())&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — stringly-typed IDs and no unique slug&lt;br&gt;
model Post {&lt;br&gt;
  id    Int    @id @default(autoincrement())&lt;br&gt;
  slug  String&lt;br&gt;
}

&lt;p&gt;// AFTER — cuid + unique slug for App Router [slug] routes&lt;br&gt;
slug String @unique&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Relations deserve early attention. A Post belongs to User today; tomorrow you add Organization with multi-tenant posts. Adding authorId without planning tenantId means painful migrations when the second customer onboard. I sketch entity relationships on paper — or Excalidraw — before writing schema.prisma, even for weekend projects. Prisma makes schema changes easy; product direction changes are what hurt.&lt;/p&gt;
&lt;p&gt;Enums versus strings is another choice. status Enum(PUBLISHED, DRAFT) gives database-level validation; status String gives flexibility when product adds ARCHIVED without a migration. For indie MVPs I use strings with Zod enums in application code; for fintech clients I prefer Prisma enums so bad data never lands in Postgres regardless of application bugs.&lt;/p&gt;
&lt;p&gt;@@index directives in schema.prisma belong on columns you filter and sort — slug, authorId, published, createdAt. Missing indexes show up as slow dashboards long after launch. I add indexes when queries exist, not speculatively on every string column. Prisma migrate dev generates the SQL; review it like any migration before applying to production.&lt;/p&gt;
&lt;h2 id="migrations"&gt;Migrations — dev flow vs production&lt;/h2&gt;
&lt;p&gt;Local development: &lt;code&gt;prisma migrate dev&lt;/code&gt; creates SQL migration files you commit to Git. Production: &lt;code&gt;prisma migrate deploy&lt;/code&gt; in CI or release step — never edit production schema by hand.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Create and apply migration locally&lt;br&gt;
npx prisma migrate dev --name init_blog
&lt;h1&gt;
  
  
  Generate client after schema changes
&lt;/h1&gt;

&lt;p&gt;npx prisma generate&lt;/p&gt;
&lt;h1&gt;
  
  
  Production (CI script)
&lt;/h1&gt;

&lt;p&gt;npx prisma migrate deploy&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I once skipped committing migration files and watched staging diverge from local — painful afternoon. Treat &lt;code&gt;prisma/migrations&lt;/code&gt; like application code.&lt;/p&gt;
&lt;p&gt;prisma db seed populates local databases with realistic fixtures — fake users, sample posts, test orders. I commit prisma/seed.ts and wire it in package.json so new engineers run npx prisma db seed after migrate dev. Seeds are not for production; they save hours clicking through empty admin UIs during development. Use faker or static JSON depending on whether you need reproducible demo data for screenshots.&lt;/p&gt;
&lt;p&gt;Rollback strategy matters when migrate deploy fails mid-pipeline. CI should fail the deploy, not half-apply schema. I run migrations in a dedicated CI step before next build, with DATABASE_URL pointing at staging first. Production promote only after smoke tests pass. Prisma documents repair commands for drift — read them before you need them at 2 AM Bengaluru time.&lt;/p&gt;
&lt;p&gt;prisma db push is for prototypes only — it syncs schema without migration history. I use it on weekend hacks, then throw away the database before production. Production always uses migrate dev and committed migration folders. Clients paying for reliability deserve auditable schema history, not a pushed schema that cannot roll back cleanly.&lt;/p&gt;
&lt;h2 id="singleton"&gt;Prisma client singleton — avoid connection exhaustion&lt;/h2&gt;
&lt;p&gt;Next.js dev hot reload can spawn multiple PrismaClient instances. The standard singleton pattern fixes "too many connections" on localhost and serverless.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// lib/prisma.ts&lt;br&gt;
import { PrismaClient } from "@prisma/client";

&lt;p&gt;const globalForPrisma = globalThis as unknown as {&lt;br&gt;
  prisma: PrismaClient | undefined;&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;export const prisma =&lt;br&gt;
  globalForPrisma.prisma ??&lt;br&gt;
  new PrismaClient({&lt;br&gt;
    log: process.env.NODE_ENV === "development" ? ["query", "error"] : ["error"],&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The singleton pattern stores PrismaClient on globalThis during development so hot module replacement does not create connection number fifty-one. In production on serverless, each function invocation may still create a client — pooling at the database layer is mandatory, not optional. I log query duration in development only; production logs errors without printing full SQL that might contain PII from WHERE clauses.&lt;/p&gt;
&lt;h2 id="server-queries"&gt;Server queries in App Router — read path&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// app/blog/[slug]/page.tsx — Server Component&lt;br&gt;
import { prisma } from "@/lib/prisma";&lt;br&gt;
import { notFound } from "next/navigation";

&lt;p&gt;type Props = { params: Promise&amp;lt;{ slug: string }&amp;gt; };&lt;/p&gt;

&lt;p&gt;export default async function BlogPostPage({ params }: Props) {&lt;br&gt;
  const { slug } = await params;&lt;/p&gt;

&lt;p&gt;const post = await prisma.post.findUnique({&lt;br&gt;
    where: { slug, published: true },&lt;br&gt;
    include: { author: { select: { name: true } } },&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;if (!post) notFound();&lt;/p&gt;

&lt;p&gt;return (&lt;br&gt;
    &amp;lt;article&amp;gt;&lt;br&gt;
      &amp;lt;h1&amp;gt;{post.title}&amp;lt;/h1&amp;gt;&lt;br&gt;
      &amp;lt;p&amp;gt;By {post.author.name}&amp;lt;/p&amp;gt;&lt;br&gt;
    &amp;lt;/article&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTML ships with content — same SEO win as my &lt;a href="https://safdarali.in/blog/nextjs-vs-react-which-to-learn-2026" rel="noopener noreferrer"&gt;Next.js vs React&lt;/a&gt; article. No client fetch waterfall.&lt;/p&gt;
&lt;p&gt;Pagination belongs in the query, not in JavaScript filter after fetch. Use cursor-based pagination for infinite scroll feeds; offset pagination is fine for admin tables under ten thousand rows. Include only fields the UI needs — select and include are your friends against over-fetching. I have seen findMany without take pull entire tables into server memory and OOM a Vercel function on a client blog with five years of posts.&lt;/p&gt;
&lt;p&gt;Caching interacts with Prisma through Next.js fetch cache and unstable_cache — wrap expensive read queries when data can be stale for sixty seconds. Do not cache user-specific rows in shared cache keys unless you include userId in the key. Public blog slugs cache well; /dashboard/me does not.&lt;/p&gt;
&lt;p&gt;generateMetadata alongside findUnique lets blog posts ship correct Open Graph titles from the database — one query, SEO and body content aligned. I colocate the prisma call in generateMetadata and the page component only when necessary; often a shared getPostBySlug helper in lib/posts.ts avoids duplicate queries through React cache() in the same request.&lt;/p&gt;
&lt;h2 id="mutations"&gt;Mutations — Server Actions with Prisma&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// app/actions/posts.ts&lt;br&gt;
"use server";

&lt;p&gt;import { prisma } from "@/lib/prisma";&lt;br&gt;
import { revalidatePath } from "next/cache";&lt;br&gt;
import { z } from "zod";&lt;/p&gt;

&lt;p&gt;const CreatePost = z.object({&lt;br&gt;
  title: z.string().min(1).max(120),&lt;br&gt;
  slug: z.string().regex(/^[a-z0-9-]+$/),&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;export async function createPost(formData: FormData) {&lt;br&gt;
  const parsed = CreatePost.safeParse({&lt;br&gt;
    title: formData.get("title"),&lt;br&gt;
    slug: formData.get("slug"),&lt;br&gt;
  });&lt;br&gt;
  if (!parsed.success) return { error: "Invalid input" };&lt;/p&gt;

&lt;p&gt;await prisma.post.create({&lt;br&gt;
    data: {&lt;br&gt;
      title: parsed.data.title,&lt;br&gt;
      slug: parsed.data.slug,&lt;br&gt;
      authorId: "user_id_from_session",&lt;br&gt;
    },&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;revalidatePath("/blog");&lt;br&gt;
  return { ok: true };&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Deeper patterns: &lt;a href="https://safdarali.in/blog/react-server-actions-production-guide" rel="noopener noreferrer"&gt;React Server Actions guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Authorization belongs in Server Actions before prisma calls — verify session, check row ownership, then mutate. Never trust authorId from hidden form fields. I load the session server-side and compare post.authorId to session.userId before update or delete.             Prisma does not know your auth rules; it executes whatever valid SQL your code requests.&lt;/p&gt;
&lt;p&gt;Soft deletes with deletedAt DateTime? optional filter every findMany — easy to forget and leak rows. I prefer explicit archived boolean or a dedicated status enum. Prisma middleware can enforce soft delete globally, but that magic confuses juniors debugging why count queries disagree. Be explicit in query helpers until the team knows the data model cold.&lt;/p&gt;
&lt;h2 id="before-after"&gt;BEFORE / AFTER — raw SQL vs Prisma in a Next.js route&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — pg pool in every route, manual mapping&lt;br&gt;
import { Pool } from "pg";&lt;br&gt;
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

&lt;p&gt;export async function GET() {&lt;br&gt;
  const res = await pool.query("SELECT * FROM Post WHERE published = true");&lt;br&gt;
  return Response.json(res.rows);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// AFTER — typed Prisma in Server Component&lt;br&gt;
const posts = await prisma.post.findMany({&lt;br&gt;
  where: { published: true },&lt;br&gt;
  orderBy: { createdAt: "desc" },&lt;br&gt;
  take: 20,&lt;br&gt;
});&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Raw SQL still has a place — reporting queries with window functions, bulk updates, or Postgres-specific features Prisma does not model. Use prisma.$queryRaw with tagged templates and parameterized values; never string-concatenate user input. For ninety percent of CRUD in Next.js apps, generated client methods stay readable and type-safe enough that raw SQL becomes the exception documented in code comments.&lt;/p&gt;
&lt;h2 id="seed-local"&gt;Seeding local data — realistic dev without production dumps&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// prisma/seed.ts&lt;br&gt;
import { PrismaClient } from "@prisma/client";

&lt;p&gt;const prisma = new PrismaClient();&lt;/p&gt;

&lt;p&gt;async function main() {&lt;br&gt;
  await prisma.user.upsert({&lt;br&gt;
    where: { email: "&lt;a href="mailto:dev@safdarali.in"&gt;dev@safdarali.in&lt;/a&gt;" },&lt;br&gt;
    update: {},&lt;br&gt;
    create: {&lt;br&gt;
      email: "&lt;a href="mailto:dev@safdarali.in"&gt;dev@safdarali.in&lt;/a&gt;",&lt;br&gt;
      name: "Safdar Ali",&lt;br&gt;
      posts: {&lt;br&gt;
        create: [&lt;br&gt;
          { title: "Draft post", slug: "draft-post", published: false },&lt;br&gt;
          { title: "Hello world", slug: "hello-world", published: true },&lt;br&gt;
        ],&lt;br&gt;
      },&lt;br&gt;
    },&lt;br&gt;
  });&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;main()&lt;br&gt;
  .then(() =&amp;gt; prisma.$disconnect())&lt;br&gt;
  .catch((e) =&amp;gt; { console.error(e); process.exit(1); });&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// package.json&lt;br&gt;
"prisma": {&lt;br&gt;
  "seed": "npx tsx prisma/seed.ts"&lt;br&gt;
}
&lt;h1&gt;
  
  
  Run after migrate dev
&lt;/h1&gt;

&lt;p&gt;npx prisma db seed&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Seed scripts keep your App Router pages testable without clicking through admin UI every refresh. Never run seed against production — use migrations plus one-off scripts with explicit environment checks.&lt;/p&gt;
&lt;h2 id="deploy"&gt;Deploy checklist — connection pooling on serverless&lt;/h2&gt;
&lt;p&gt;Serverless functions should not open unlimited DB connections. Use Prisma Accelerate, PgBouncer, or Neon's pooled connection string in &lt;code&gt;DATABASE_URL&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Vercel build&lt;br&gt;
prisma generate&lt;br&gt;
prisma migrate deploy&lt;br&gt;
next build
&lt;h1&gt;
  
  
  .env.production — pooled URL example (Neon)
&lt;/h1&gt;

&lt;p&gt;DATABASE_URL="postgresql://user:&lt;a href="mailto:pass@ep-xxx.ap-southeast-1.aws.neon.tech"&gt;pass@ep-xxx.ap-southeast-1.aws.neon.tech&lt;/a&gt;/neondb?sslmode=require&amp;amp;pgbouncer=true"&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Platform comparison: &lt;a href="https://safdarali.in/blog/deploy-nextjs-free-vercel-netlify-railway" rel="noopener noreferrer"&gt;deploy Next.js free&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Preview deployments on Vercel need their own DATABASE_URL or a shared staging database with clear naming — I prefix preview branch names in Neon and rotate credentials when interns leave. Never point preview at production Postgres; one bad seed script deletes real customer rows. Environment separation is boring until it saves your company.&lt;/p&gt;
&lt;h2 id="closing"&gt;Production habits that save you on call&lt;/h2&gt;
&lt;p&gt;Index columns you filter on (&lt;code&gt;slug&lt;/code&gt;, &lt;code&gt;authorId&lt;/code&gt;). Never expose &lt;code&gt;prisma&lt;/code&gt; to client components. Seed scripts for local dev only. &lt;strong&gt;Prisma + PostgreSQL + Next.js&lt;/strong&gt; is the full-stack default I recommend when the product stores data — learn the schema first, ship server queries second, wire actions third.&lt;/p&gt;
&lt;p&gt;Related: &lt;a href="https://safdarali.in/blog/graphql-vs-rest-nextjs-2026" rel="noopener noreferrer"&gt;GraphQL vs REST&lt;/a&gt;, &lt;a href="https://safdarali.in/contact" rel="noopener noreferrer"&gt;contact&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Serverless without a connection pooler exhausts Postgres connections under traffic spikes. Neon, Supabase, and Railway offer pooled URLs — use them in Vercel environment variables. Prisma Accelerate is an option when latency to your database region is high from Indian users hitting a US-east instance.&lt;/p&gt;
&lt;p&gt;Wrap multi-step writes in &lt;code&gt;prisma.$transaction&lt;/code&gt; inside Server Actions — partial updates corrupt data. Validate input with Zod before every Prisma call. Use a separate &lt;code&gt;TEST_DATABASE_URL&lt;/code&gt; in Jest; never hit production from tests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;prisma nextjs tutorial takeaway:&lt;/strong&gt; schema first, migrate second, singleton client third, Server Component reads fourth, Server Action writes fifth, deploy with pooler sixth. Ship one CRUD feature this weekend — a blog, a todo app, a contact form that persists — and you will understand the stack better than reading docs alone.&lt;/p&gt;
&lt;p&gt;prisma studio is underrated for debugging — npx prisma studio opens a GUI on local data without writing SQL. Use it when Server Actions return unexpected null relations. For production debugging, prefer read-only replicas and logged queries over connecting Studio to live databases from a coffee shop WiFi in Bengaluru.&lt;/p&gt;
&lt;p&gt;Indexing strategy: add @@index([authorId]) and @@index([slug]) in schema when lists filter on those columns. Prisma migrate generates the SQL; you review the migration file before apply. Missing indexes show up as slow queries in Neon dashboard long before users complain — if you are not monitoring query time, start there this week.&lt;/p&gt;
&lt;p&gt;Soft deletes with deletedAt DateTime fields keep rows for audit without losing foreign keys — Prisma supports filtering where deletedAt is null in every query. Add a wrapper function so engineers cannot forget the filter. Hard deletes for GDPR erasure stay as explicit admin-only Server Actions with logging.&lt;/p&gt;
&lt;p&gt;Read replicas are a later concern. For MVPs on Neon free tier, a single primary with connection pooling is enough. When read traffic ten times exceeds write traffic, split reads or move analytics to a warehouse — not on day one from a Bengaluru apartment dev setup.&lt;/p&gt;
&lt;p&gt;If this helped you&lt;/p&gt;
&lt;p&gt;I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;span&gt;→ &lt;/span&gt;&lt;a href="https://buymeacoffee.com/safdarali" rel="noopener noreferrer"&gt;Buy me a coffee at buymeacoffee.com/safdarali&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;👉 &lt;/span&gt;&lt;a href="https://www.youtube.com/@safdarali_?sub_confirmation=1" rel="noopener noreferrer"&gt;Subscribe to my YouTube channel&lt;/a&gt;&lt;span&gt; — it's free; 70+ React &amp;amp; Next.js tutorials&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt; 

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>prisma</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>PHP2Shell: what the two critical PHP RCE CVEs actually mean for your stack</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Thu, 22 Oct 2026 22:15:57 +0000</pubDate>
      <link>https://dev.to/safdarali25/php2shell-what-the-two-critical-php-rce-cves-actually-mean-for-your-stack-4gf7</link>
      <guid>https://dev.to/safdarali25/php2shell-what-the-two-critical-php-rce-cves-actually-mean-for-your-stack-4gf7</guid>
      <description>&lt;p&gt;If you're on &lt;strong&gt;PHP 8.1 or older, there is no official fix coming&lt;/strong&gt;. Those branches are end-of-life. Your options are: upgrade, or ask your hosting/distro vendor if they provide a backported patch (some LTS vendors do).&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do today
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Check what's really running your site, not just your CLI version&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows your CLI version. It is &lt;em&gt;not necessarily&lt;/em&gt; the version serving requests. Check the SAPI that's actually live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php-fpm &lt;span class="nt"&gt;-v&lt;/span&gt;
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://yoursite.com/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"^(server|x-powered-by):"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CLI and web versions can differ, especially on servers hosting multiple projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Patch internet-facing systems first&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If it's reachable from the open internet, it's the priority. Internal-only services can wait a few hours if you're triaging a lot of infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Don't forget bundled PHP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Docker base images, CMS installer bundles, and vendor tarballs often ship their own PHP. A host-level patch won't touch these. Check every running image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &amp;lt;container&amp;gt; php &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Ask your hosting provider directly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Managed hosts (SiteGround, Cloudflare-fronted setups, etc.) may have already deployed WAF-level mitigations or patched server-side. Don't assume — ask.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Watch for post-exploitation signs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keep an eye on PHP worker processes spawning shells, curl/wget calls, or unexpected outbound connections. That's the pattern seen in active exploitation so far.&lt;/p&gt;

&lt;h2&gt;
  
  
  A stopgap, not a fix
&lt;/h2&gt;

&lt;p&gt;Some researchers have floated disabling &lt;code&gt;session.upload_progress&lt;/code&gt; as a temporary mitigation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;session.upload_progress.enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;not a substitute for patching&lt;/strong&gt;. Treat it as something you do while you're rolling out the real fix, not instead of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you manage sites for other people
&lt;/h2&gt;

&lt;p&gt;Send a short, plain update today — what happened, what you're doing about it, when it'll be done. Clients hearing about a "PHP vulnerability" from the news before they hear it from you is a worse conversation than this one.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.php.net/archive/2026.php" rel="noopener noreferrer"&gt;PHP.net security archive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://watchtowr.com/intelligence/php-cve-2026-109618-faq/" rel="noopener noreferrer"&gt;watchTowr PHP2Shell FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.helpnetsecurity.com/2026/10/21/php2shell-php-vulnerabilities-patched-cve-2026-109618-cve-2026-109619/" rel="noopener noreferrer"&gt;Help Net Security writeup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.wiz.io/blog/php2shell-php-rce" rel="noopener noreferrer"&gt;Wiz Research disclosure&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Has your stack been patched yet? Drop your setup in the comments — curious how many people are finding PHP buried in places they forgot about.&lt;/p&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>webdev</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>How AI Changed the Way I Write React Code — 2 Years In</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Sun, 11 Oct 2026 20:40:04 +0000</pubDate>
      <link>https://dev.to/safdarali25/how-ai-changed-the-way-i-write-react-code-2-years-in-4ao7</link>
      <guid>https://dev.to/safdarali25/how-ai-changed-the-way-i-write-react-code-2-years-in-4ao7</guid>
      <description>&lt;p&gt;I'm &lt;a href="https://safdarali.in/about" rel="noopener noreferrer"&gt;Safdar Ali&lt;/a&gt;, a frontend engineer in Bengaluru. In late 2024 I installed GitHub Copilot as an experiment. Today — October 2026 — Cursor Agent with Claude is open on every project, including this portfolio at &lt;a href="https://safdarali.in/" rel="noopener noreferrer"&gt;safdarali.in&lt;/a&gt;. Two years of daily &lt;strong&gt;ai react development&lt;/strong&gt; did not make me obsolete. It changed where I spend cognitive effort: less on boilerplate, more on architecture, review, and the questions AI cannot answer. This is an honest accounting — not a pitch for any tool.&lt;/p&gt;
&lt;h2 id="what-changed"&gt;What actually changed in my daily work&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Speed on scaffolding.&lt;/strong&gt; New components, API route stubs, Zod schemas, and test files appear in minutes instead of half-hours. I start further along on every task.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Context switching dropped.&lt;/strong&gt; I used to alt-tab between docs, Stack Overflow, and my editor constantly. Now I ask the agent to explain an unfamiliar API against my actual codebase — the answer is contextual, not generic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Refactors got cheaper.&lt;/strong&gt; Renaming a prop across 12 files, converting client fetches to Server Components, adding TypeScript strict types to a legacy module — these used to be "schedule for Friday" tasks. Now they're afternoon tasks with review.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My commit size changed.&lt;/strong&gt; More frequent, smaller commits. AI generates chunks; I review and merge chunk by chunk instead of writing 400-line PRs manually.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 2024 — I wrote every line of this by hand&lt;br&gt;
export function useDebounce&amp;lt;T&amp;gt;(value: T, delay: number): T {&lt;br&gt;
  const [debounced, setDebounced] = useState(value);&lt;br&gt;
  useEffect(() =&amp;gt; {&lt;br&gt;
    const timer = setTimeout(() =&amp;gt; setDebounced(value), delay);&lt;br&gt;
    return () =&amp;gt; clearTimeout(timer);&lt;br&gt;
  }, [value, delay]);&lt;br&gt;
  return debounced;&lt;br&gt;
}

&lt;p&gt;// 2026 — AI generates this in 3 seconds; I verify edge cases and add tests&lt;br&gt;
// My value-add: knowing WHEN to debounce, not typing the hook from memory&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="what-didnt-change"&gt;What did not change — and probably never will&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Architecture decisions.&lt;/strong&gt; Server Component vs client component boundaries, state management choices, caching strategy — AI suggests patterns but cannot know your product constraints, team size, or traffic profile.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debugging production incidents.&lt;/strong&gt; AI helps read stack traces. It does not replace opening DevTools at 11pm when a hydration mismatch only appears on Android Chrome in India.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code review judgment.&lt;/strong&gt; Someone must reject the confident wrong output. That someone needs to understand React reconciliation, not just prompt engineering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stakeholder communication.&lt;/strong&gt; Translating "we need ISR here because catalog updates hourly" into business language — no model does that for you.&lt;/p&gt;
&lt;p&gt;What surprised me most over two years: AI did not reduce total hours worked — it shifted where those hours go. I spend less time writing boilerplate and more time in code review, architecture discussions, and debugging the subtle bugs AI introduces — wrong dependency arrays, missing error boundaries, imports from packages not in package.json. The job got more senior, not more absent.&lt;/p&gt;
&lt;p&gt;Pair programming changed too. When I mentor juniors in Bengaluru, we use AI together — they prompt, I ask why they accepted the output, we fix it together. That teaches review skills faster than solo tutorial watching. The tool is the same; the mentorship model adapted.&lt;/p&gt;
&lt;h2 id="skills-table"&gt;Skills that matter more now — ranked honestly&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Importance in 2024&lt;/th&gt;
&lt;th&gt;Importance in 2026&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reading diffs critically&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;AI generates more code to review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React fundamentals&lt;/td&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;Cannot debug AI output without them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typing speed / memorisation&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Lower&lt;/td&gt;
&lt;td&gt;Autocomplete handles syntax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System design&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Higher&lt;/td&gt;
&lt;td&gt;AI fills files; you design boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt/task specification&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Bad prompts waste more time now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing discipline&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;Safety net for AI-generated code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security awareness&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;AI leaks patterns, misses auth gaps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2 id="before-after"&gt;Before and after — same feature, two eras&lt;/h2&gt;
&lt;p&gt;Adding a contact form to a Next.js portfolio — the task I did for this site in 2024 vs 2026.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE (2024) — ~4 hours total&lt;br&gt;
// 1. Create API route manually (30 min)&lt;br&gt;
// 2. Write client form with useState validation (45 min)&lt;br&gt;
// 3. Style with Tailwind trial-and-error (60 min)&lt;br&gt;
// 4. Add loading/error states (30 min)&lt;br&gt;
// 5. Debug CORS and env vars (45 min)&lt;br&gt;
// 6. Write tests (30 min)

&lt;p&gt;// AFTER (2026) — ~90 minutes total&lt;br&gt;
// 1. Agent scaffolds Server Action + Zod validation (5 min gen, 15 min review)&lt;br&gt;
// 2. v0 generates form UI, I wire action prop (10 min gen, 20 min review)&lt;br&gt;
// 3. Manual: rate limiting, honeypot, email provider integration (40 min)&lt;br&gt;
// 4. Tests generated, I add edge cases (10 min)&lt;/p&gt;

&lt;p&gt;// Time saved: ~2.5 hours. Time still required: integration + review.&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The saved time went into performance work and content — not into leaving early. AI shifted the work mix, not the total output expectation.&lt;/p&gt;
&lt;p&gt;Code quality did not automatically improve — it shifted form. I write fewer lines manually but read more lines in review. Net lines of code per feature dropped roughly 20% on client projects where AI handled boilerplate, but bug density stayed similar when measured per thousand lines. The bugs moved from typos to logic errors in AI-generated conditionals — a different debugging skill, not fewer bugs by default.&lt;/p&gt;
&lt;h2 id="junior-devs"&gt;What this means if you are learning React in India&lt;/h2&gt;
&lt;p&gt;Juniors ask me: "Should I learn to code if AI writes it?" Yes — but learn differently. Skip memorising array method signatures; do not skip building a todo app without AI so you understand state updates. Use AI to explain errors, not to skip the error entirely.&lt;/p&gt;
&lt;p&gt;Interviewers in Bengaluru still ask you to walk through a component you wrote. If AI wrote all of it and you cannot explain &lt;code&gt;useEffect&lt;/code&gt; dependencies, you will not pass. The bar moved from "can you type code" to "can you own code."&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Bad learning loop — paste AI output, ship, repeat&lt;br&gt;
// You never build mental models

&lt;p&gt;// Good learning loop — AI as tutor&lt;br&gt;
// 1. Try writing the hook yourself (15 min)&lt;br&gt;
// 2. Ask AI to review YOUR code, not generate fresh&lt;br&gt;
// 3. Compare its suggestions to your attempt&lt;br&gt;
// 4. Write the final version by hand with its hints&lt;/p&gt;

&lt;p&gt;export function ProductFilter({ onFilter }: { onFilter: (q: string) =&amp;gt; void }) {&lt;br&gt;
  const [query, setQuery] = useState("");&lt;br&gt;
  const debounced = useDebounce(query, 300);&lt;/p&gt;

&lt;p&gt;useEffect(() =&amp;gt; {&lt;br&gt;
    onFilter(debounced);&lt;br&gt;
  }, [debounced, onFilter]);&lt;/p&gt;

&lt;p&gt;return &amp;lt;input value={query} onChange={(e) =&amp;gt; setQuery(e.target.value)} /&amp;gt;;&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="mistakes"&gt;Mistakes I made adopting AI too fast&lt;/h2&gt;
&lt;p&gt;Shipping AI-generated auth logic without reading it — found a missing CSRF check in review, not in production, luckily. Accepting overly abstracted code because it looked "clean" — three files for what should have been one function. Stopping practice on fundamentals because autocomplete felt good enough — until a whiteboard interview reminded me why muscle memory matters.&lt;/p&gt;
&lt;p&gt;The fix was rules, not quitting AI: never merge agent output without reading every changed line; never let AI touch auth, payments, or PII handling without manual security review; keep one personal project per month with AI disabled to stay sharp.&lt;/p&gt;
&lt;p&gt;Tools will keep improving — faster models, better repo indexing, cheaper inference. The developers who treat that as leverage rather than replacement will compound their careers. The ones who stop learning fundamentals because autocomplete feels good enough will hit a ceiling in interviews and production incidents. I have seen both outcomes in my network across India's tech hubs over the last two years.&lt;/p&gt;
&lt;p&gt;My YouTube audience often asks whether AI makes React tutorials obsolete. No — it makes understanding React more valuable. When anyone can generate a component, the developer who explains why it re-renders, why keys matter, and why the Server Component boundary exists becomes the one teams hire. That is the real shift behind &lt;strong&gt;ai react development 2026&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="weekly-rhythm"&gt;My weekly rhythm with AI in 2026&lt;/h2&gt;
&lt;p&gt;Monday: plan features manually — no AI for architecture decisions. Tuesday–Thursday: Cursor Agent for implementation, Copilot tab completion while I edit agent output. Friday: code review day — re-read the week's merges without AI assist, run Lighthouse and tests, fix anything that slipped through. Weekend: one hour on personal learning with AI disabled — currently revisiting TypeScript generics the slow way.&lt;/p&gt;
&lt;p&gt;That rhythm keeps AI as accelerator, not autopilot. The Friday review catches the subtle bugs — missing dependency arrays, hardcoded API URLs, accessibility gaps — that autocomplete does not flag because they are syntactically valid. Two years in, this rhythm matters more than which model or editor I use.&lt;/p&gt;
&lt;h2 id="2026-outlook"&gt;Where ai react development goes from here&lt;/h2&gt;
&lt;p&gt;Agents will get better at multi-step tasks — migrations, test coverage, dependency upgrades. They will not replace the engineer who decides whether a migration is worth the risk this sprint. The developers who thrive will treat AI like a fast junior: delegate boilerplate, review everything, own the architecture.&lt;/p&gt;
&lt;p&gt;I ship roughly 3x more features per month than in 2024, measured by merged PRs on client work — not because AI writes 3x the code, but because the idle time between "I know what to build" and "first working draft" collapsed. That is the real &lt;strong&gt;ai react development 2026&lt;/strong&gt; story.&lt;/p&gt;
&lt;p&gt;Document your own before/after metrics if you adopt AI tools seriously — merged PRs per week, time from ticket to staging, lines changed in review versus lines changed manually. Without numbers, you will not know if AI actually helps your workflow or just feels faster because typing is more entertaining. I review my metrics monthly and adjust tool usage accordingly.&lt;/p&gt;
&lt;h2 id="closing"&gt;The single takeaway&lt;/h2&gt;
&lt;p&gt;AI changed my React workflow profoundly — not by replacing thinking, but by removing friction between thinking and implementation. Fundamentals matter more, not less, because the bottleneck moved from typing to judgment. Learn React deeply, use AI aggressively, review ruthlessly.&lt;/p&gt;
&lt;p&gt;Two years from now the tools will look different — new models, new editors, maybe AI built into browsers. The pattern will hold: engineers who understand their stack and review AI output will ship faster than engineers who either reject AI entirely or accept it blindly. Meet in the middle. That is where the career growth lives in &lt;strong&gt;ai react development 2026&lt;/strong&gt; and beyond.&lt;/p&gt;
&lt;p&gt;Related: &lt;a href="https://safdarali.in/blog/cursor-claude-react-workflow-2026" rel="noopener noreferrer"&gt;Cursor + Claude React workflow&lt;/a&gt;, &lt;a href="https://safdarali.in/blog/github-copilot-vs-cursor-2026" rel="noopener noreferrer"&gt;Copilot vs Cursor comparison&lt;/a&gt;. More: &lt;a href="https://safdarali.in/blog" rel="noopener noreferrer"&gt;safdarali.in/blog&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you are early in your career, prioritise understanding over speed for the next six months — use AI to explain code you wrote, not to write code you cannot explain. The productivity multiplier kicks in once fundamentals are solid; before that, AI mostly hides gaps that interviews expose.&lt;/p&gt;
&lt;p&gt;If this helped you&lt;/p&gt;
&lt;p&gt;I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;span&gt;→ &lt;/span&gt;&lt;a href="https://buymeacoffee.com/safdarali" rel="noopener noreferrer"&gt;Buy me a coffee at buymeacoffee.com/safdarali&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;👉 &lt;/span&gt;&lt;a href="https://www.youtube.com/@safdarali_?sub_confirmation=1" rel="noopener noreferrer"&gt;Subscribe to my YouTube channel&lt;/a&gt;&lt;span&gt; — it's free; 70+ React &amp;amp; Next.js tutorials&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="related-posts-heading"&gt;Related reading&lt;/h2&gt;
&lt;p&gt;More guides on safdarali.in — same author, production-focused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/vibe-coding-2026-reality-check" rel="noopener noreferrer"&gt;&lt;span&gt;Opinion&lt;/span&gt;&lt;h3&gt;Vibe Coding in 2026 — Is It Real or Just a Trend?&lt;/h3&gt;
&lt;p&gt;Vibe coding in 2026 — reality check from a developer who uses AI daily. When it works, when it fails, and what you still need to learn.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Aug 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/free-video-thumbnail-generator-online-no-upload" rel="noopener noreferrer"&gt;&lt;span&gt;FrameSnap&lt;/span&gt;&lt;h3&gt;Free Video Thumbnail Generator Online — No Upload&lt;/h3&gt;
&lt;p&gt;Free video thumbnail generator online — extract frames from video without upload. Browser-based, no watermark, YouTube thumbnail from MP4 workflow.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Jun 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>react</category>
    </item>
    <item>
      <title>Jev Is Not an LLM. That Is the Point.</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Thu, 24 Sep 2026 08:06:46 +0000</pubDate>
      <link>https://dev.to/safdarali25/jev-is-not-an-llm-that-is-the-point-2hd7</link>
      <guid>https://dev.to/safdarali25/jev-is-not-an-llm-that-is-the-point-2hd7</guid>
      <description>&lt;p&gt;Most "AI features" are not writing tasks. They are decisions.&lt;/p&gt;

&lt;p&gt;Is this ticket urgent? Which queue owns it? Is this shell command safe to run? Should this request go to a small model, a large one, or a person?&lt;/p&gt;

&lt;p&gt;A language model can answer all of those. It does it by generating text, one token at a time, even when you asked for JSON. &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;Jev&lt;/a&gt;, from TypeSafe AI, does not generate text at all. You hand it a piece of state and a set of questions whose answers you already defined. It fills those answers in one pass and attaches a confidence score to each one.&lt;/p&gt;

&lt;p&gt;That tradeoff is the product. Jev cannot write the email, the summary, or the code. Your program still decides what to do with the judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are actually calling
&lt;/h2&gt;

&lt;p&gt;TypeSafe calls Jev a System One model, after the fast, intuitive half of Daniel Kahneman's &lt;em&gt;Thinking, Fast and Slow&lt;/em&gt;. The name Jev comes from William Stanley Jevons: when a resource gets cheap enough, people find far more uses for it. The bet is that a decision cheap enough to put inside ordinary control flow gets used in places a frontier chat model never will.&lt;/p&gt;

&lt;p&gt;The interface is closer to a function than to a chat:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;A language model&lt;/th&gt;
&lt;th&gt;Jev&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;A string you parse&lt;/td&gt;
&lt;td&gt;A value you declared up front&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sampling&lt;/td&gt;
&lt;td&gt;One token after another&lt;/td&gt;
&lt;td&gt;Every answer in the same request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uncertainty&lt;/td&gt;
&lt;td&gt;Only if you ask, and often overconfident&lt;/td&gt;
&lt;td&gt;A confidence score on every answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure mode&lt;/td&gt;
&lt;td&gt;Wrong words, broken JSON, a type you did not expect&lt;/td&gt;
&lt;td&gt;A wrong choice inside the types you allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;TypeSafe's published pricing is $0.042 per million input tokens, with output tokens not billed. They report end-to-end times of about 70–500ms, and on their own workflow evals they cite figures around 190× faster and 440× cheaper than the large models they compared against. Those numbers are their benchmark, on workflows their own team wrote, scored against other labs' models. Treat them as a reason to measure your path, not as a number to put in a launch post.&lt;/p&gt;

&lt;p&gt;Schema match is a harder claim, and a narrower one. Jev cannot return a string when the field is an enum. It can still pick &lt;code&gt;sales&lt;/code&gt; when &lt;code&gt;billing&lt;/code&gt; was right. You fixed the shape. You did not get correctness for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the question on the type, not in the prompt
&lt;/h2&gt;

&lt;p&gt;If you have used Pydantic AI, the call looks like any other structured-output agent. Install the TypeSafe extra and set &lt;code&gt;TYPESAFE_API_KEY&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"pydantic-ai-slim[typesafe]"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TYPESAFE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-api-key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The habit that does not carry over from a language model: the prompt is only the material being judged. The questions live on the output type. A question written into the prompt is more text to judge, not an instruction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic_ai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Ticket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Triage a support ticket.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;urgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Does this need a reply within the hour?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Which team owns it?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;typesafe:jev-latest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Ticket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You charged me twice and my account is overdrawn. I need this reversed today.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# urgent=True area='billing'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Several fields go out together. That is where the latency win comes from: independent judgments are not separate generation rounds. Extra fields cost input tokens, not another round trip.&lt;/p&gt;

&lt;p&gt;Ask one thing per field. "Is this a good ticket?" is several judgments wearing one name. Jev will still return something, often with low confidence, and you will notice too late. Split it. Combine the answers in code, where the rule is exact.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;str&lt;/code&gt; field, an unbounded number, or a datetime is rejected before the request is sent. If the answer is prose, this is the wrong model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confidence is the part you branch on
&lt;/h2&gt;

&lt;p&gt;A boolean becomes &lt;code&gt;True&lt;/code&gt; when Jev's probability of yes is at least &lt;code&gt;typesafe_boolean_threshold&lt;/code&gt;, which defaults to 0.5. That default is a coin flip. It is the wrong bar when the two mistakes do not cost the same. Raising the threshold means a &lt;code&gt;True&lt;/code&gt; has to be earned, which is what you want for "safe to run this command with no one watching."&lt;/p&gt;

&lt;p&gt;The margin on each field is on the response, not on the model instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider_details&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
&lt;span class="c1"&gt;# {'urgent': 0.91, 'area': 0.74}  — illustrative
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;confidence&lt;/code&gt; here is a margin, not "the probability this label is correct." For a yes/no it measures how far the probability sits from the threshold that decided it. For a pick-one, the full distribution is in &lt;code&gt;provider_details['probabilities']&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A practical split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High margin: let code act.&lt;/li&gt;
&lt;li&gt;Middle: send the same state to a stronger generative model.&lt;/li&gt;
&lt;li&gt;Low: ask a person.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;jev-latest&lt;/code&gt; moves when TypeSafe ships a release, and the numbers under a tuned threshold can move with it. Once a bar is calibrated on your own labels, pin a version such as &lt;code&gt;typesafe:jev-1.13.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the honest answer is sometimes "not enough information," put that in the type. A yes/no with no third option forces a side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fits, and where it does not
&lt;/h2&gt;

&lt;p&gt;Use Jev for the small semantic branch that used to be either a brittle regex or a full model call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routing a ticket, a tool call, or a model.&lt;/li&gt;
&lt;li&gt;Scoring a rubric you defined (ordered levels, each one described).&lt;/li&gt;
&lt;li&gt;Checking an agent step: did it finish, did it leave the requested scope, does a human need to see this?&lt;/li&gt;
&lt;li&gt;Guardrails around something a language model already wrote.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep a language model for generation and for reasoning that does not fit in one judgment. Keep ordinary code for rules you can write down: amount greater than a limit, a missing field, an allow-list. Jev sitting in the middle does not replace either of those.&lt;/p&gt;

&lt;p&gt;Two design limits are worth knowing before you sketch an architecture. Choice lists top out at 255 options, and a high-cardinality pick may be scored in two stages. Fields in the same request are independent, so one answer cannot depend on another answer from that same call. If the second judgment needs the first, make them two steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small architecture, not a new model in every slot
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exact rule            → code
bounded judgment      → Jev
writing or reasoning  → a language model
high-stakes uncertainty → a person
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful question is not which model to standardize on. It is what kind of intelligence a given step needs. If the step is a decision with answers you can name in advance, generating those answers token by token is a lot of machinery for a branch.&lt;/p&gt;

&lt;p&gt;Jev is in early access. The &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;TypeSafe announcement&lt;/a&gt; has their evals and the caveats attached to them. The &lt;a href="https://pydantic.dev/docs/ai/models/typesafe/" rel="noopener noreferrer"&gt;Pydantic AI TypeSafe docs&lt;/a&gt; are the practical reference for field types, thresholds, and &lt;code&gt;provider_details&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☕ If This Helped You
&lt;/h2&gt;

&lt;p&gt;I publish free tutorials and write-ups like this regularly.&lt;/p&gt;

&lt;p&gt;If this article saved you time:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Buy me a coffee:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://buymeacoffee.com/safdarali" rel="noopener noreferrer"&gt;https://buymeacoffee.com/safdarali&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Subscribe to my YouTube channel (free):&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://www.youtube.com/@safdarali_?sub_confirmation=1" rel="noopener noreferrer"&gt;https://www.youtube.com/@safdarali_?sub_confirmation=1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;More about how I work: &lt;a href="https://safdarali.in/about" rel="noopener noreferrer"&gt;safdarali.in/about&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Projects and case studies: &lt;a href="https://safdarali.in/projects" rel="noopener noreferrer"&gt;safdarali.in/projects&lt;/a&gt;&lt;/em&gt; &lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>webdev</category>
      <category>jev</category>
    </item>
    <item>
      <title>Next.js App Router Complete Beginner Guide 2026</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Fri, 10 Jul 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/safdarali25/nextjs-app-router-complete-beginner-guide-2026-4j6n</link>
      <guid>https://dev.to/safdarali25/nextjs-app-router-complete-beginner-guide-2026-4j6n</guid>
      <description>&lt;p&gt;The &lt;strong&gt;nextjs app router tutorial&lt;/strong&gt; gap is real: official docs are reference-heavy, and beginners still think in Pages Router (&lt;code&gt;pages/index.tsx&lt;/code&gt;). App Router uses folders in &lt;code&gt;app/&lt;/code&gt; where the file name tells Next.js what to do. I migrated client sites in 2024–2025 and teach this sequence on my YouTube channel. Follow these steps in order — layout, page, loading, then server fetch.&lt;/p&gt;
&lt;h2 id="mental-model"&gt;Mental model — folders are routes&lt;/h2&gt;
&lt;p&gt;Every folder under &lt;code&gt;app/&lt;/code&gt; maps to a URL segment. Special files: &lt;code&gt;page.tsx&lt;/code&gt; = UI, &lt;code&gt;layout.tsx&lt;/code&gt; = shared shell, &lt;code&gt;loading.tsx&lt;/code&gt; = skeleton while slow segments load.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app/&lt;br&gt;
  layout.tsx      → wraps ALL routes&lt;br&gt;
  page.tsx        → URL: /&lt;br&gt;
  about/&lt;br&gt;
    page.tsx      → URL: /about&lt;br&gt;
  blog/&lt;br&gt;
    page.tsx      → URL: /blog&lt;br&gt;
    [slug]/&lt;br&gt;
      page.tsx    → URL: /blog/my-post&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No React Router install. Rename a folder, you rename a route. That is the core App Router win.&lt;/p&gt;
&lt;p&gt;If you are coming from Create React App, delete the mental model of a single &lt;code&gt;App.tsx&lt;/code&gt; with a Switch. You will have multiple layout.tsx files at different depths — root for html/body, nested for dashboard sidebars. Each layout wraps only its subtree.&lt;/p&gt;
&lt;h2 id="step-layout"&gt;Step 1 — Root layout (HTML shell once)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// app/layout.tsx&lt;br&gt;
import type { Metadata } from "next";&lt;br&gt;
import "./globals.css";

&lt;p&gt;export const metadata: Metadata = {&lt;br&gt;
  title: "My App",&lt;br&gt;
  description: "Beginner App Router site",&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;export default function RootLayout({&lt;br&gt;
  children,&lt;br&gt;
}: {&lt;br&gt;
  children: React.ReactNode;&lt;br&gt;
}) {&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;html lang="en"&amp;gt;&lt;br&gt;
      &amp;lt;body className="min-h-screen bg-white text-neutral-900"&amp;gt;&lt;br&gt;
        &amp;lt;header className="border-b p-4"&amp;gt;My Site&amp;lt;/header&amp;gt;&lt;br&gt;
        &amp;lt;main&amp;gt;{children}&amp;lt;/main&amp;gt;&lt;br&gt;
      &amp;lt;/body&amp;gt;&lt;br&gt;
    &amp;lt;/html&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Layouts do not remount when navigating between child pages — perfect for nav bars and fonts. Keep providers that must persist here (theme, analytics).&lt;/p&gt;
&lt;h2 id="step-page"&gt;Step 2 — First page&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// app/page.tsx — home route&lt;br&gt;
export default function HomePage() {&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;section className="p-8"&amp;gt;&lt;br&gt;
      &amp;lt;h1 className="text-3xl font-bold"&amp;gt;Welcome&amp;lt;/h1&amp;gt;&lt;br&gt;
      &amp;lt;p&amp;gt;Your first App Router page — no use client needed.&amp;lt;/p&amp;gt;&lt;br&gt;
    &amp;lt;/section&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Default export must be a component. This is a Server Component unless you add &lt;code&gt;"use client"&lt;/code&gt; at the top.&lt;/p&gt;
&lt;h2 id="step-loading"&gt;Step 3 — loading.tsx for perceived speed&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// app/blog/loading.tsx — shows while blog segment loads&lt;br&gt;
export default function BlogLoading() {&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;div className="animate-pulse space-y-4 p-8"&amp;gt;&lt;br&gt;
      &amp;lt;div className="h-8 w-48 rounded bg-neutral-200" /&amp;gt;&lt;br&gt;
      &amp;lt;div className="h-4 w-full rounded bg-neutral-200" /&amp;gt;&lt;br&gt;
      &amp;lt;div className="h-4 w-3/4 rounded bg-neutral-200" /&amp;gt;&lt;br&gt;
    &amp;lt;/div&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next.js wraps the segment in Suspense automatically. Users see skeletons instead of frozen UI — critical on Indian mobile networks.&lt;/p&gt;
&lt;h2 id="step-fetch"&gt;Step 4 — Server Component fetch (no useEffect)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// app/blog/page.tsx&lt;br&gt;
type Post = { slug: string; title: string };

&lt;p&gt;async function getPosts(): Promise&amp;lt;Post[]&amp;gt; {&lt;br&gt;
  const res = await fetch("&lt;a href="https://api.example.com/posts" rel="noopener noreferrer"&gt;https://api.example.com/posts&lt;/a&gt;", {&lt;br&gt;
    next: { revalidate: 3600 },&lt;br&gt;
  });&lt;br&gt;
  if (!res.ok) throw new Error("Failed to load posts");&lt;br&gt;
  return res.json();&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;export default async function BlogPage() {&lt;br&gt;
  const posts = await getPosts();&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;ul className="space-y-2 p-8"&amp;gt;&lt;br&gt;
      {posts.map((post) =&amp;gt; (&lt;br&gt;
        &amp;lt;li key={post.slug}&amp;gt;&lt;br&gt;
          &amp;lt;a href={"/blog/" + post.slug}&amp;gt;{post.title}&amp;lt;/a&amp;gt;&lt;br&gt;
        &amp;lt;/li&amp;gt;&lt;br&gt;
      ))}&lt;br&gt;
    &amp;lt;/ul&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — Pages Router habit (client fetch)&lt;br&gt;
"use client";&lt;br&gt;
useEffect(() =&amp;gt; fetch("/api/posts").then(/* ... */), []);

&lt;p&gt;// AFTER — async server component&lt;br&gt;
// Data ready before HTML ships — better SEO and LCP&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Deeper caching rules: &lt;a href="https://safdarali.in/blog/ssr-ssg-isr-nextjs-explained" rel="noopener noreferrer"&gt;SSR vs SSG vs ISR&lt;/a&gt;. Performance tuning: &lt;a href="https://safdarali.in/blog/nextjs-performance-60-percent" rel="noopener noreferrer"&gt;60% load time case study&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="comparison"&gt;App Router files — quick comparison table&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Required?&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;layout.tsx&lt;/td&gt;
&lt;td&gt;Shared UI wrapper&lt;/td&gt;
&lt;td&gt;Root required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;page.tsx&lt;/td&gt;
&lt;td&gt;Route UI&lt;/td&gt;
&lt;td&gt;Yes per route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;loading.tsx&lt;/td&gt;
&lt;td&gt;Loading skeleton&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;error.tsx&lt;/td&gt;
&lt;td&gt;Error boundary&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;not-found.tsx&lt;/td&gt;
&lt;td&gt;404 UI&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;route.ts&lt;/td&gt;
&lt;td&gt;API endpoint&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2 id="client-leaf"&gt;Step 5 — Add client components only as leaves&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// components/CounterButton.tsx&lt;br&gt;
"use client";&lt;br&gt;
import { useState } from "react";

&lt;p&gt;export function CounterButton() {&lt;br&gt;
  const [n, setN] = useState(0);&lt;br&gt;
  return &amp;lt;button onClick={() =&amp;gt; setN(n + 1)}&amp;gt;Clicked {n} times&amp;lt;/button&amp;gt;;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// app/page.tsx — server page imports client leaf&lt;br&gt;
import { CounterButton } from "@/components/CounterButton";&lt;/p&gt;

&lt;p&gt;export default function HomePage() {&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;section&amp;gt;&lt;br&gt;
      &amp;lt;h1&amp;gt;Home&amp;lt;/h1&amp;gt;&lt;br&gt;
      &amp;lt;CounterButton /&amp;gt;&lt;br&gt;
    &amp;lt;/section&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read &lt;a href="https://safdarali.in/blog/rsc-vs-client-components" rel="noopener noreferrer"&gt;RSC vs client components&lt;/a&gt; before marking whole pages as client — that is the beginner mistake that balloons bundle size.&lt;/p&gt;
&lt;h2 id="dynamic-route"&gt;Step 6 — Dynamic routes and params&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// app/blog/[slug]/page.tsx&lt;br&gt;
type Props = { params: Promise&amp;lt;{ slug: string }&amp;gt; };

&lt;p&gt;export default async function PostPage({ params }: Props) {&lt;br&gt;
  const { slug } = await params;&lt;br&gt;
  const post = await getPost(slug);&lt;br&gt;
  if (!post) return &amp;lt;p&amp;gt;Not found&amp;lt;/p&amp;gt;;&lt;br&gt;
  return &amp;lt;article&amp;gt;&amp;lt;h1&amp;gt;{post.title}&amp;lt;/h1&amp;gt;&amp;lt;/article&amp;gt;;&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Next.js 15, params is a Promise — await it. TypeScript strict mode helps catch forgotten awaits — &lt;a href="https://safdarali.in/blog/typescript-strict-mode-guide-2026" rel="noopener noreferrer"&gt;strict mode guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="learn-path"&gt;Learning path after this guide&lt;/h2&gt;
&lt;p&gt;Week 1: layouts + pages + loading. Week 2: server fetch + one dynamic route. Week 3: one client form with Server Action. Week 4: deploy to Vercel and run Lighthouse. If you are choosing between stacks first, read &lt;a href="https://safdarali.in/blog/nextjs-vs-react-which-to-learn-2026" rel="noopener noreferrer"&gt;Next.js vs React&lt;/a&gt;. Organise folders early with &lt;a href="https://safdarali.in/blog/nextjs-project-structure-guide-2026" rel="noopener noreferrer"&gt;project structure guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For AI-assisted coding, see &lt;a href="https://safdarali.in/blog/cursor-claude-react-workflow-2026" rel="noopener noreferrer"&gt;Cursor + Claude workflow&lt;/a&gt; — but build this hello-world tree by hand once so file conventions stick.&lt;/p&gt;
&lt;h2 id="production"&gt;My production setup&lt;/h2&gt;
&lt;p&gt;In production I scaffold with root layout, route groups for marketing vs app, loading.tsx on slow segments, and async server pages for public content. This portfolio follows the same pattern — thin &lt;code&gt;app/blog/.../page.tsx&lt;/code&gt; files, heavy logic elsewhere.&lt;/p&gt;
&lt;p&gt;At my day job, beginners who complete these six steps ship their first internal page in a week — faster than learning Pages Router and relearning later.&lt;/p&gt;
&lt;h2 id="closing"&gt;The single takeaway&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;App Router = folders + special files.&lt;/strong&gt; Layout once, page per route, loading for UX, async server fetch for data. Client components are seasoning, not the main dish.&lt;/p&gt;
&lt;p&gt;Related: &lt;a href="https://safdarali.in/blog/react-19-features-production-guide" rel="noopener noreferrer"&gt;React 19 features&lt;/a&gt;. &lt;a href="https://safdarali.in/contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If this helped you&lt;/p&gt;
&lt;p&gt;I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;span&gt;→ &lt;/span&gt;&lt;a href="https://buymeacoffee.com/safdarali" rel="noopener noreferrer"&gt;Buy me a coffee at buymeacoffee.com/safdarali&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;👉 &lt;/span&gt;&lt;a href="https://www.youtube.com/@safdarali_?sub_confirmation=1" rel="noopener noreferrer"&gt;Subscribe to my YouTube channel&lt;/a&gt;&lt;span&gt; — it's free; 70+ React &amp;amp; Next.js tutorials&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="related-posts-heading"&gt;Related reading&lt;/h2&gt;
&lt;p&gt;More guides on safdarali.in — same author, production-focused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://safdarali.in&lt;br&gt;%0A/blog/free-video-thumbnail-generator-online-no-upload" rel="noopener noreferrer"&gt; &lt;h3&gt;Free Video Thumbnail Generator Online — No Upload&lt;/h3&gt;
&lt;p&gt;Free video thumbnail generator online — extract frames from video without upload. Browser-based, no watermark, YouTube thumbnail from MP4 workflow.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Jun 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/nextjs-15-still-slow-react-19-hydration-fix" rel="noopener noreferrer"&gt; &lt;h3&gt;Why Your Next.js 15 App is Still Slow (And How to Fix the React 19 Hydration Lag)&lt;/h3&gt;
&lt;p&gt;Next.js 15 performance optimization — fix INP, LCP layout shifts, React 19 hydration errors, and React Compiler gaps with a production DevTools workflow.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;May 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>ai</category>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>WCAG 2.2 Accessibility for React Developers — Practical Guide</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Thu, 09 Jul 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/safdarali25/wcag-22-accessibility-for-react-developers-practical-guide-1b7o</link>
      <guid>https://dev.to/safdarali25/wcag-22-accessibility-for-react-developers-practical-guide-1b7o</guid>
      <description>&lt;p&gt;I'm &lt;a href="https://safdarali.in" rel="noopener noreferrer"&gt;Safdar Ali&lt;/a&gt;, a frontend engineer in Bengaluru. Last quarter I audited a client dashboard that looked polished — clean Tailwind, smooth transitions, Lighthouse performance in the 90s — and failed basic keyboard navigation in under two minutes. Tab order jumped randomly, modals trapped nothing, and icon-only buttons had no labels. WCAG 2.2 is not a legal checkbox for enterprise contracts alone. It is how you ship React UI that works for everyone: screen reader users, keyboard-only users, people on slow 4G with zoom enabled, and your future self debugging at 11pm. This guide covers the wcag 2.2 react patterns I run before every merge.&lt;/p&gt;

&lt;p&gt;Why WCAG 2.2 matters for React in 2026&lt;br&gt;
WCAG 2.2 added criteria that directly affect React apps: focus not obscured, dragging movements, target size minimums, and consistent help. React's component model makes accessibility both easier and easier to break — you can encapsulate good patterns in a shared Dialog component, but you can also copy-paste a div-with-onClick button across forty files.&lt;/p&gt;

&lt;p&gt;The legal landscape in India is catching up. Government portals and fintech products increasingly require accessibility audits before launch. Even when nobody asks, inclusive UI reduces support tickets — unclear error messages and broken focus management generate more "the form is broken" emails than actual backend failures.&lt;/p&gt;

&lt;p&gt;React does not ship accessible components by default. A  is focusable; a  is not, unless you wire it. Your job is to make the accessible path the default path in your design system.&lt;/p&gt;


&lt;p&gt;Focus traps — modals that actually work&lt;br&gt;
A focus trap keeps keyboard focus inside a modal until the user dismisses it. Without one, Tab sends focus to elements behind the overlay — confusing for sighted keyboard users and disorienting for screen reader users who hear content from two layers at once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://safdarali.in/blog/wcag-22-react-accessibility-guide" rel="noopener noreferrer"&gt;Continue Reading...&lt;/a&gt;&lt;/p&gt;


</description>
      <category>wcag</category>
      <category>a11y</category>
      <category>react</category>
    </item>
    <item>
      <title>How I Structure Large Next.js Projects — Folder Architecture Guide</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Wed, 08 Jul 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/safdarali25/how-i-structure-large-nextjs-projects-folder-architecture-guide-2f2i</link>
      <guid>https://dev.to/safdarali25/how-i-structure-large-nextjs-projects-folder-architecture-guide-2f2i</guid>
      <description>&lt;p&gt;Bad &lt;strong&gt;nextjs folder structure&lt;/strong&gt; does not show up on day one. It shows up at month six when three developers search for the checkout form hook and find four copies. I reorganised a client dashboard after exactly that — this guide is the tree I use now on large App Router projects, why each folder exists, mistakes from my first Next.js apps, and the &lt;strong&gt;10-second findability rule&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="tree"&gt;Real folder tree — production-shaped layout&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;my-app/&lt;br&gt;
├── app/                    # routes only — thin pages&lt;br&gt;
│   ├── (marketing)/        # route group — shared layout, no URL segment&lt;br&gt;
│   │   ├── layout.tsx&lt;br&gt;
│   │   ├── page.tsx&lt;br&gt;
│   │   └── pricing/page.tsx&lt;br&gt;
│   ├── (dashboard)/&lt;br&gt;
│   │   ├── layout.tsx&lt;br&gt;
│   │   └── orders/page.tsx&lt;br&gt;
│   ├── api/                # route handlers&lt;br&gt;
│   │   └── webhooks/stripe/route.ts&lt;br&gt;
│   ├── layout.tsx          # root layout&lt;br&gt;
│   └── globals.css&lt;br&gt;
├── components/             # shared UI — buttons, cards, shell&lt;br&gt;
│   ├── ui/&lt;br&gt;
│   └── layout/&lt;br&gt;
├── features/               # business domains — colocated logic&lt;br&gt;
│   ├── auth/&lt;br&gt;
│   │   ├── components/&lt;br&gt;
│   │   ├── hooks/&lt;br&gt;
│   │   └── actions.ts&lt;br&gt;
│   └── orders/&lt;br&gt;
│       ├── components/&lt;br&gt;
│       ├── api.ts&lt;br&gt;
│       └── types.ts&lt;br&gt;
├── lib/                    # server + shared utilities&lt;br&gt;
│   ├── db.ts&lt;br&gt;
│   └── env.ts&lt;br&gt;
├── hooks/                  # truly global client hooks&lt;br&gt;
├── types/                  # global TS types&lt;br&gt;
├── data/                   # static data, blog posts list&lt;br&gt;
└── public/&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Routes live in &lt;code&gt;app/&lt;/code&gt;. Business logic lives in &lt;code&gt;features/&lt;/code&gt;. Generic design system pieces live in &lt;code&gt;components/ui&lt;/code&gt;. That separation is the whole game.&lt;/p&gt;
&lt;h2 id="why-each"&gt;Why each folder exists&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Folder&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Do not put here&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;app/&lt;/td&gt;
&lt;td&gt;URLs, layouts, loading.tsx&lt;/td&gt;
&lt;td&gt;Fat business logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;features/&lt;/td&gt;
&lt;td&gt;Domain modules (orders, auth)&lt;/td&gt;
&lt;td&gt;Generic Button&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;components/ui&lt;/td&gt;
&lt;td&gt;Reusable primitives&lt;/td&gt;
&lt;td&gt;Order-specific tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lib/&lt;/td&gt;
&lt;td&gt;DB clients, env validation&lt;/td&gt;
&lt;td&gt;React components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;app/api&lt;/td&gt;
&lt;td&gt;Webhooks, REST edge cases&lt;/td&gt;
&lt;td&gt;Every form POST (prefer actions)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2 id="thin-pages"&gt;Thin pages — route files under 40 lines&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// app/(dashboard)/orders/page.tsx — orchestration only&lt;br&gt;
import { OrderTable } from "@/features/orders/components/OrderTable";&lt;br&gt;
import { getOrders } from "@/features/orders/api";

&lt;p&gt;export default async function OrdersPage() {&lt;br&gt;
  const orders = await getOrders();&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;section&amp;gt;&lt;br&gt;
      &amp;lt;h1&amp;gt;Orders&amp;lt;/h1&amp;gt;&lt;br&gt;
      &amp;lt;OrderTable rows={orders} /&amp;gt;&lt;br&gt;
    &amp;lt;/section&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your page file exceeds a screen, extract to features. Pages should read like a table of contents.&lt;/p&gt;
&lt;p&gt;Thin pages also make Server Component boundaries obvious: data fetching stays in api.ts or the page, UI in feature components, client interactivity in a single leaf with "use client". When a page mixes all three, reviews slow down and hydration bugs hide in the middle of a 200-line file.&lt;/p&gt;
&lt;h2 id="findability"&gt;The 10-second findability rule&lt;/h2&gt;
&lt;p&gt;The rule applies to you six months later, not only new hires. I have opened my own repos and lost minutes searching for a webhook handler buried under utils — that shame is why I enforce feature folders now.&lt;/p&gt;
&lt;p&gt;Ask: "Where is the code that sends the password reset email?" If you cannot answer in ten seconds, the structure failed. Correct answer in my tree: &lt;code&gt;features/auth/actions.ts&lt;/code&gt; or &lt;code&gt;features/auth/emails.ts&lt;/code&gt; — not scattered across utils, hooks, and app/api.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Naming convention that helps findability&lt;br&gt;
features/orders/&lt;br&gt;
  components/   → OrderTable.tsx, OrderFilters.tsx&lt;br&gt;
  hooks/        → useOrderFilters.ts (client)&lt;br&gt;
  api.ts        → getOrders, createOrder (server-safe)&lt;br&gt;
  types.ts      → Order, OrderStatus&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="mistakes"&gt;First project mistakes I made&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;components/&lt;/strong&gt; dump with 80 files and no subfolders. &lt;strong&gt;utils.ts&lt;/strong&gt; at 1,200 lines. Duplicating fetch logic in every page instead of feature api modules. Putting Server Actions inside random page files instead of colocated actions.ts.&lt;/p&gt;
&lt;p&gt;Another mistake: mirroring REST URLs in folder names under components — &lt;code&gt;components/api/users/get.tsx&lt;/code&gt; — which fights App Router conventions. Routes belong in app/; business logic belongs in features/. Mixing them creates two sources of truth for the same URL.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — everything in components/&lt;br&gt;
components/&lt;br&gt;
  OrderTable.tsx&lt;br&gt;
  UserAvatar.tsx&lt;br&gt;
  pricing-card.tsx&lt;br&gt;
  helper.ts

&lt;p&gt;// AFTER — domain colocation&lt;br&gt;
features/orders/components/OrderTable.tsx&lt;br&gt;
features/users/components/UserAvatar.tsx&lt;br&gt;
app/(marketing)/pricing/page.tsx  // thin&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="route-groups"&gt;Route groups for different shells&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// app/(marketing)/layout.tsx — public nav, footer&lt;br&gt;
// app/(dashboard)/layout.tsx — sidebar, auth gate

&lt;p&gt;// URL stays /pricing and /orders — parentheses omit segment&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pair with rendering choices from &lt;a href="https://safdarali.in/blog/ssr-ssg-isr-nextjs-explained" rel="noopener noreferrer"&gt;SSR vs SSG vs ISR guide&lt;/a&gt; — marketing group ISR, dashboard group client-heavy.&lt;/p&gt;
&lt;h2 id="imports"&gt;Path aliases and import direction&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// tsconfig paths — "@/&lt;em&gt;": ["./&lt;/em&gt;"]

&lt;p&gt;// Allowed import flow:&lt;br&gt;
// app → features → components/ui → lib&lt;br&gt;
// features → lib&lt;br&gt;
// Never: lib → features (inverts layers)&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Enforce with ESLint import rules when the team grows past three people.&lt;/p&gt;
&lt;p&gt;I also keep a single &lt;code&gt;README.md&lt;/code&gt; at the repo root with a one-paragraph map: where routes live, where features live, how to run migrations. The tree in this article is the visual version of that map — new contractors read it before their first PR.&lt;/p&gt;
&lt;h2 id="production"&gt;My production setup&lt;/h2&gt;
&lt;p&gt;In production on this portfolio and client sites: route groups for marketing vs app shells, features/ for blog and contact logic, components/ui for sparkles and buttons. TypeScript strict — see &lt;a href="https://safdarali.in/blog/typescript-strict-mode-guide-2026" rel="noopener noreferrer"&gt;strict mode guide&lt;/a&gt;. New engineers onboard by reading features/ first, not app/ line by line.&lt;/p&gt;
&lt;p&gt;At my day job, we added a CODEOWNERS file per feature folder — reviews stay scoped. Structure is team policy, not personal taste.&lt;/p&gt;
&lt;p&gt;When a feature folder grows past ~15 files, split subdomains — &lt;code&gt;features/orders/checkout/&lt;/code&gt; vs &lt;code&gt;features/orders/history/&lt;/code&gt; — before you invent a second top-level folder that duplicates the domain name.&lt;/p&gt;
&lt;h2 id="closing"&gt;The single takeaway&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Routes in app/, domains in features/, primitives in components/ui.&lt;/strong&gt; If findability fails the 10-second test, refactor before adding the next feature.&lt;/p&gt;
&lt;p&gt;Related: &lt;a href="https://safdarali.in/blog/nextjs-app-router-complete-guide-2026" rel="noopener noreferrer"&gt;App Router beginner guide&lt;/a&gt;. &lt;a href="https://safdarali.in/contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If this helped you&lt;/p&gt;
&lt;p&gt;I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;span&gt;→ &lt;/span&gt;&lt;a href="https://buymeacoffee.com/safdarali" rel="noopener noreferrer"&gt;Buy me a coffee at buymeacoffee.com/safdarali&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;👉 &lt;/span&gt;&lt;a href="https://www.youtube.com/@safdarali_?sub_confirmation=1" rel="noopener noreferrer"&gt;Subscribe to my YouTube channel&lt;/a&gt;&lt;span&gt; — it's free; 70+ React &amp;amp; Next.js tutorials&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="related-posts-heading"&gt;Related reading&lt;/h2&gt;
&lt;p&gt;More guides on safdarali.in — same author, production-focused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/frontend-developer-portfolio-guide-india-2026" rel="noopener noreferrer"&gt; &lt;h3&gt;How to Build a Frontend Developer Portfolio That Stands Out&lt;/h3&gt;
&lt;p&gt;Frontend developer portfolio guide for India — sections, React/Next.js examples, SEO, performance, personal branding, FAQ, and checklist to build and rank.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;May 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/rsc-vs-client-components" rel="noopener noreferrer"&gt; &lt;h3&gt;React Server Components vs Client Components — When to Use Which&lt;/h3&gt;
&lt;p&gt;Practical RSC vs client guide for Next.js App Router — when to use each, real code, bundle before/after, and performance impact.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;May 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>React useCallback vs useMemo — When You Actually Need Them</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Wed, 08 Jul 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/safdarali25/react-usecallback-vs-usememo-when-you-actually-need-them-1bef</link>
      <guid>https://dev.to/safdarali25/react-usecallback-vs-usememo-when-you-actually-need-them-1bef</guid>
      <description>&lt;p&gt;Junior PRs in my reviews often wrap every function in &lt;code&gt;useCallback&lt;/code&gt; and every array in &lt;code&gt;useMemo&lt;/code&gt; because a blog post said so. The React Profiler showed the opposite: more memoisation, more comparison work, slower interactions. This &lt;strong&gt;usecallback vs usememo&lt;/strong&gt; guide is profiler-first — what each hook does, when it helps, and the over-memoising mistake I made on a real table.&lt;/p&gt;
&lt;h2 id="definitions"&gt;What each hook actually does&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;useMemo&lt;/strong&gt; caches a &lt;em&gt;computed value&lt;/em&gt; between renders when dependencies are unchanged. &lt;strong&gt;useCallback&lt;/strong&gt; caches a &lt;em&gt;function reference&lt;/em&gt; — it is useMemo for functions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useMemo, useCallback } from "react";

&lt;p&gt;function Dashboard({ rows }: { rows: Row[] }) {&lt;br&gt;
  const total = useMemo(() =&amp;gt; rows.reduce((sum, r) =&amp;gt; sum + r.amount, 0), [rows]);&lt;/p&gt;

&lt;p&gt;const handleExport = useCallback(() =&amp;gt; {&lt;br&gt;
    downloadCsv(rows);&lt;br&gt;
  }, [rows]);&lt;/p&gt;

&lt;p&gt;return &amp;lt;Toolbar total={total} onExport={handleExport} /&amp;gt;;&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Neither hook prevents re-renders by itself. They only help when a child skips render because props are referentially equal — usually with &lt;code&gt;React.memo&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A common confusion: developers think useMemo stops the parent re-rendering. It does not. The parent still runs its function body every time state changes — useMemo only skips recomputing one expression inside that run. useCallback is the same for function identity. If your performance problem is "the whole page re-renders on every keystroke," move state down or split context before touching memo hooks.&lt;/p&gt;
&lt;h2 id="over-memo"&gt;The over-memoising mistake I made&lt;/h2&gt;
&lt;p&gt;On a 500-row admin table I wrapped every cell formatter in useMemo and passed useCallback handlers to each row. Scroll jank got worse — dependency checks on every scroll event dominated.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — memo soup, slower scroll&lt;br&gt;
const formatted = useMemo(&lt;br&gt;
  () =&amp;gt; rows.map((r) =&amp;gt; formatCurrency(r.amount)),&lt;br&gt;
  [rows]&lt;br&gt;
);&lt;br&gt;
const onRowClick = useCallback((id: string) =&amp;gt; navigate("/row/" + id), []);

&lt;p&gt;// AFTER — virtualise the list, drop per-row memo&lt;br&gt;
import { useVirtualizer } from "@tanstack/react-virtual";&lt;br&gt;
// Only memo expensive derived data used by memoised children&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fix was virtualisation + smaller components, not more hooks. Lesson: measure before memoising.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — unstable inline object recreates every parent render&lt;br&gt;
&amp;lt;HeavyChart config={{ theme: "dark", showGrid: true }} /&amp;gt;

&lt;p&gt;// AFTER — stable reference only if HeavyChart is memoised&lt;br&gt;
const config = useMemo(() =&amp;gt; ({ theme: "dark", showGrid: true }), []);&lt;br&gt;
&amp;lt;HeavyChart config={config} /&amp;gt;&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without React.memo on HeavyChart, the AFTER snippet still re-renders the chart — you paid for useMemo and got nothing. That is the over-memoising pattern: hooks without a memoised consumer.&lt;/p&gt;
&lt;h2 id="profiler"&gt;How I use the React Profiler&lt;/h2&gt;
&lt;p&gt;Chrome React DevTools → Profiler → record interaction → look for components with long render times and high render counts. Yellow bars mean wasted renders. I fix structure first (split context, move state down, virtual lists), then add memo where a memoised child still re-renders with stable props.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Wrap only when Profiler shows child re-renders are expensive&lt;br&gt;
import { memo } from "react";

&lt;p&gt;const HeavyChart = memo(function HeavyChart({ data }: { data: Point[] }) {&lt;br&gt;
  // expensive canvas draw&lt;br&gt;
  return &amp;lt;canvas /&amp;gt;;&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;// Parent must pass stable data reference&lt;br&gt;
const data = useMemo(() =&amp;gt; computePoints(raw), [raw]);&lt;br&gt;
return &amp;lt;HeavyChart data={data} /&amp;gt;;&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On marketing sites built with Server Components, much of the tree never hydrates — memo hooks on the server branch are pointless. See &lt;a href="https://safdarali.in/blog/rsc-vs-client-components" rel="noopener noreferrer"&gt;RSC vs client components&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="when-table"&gt;When to use which — table&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;useMemo&lt;/th&gt;
&lt;th&gt;useCallback&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Expensive calculation&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stable prop to memo child&lt;/td&gt;
&lt;td&gt;For objects/arrays&lt;/td&gt;
&lt;td&gt;For functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useEffect dependency&lt;/td&gt;
&lt;td&gt;Rarely needed&lt;/td&gt;
&lt;td&gt;Sometimes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primitive props to memo child&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List without virtualisation&lt;/td&gt;
&lt;td&gt;Fix list first&lt;/td&gt;
&lt;td&gt;Fix list first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context provider value&lt;/td&gt;
&lt;td&gt;Often yes (object)&lt;/td&gt;
&lt;td&gt;Callbacks inside value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2 id="usecallback-example"&gt;useCallback — legitimate use with memoised child&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;"use client";&lt;br&gt;
import { memo, useCallback, useState } from "react";

&lt;p&gt;const SearchInput = memo(function SearchInput({&lt;br&gt;
  onSearch,&lt;br&gt;
}: {&lt;br&gt;
  onSearch: (q: string) =&amp;gt; void;&lt;br&gt;
}) {&lt;br&gt;
  console.log("SearchInput render");&lt;br&gt;
  return &amp;lt;input onChange={(e) =&amp;gt; onSearch(e.target.value)} /&amp;gt;;&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;export function FilterBar() {&lt;br&gt;
  const [query, setQuery] = useState("");&lt;br&gt;
  const onSearch = useCallback((q: string) =&amp;gt; setQuery(q), []);&lt;br&gt;
  return &amp;lt;SearchInput onSearch={onSearch} /&amp;gt;;&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without useCallback, SearchInput re-renders whenever FilterBar state unrelated to search changes — only worth fixing if Profiler proves SearchInput is costly.&lt;/p&gt;
&lt;h2 id="usememo-example"&gt;useMemo — filtering large lists&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const filtered = useMemo(&lt;br&gt;
  () =&amp;gt; products.filter((p) =&amp;gt; p.name.toLowerCase().includes(query.toLowerCase())),&lt;br&gt;
  [products, query]&lt;br&gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the filter runs in under a millisecond on your data size, skip useMemo — the hook overhead can cost more than the loop.&lt;/p&gt;
&lt;h2 id="rule-thumb"&gt;Rule of thumb I teach in reviews&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Default: no memo hooks.&lt;/strong&gt; Add when Profiler shows a problem. useMemo for expensive derived data consumed by memoised children. useCallback for stable handlers passed to those children. Never memoise to silence ESLint exhaustive-deps without understanding why deps change.&lt;/p&gt;
&lt;p&gt;React 19 and the future compiler may auto-memoise — until then, stay boring. Performance wins on public sites still come from server rendering and smaller bundles — &lt;a href="https://safdarali.in/blog/nextjs-performance-60-percent" rel="noopener noreferrer"&gt;Next.js performance guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="production"&gt;My production setup&lt;/h2&gt;
&lt;p&gt;In production I memoise chart components and heavy derived selectors in dashboards. Marketing pages: almost zero useCallback/useMemo. At my day job I comment why when memo is added — "Profiler: Chart 40ms → 8ms" — so the next dev does not delete it blindly.&lt;/p&gt;
&lt;p&gt;When mentoring in Bengaluru, I ask juniors to screenshot Profiler flamegraphs in PRs that add memo hooks — not because I love process, but because it proves they measured. Copy-pasting useCallback from Stack Overflow without a memo child is the most common wasted line I delete in review.&lt;/p&gt;
&lt;h2 id="closing"&gt;The single takeaway&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;useCallback stabilises functions; useMemo stabilises values.&lt;/strong&gt; Both only matter when referential equality blocks wasted work. Profile first, memo second.&lt;/p&gt;
&lt;p&gt;Related: &lt;a href="https://safdarali.in/blog/react-19-features-production-guide" rel="noopener noreferrer"&gt;React 19 features&lt;/a&gt;. &lt;a href="https://safdarali.in/contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If this helped you&lt;/p&gt;
&lt;p&gt;I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;span&gt;→ &lt;/span&gt;&lt;a href="https://buymeacoffee.com/safdarali" rel="noopener noreferrer"&gt;Buy me a coffee at buymeacoffee.com/safdarali&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;👉 &lt;/span&gt;&lt;a href="https://www.youtube.com/@safdarali_?sub_confirmation=1" rel="noopener noreferrer"&gt;Subscribe to my YouTube channel&lt;/a&gt;&lt;span&gt; — it's free; 70+ React &amp;amp; Next.js tutorials&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="related-posts-heading"&gt;Related reading&lt;/h2&gt;
&lt;p&gt;More guides on safdarali.in — same author, production-focused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/frontend-developer-portfolio-guide-india-2026" rel="noopener noreferrer"&gt; &lt;h3&gt;How to Build a Frontend Developer Portfolio That Stands Out&lt;/h3&gt;
&lt;p&gt;Frontend developer portfolio guide for India — sections, React/Next.js examples, SEO, performance, personal branding, FAQ, and checklist to build and rank.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;May 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/rsc-vs-client-components" rel="noopener noreferrer"&gt; &lt;h3&gt;React Server Components vs Client Components — When to Use Which&lt;/h3&gt;
&lt;p&gt;Practical RSC vs client guide for Next.js App Router — when to use each, real code, bundle before/after, and performance impact.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;May 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>usecallback</category>
      <category>usememo</category>
      <category>ai</category>
    </item>
    <item>
      <title>Zustand vs Redux Toolkit — Which State Manager in 2026?</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/safdarali25/zustand-vs-redux-toolkit-which-state-manager-in-2026-1dee</link>
      <guid>https://dev.to/safdarali25/zustand-vs-redux-toolkit-which-state-manager-in-2026-1dee</guid>
      <description>&lt;p&gt;Global state still matters in 2026 — but most pages should not need it. When they do, the debate is &lt;strong&gt;zustand vs redux 2026&lt;/strong&gt;: Redux Toolkit (RTK) is the enterprise default; Zustand is the minimal store juniors actually read. I have shipped both on dashboards in production. This article implements the same counter plus async user fetch in each, compares bundle size, and ends with what I pick for new repos.&lt;/p&gt;
&lt;h2 id="zustand-counter"&gt;Zustand — counter and async fetch&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// store/useAppStore.ts&lt;br&gt;
import { create } from "zustand";

&lt;p&gt;type User = { id: string; name: string } | null;&lt;/p&gt;

&lt;p&gt;type State = {&lt;br&gt;
  count: number;&lt;br&gt;
  user: User;&lt;br&gt;
  loading: boolean;&lt;br&gt;
  increment: () =&amp;gt; void;&lt;br&gt;
  fetchUser: () =&amp;gt; Promise&amp;lt;void&amp;gt;;&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;export const useAppStore = create&amp;lt;State&amp;gt;((set) =&amp;gt; ({&lt;br&gt;
  count: 0,&lt;br&gt;
  user: null,&lt;br&gt;
  loading: false,&lt;br&gt;
  increment: () =&amp;gt; set((s) =&amp;gt; ({ count: s.count + 1 })),&lt;br&gt;
  fetchUser: async () =&amp;gt; {&lt;br&gt;
    set({ loading: true });&lt;br&gt;
    const res = await fetch("/api/me");&lt;br&gt;
    const user = await res.json();&lt;br&gt;
    set({ user, loading: false });&lt;br&gt;
  },&lt;br&gt;
}));&lt;/p&gt;

&lt;p&gt;// components/CounterPanel.tsx&lt;br&gt;
"use client";&lt;br&gt;
import { useAppStore } from "@/store/useAppStore";&lt;/p&gt;

&lt;p&gt;export function CounterPanel() {&lt;br&gt;
  const count = useAppStore((s) =&amp;gt; s.count);&lt;br&gt;
  const increment = useAppStore((s) =&amp;gt; s.increment);&lt;br&gt;
  return &amp;lt;button onClick={increment}&amp;gt;Count: {count}&amp;lt;/button&amp;gt;;&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No providers, no slices folder — one file, selective subscriptions via selectors. That is why Zustand spreads on small teams.&lt;/p&gt;
&lt;p&gt;The async fetch above is intentionally boring — no thunk middleware, just async/await inside the store action. For error handling you extend with try/catch and an error field; for retries you either wrap fetch or move the request to TanStack Query. Zustand does not prescribe async patterns, which is freedom or chaos depending on team discipline.&lt;/p&gt;
&lt;h2 id="rtk-counter"&gt;Redux Toolkit — same features, more structure&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// store/appSlice.ts&lt;br&gt;
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";

&lt;p&gt;export const fetchUser = createAsyncThunk("app/fetchUser", async () =&amp;gt; {&lt;br&gt;
  const res = await fetch("/api/me");&lt;br&gt;
  return res.json();&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;const appSlice = createSlice({&lt;br&gt;
  name: "app",&lt;br&gt;
  initialState: { count: 0, user: null as null | { id: string; name: string }, loading: false },&lt;br&gt;
  reducers: {&lt;br&gt;
    increment: (state) =&amp;gt; { state.count += 1; },&lt;br&gt;
  },&lt;br&gt;
  extraReducers: (builder) =&amp;gt; {&lt;br&gt;
    builder&lt;br&gt;
      .addCase(fetchUser.pending, (state) =&amp;gt; { state.loading = true; })&lt;br&gt;
      .addCase(fetchUser.fulfilled, (state, action) =&amp;gt; {&lt;br&gt;
        state.user = action.payload;&lt;br&gt;
        state.loading = false;&lt;br&gt;
      });&lt;br&gt;
  },&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;export const { increment } = appSlice.actions;&lt;br&gt;
export default appSlice.reducer;&lt;/p&gt;

&lt;p&gt;// app/providers.tsx + useSelector in components&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RTK removes classic Redux boilerplate but still needs a store provider, typed hooks, and slice conventions — worth it when ten engineers touch the same state graph.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// components/UserPanel.tsx — RTK async usage&lt;br&gt;
"use client";&lt;br&gt;
import { useEffect } from "react";&lt;br&gt;
import { useAppDispatch, useAppSelector } from "@/store/hooks";&lt;br&gt;
import { fetchUser, increment } from "@/store/appSlice";

&lt;p&gt;export function UserPanel() {&lt;br&gt;
  const dispatch = useAppDispatch();&lt;br&gt;
  const count = useAppSelector((s) =&amp;gt; s.app.count);&lt;br&gt;
  const user = useAppSelector((s) =&amp;gt; s.app.user);&lt;br&gt;
  const loading = useAppSelector((s) =&amp;gt; s.app.loading);&lt;/p&gt;

&lt;p&gt;useEffect(() =&amp;gt; {&lt;br&gt;
    dispatch(fetchUser());&lt;br&gt;
  }, [dispatch]);&lt;/p&gt;

&lt;p&gt;return (&lt;br&gt;
    &amp;lt;div&amp;gt;&lt;br&gt;
      &amp;lt;button onClick={() =&amp;gt; dispatch(increment())}&amp;gt;Count: {count}&amp;lt;/button&amp;gt;&lt;br&gt;
      {loading ? &amp;lt;p&amp;gt;Loading…&amp;lt;/p&amp;gt; : &amp;lt;p&amp;gt;{user?.name ?? "Guest"}&amp;lt;/p&amp;gt;}&lt;br&gt;
    &amp;lt;/div&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same UX as the Zustand panel — more files, clearer audit trail in Redux DevTools when a bug report says "count jumped to 99 after refresh." That traceability is why enterprise codebases keep RTK despite smaller alternatives.&lt;/p&gt;
&lt;h2 id="bundle"&gt;Bundle size comparison (approximate, gzipped)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Measured on a minimal Next.js 15 client chunk (2026, rough)&lt;br&gt;
// zustand@5 alone          ~ 1.2 kB gzip&lt;br&gt;
// @reduxjs/toolkit + react-redux ~ 12–14 kB gzip&lt;br&gt;
// Note: RTK buys DevTools, middleware patterns, large-team norms&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Numbers vary with tree-shaking and what you import. For a marketing site with one modal flag, neither library may be necessary — React context or URL state is enough. For a data-heavy dashboard, 12 kB might be cheap compared to engineering consistency.&lt;/p&gt;
&lt;p&gt;Before you optimise kilobytes, profile real user metrics — I document that workflow in &lt;a href="https://safdarali.in/blog/nextjs-performance-60-percent" rel="noopener noreferrer"&gt;Next.js performance case study&lt;/a&gt;. A 10 kB store library rarely matters next to an unvirtualised table or a chart library. Still, greenfield SPAs with tight mobile budgets in India often pick Zustand because every gram of JS counts on 4G.&lt;/p&gt;
&lt;h2 id="table"&gt;10-criteria comparison table&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;Zustand&lt;/th&gt;
&lt;th&gt;Redux Toolkit&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boilerplate&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;td&gt;Structured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DevTools&lt;/td&gt;
&lt;td&gt;Plugin available&lt;/td&gt;
&lt;td&gt;Excellent native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Middleware&lt;/td&gt;
&lt;td&gt;Custom, light&lt;/td&gt;
&lt;td&gt;Mature ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Async patterns&lt;/td&gt;
&lt;td&gt;You write it&lt;/td&gt;
&lt;td&gt;createAsyncThunk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team scale&lt;/td&gt;
&lt;td&gt;Small–medium&lt;/td&gt;
&lt;td&gt;Medium–large&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js App Router&lt;/td&gt;
&lt;td&gt;Client-only store&lt;/td&gt;
&lt;td&gt;Client-only store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selectors&lt;/td&gt;
&lt;td&gt;Inline functions&lt;/td&gt;
&lt;td&gt;reselect / createSelector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Easy store reset&lt;/td&gt;
&lt;td&gt;Well-documented patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hiring familiarity in India&lt;/td&gt;
&lt;td&gt;Growing fast&lt;/td&gt;
&lt;td&gt;Still very common&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2 id="before-after"&gt;Before and after — prop drilling vs store&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — theme passed through five layers&lt;br&gt;
&amp;lt;Layout theme={theme} setTheme={setTheme}&amp;gt;&lt;br&gt;
  &amp;lt;Sidebar theme={theme} setTheme={setTheme}&amp;gt;&lt;br&gt;
    &amp;lt;Nav theme={theme} setTheme={setTheme} /&amp;gt;

&lt;p&gt;// AFTER — Zustand (or RTK) at leaves only&lt;br&gt;
const theme = useAppStore((s) =&amp;gt; s.theme);&lt;br&gt;
const setTheme = useAppStore((s) =&amp;gt; s.setTheme);&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do not reach for a global store because props are annoying once — reach when multiple distant trees share writable state that is not server data.&lt;/p&gt;
&lt;h2 id="server-state"&gt;Server state is not Redux or Zustand&lt;/h2&gt;
&lt;p&gt;API lists, pagination, cache invalidation — use TanStack Query or Server Components + &lt;code&gt;fetch&lt;/code&gt; with cache tags. I see teams stuff fetch results into Redux out of habit; that duplicates what Next.js already solves on public pages — see &lt;a href="https://safdarali.in/blog/ssr-ssg-isr-nextjs-explained" rel="noopener noreferrer"&gt;SSR vs SSG vs ISR&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="production"&gt;My production setup&lt;/h2&gt;
&lt;p&gt;New dashboard in 2026: Zustand for UI chrome (sidebar, filters, wizard step). RTK when joining a legacy codebase that already exports slices and middleware. At my day job, the RTK codebase had time-travel debugging worth the bytes; greenfield internal tools get Zustand in under an hour.&lt;/p&gt;
&lt;p&gt;Pair with &lt;a href="https://safdarali.in/blog/rsc-vs-client-components" rel="noopener noreferrer"&gt;RSC boundaries&lt;/a&gt; — stores are client-only; never import them into Server Components.&lt;/p&gt;
&lt;h2 id="closing"&gt;The single takeaway&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Zustand for new small/medium apps; RTK for large coordinated teams.&lt;/strong&gt; Measure bundle impact, but optimise for maintainability. Most state should stay local or on the server.&lt;/p&gt;
&lt;p&gt;Related: &lt;a href="https://safdarali.in/blog/usecallback-vs-usememo-react-guide" rel="noopener noreferrer"&gt;useCallback vs useMemo&lt;/a&gt;. &lt;a href="https://safdarali.in/contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If this helped you&lt;/p&gt;
&lt;p&gt;I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;span&gt;→ &lt;/span&gt;&lt;a href="https://buymeacoffee.com/safdarali" rel="noopener noreferrer"&gt;Buy me a coffee at buymeacoffee.com/safdarali&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;👉 &lt;/span&gt;&lt;a href="https://www.youtube.com/@safdarali_?sub_confirmation=1" rel="noopener noreferrer"&gt;Subscribe to my YouTube channel&lt;/a&gt;&lt;span&gt; — it's free; 70+ React &amp;amp; Next.js tutorials&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="related-posts-heading"&gt;Related reading&lt;/h2&gt;
&lt;p&gt;More guides on safdarali.in — same author, production-focused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/tailwind-css-vs-css-modules-2026" rel="noopener noreferrer"&gt; &lt;h3&gt;Tailwind CSS vs CSS Modules — What I Use in Production&lt;/h3&gt;
&lt;p&gt;Tailwind vs CSS Modules 2026 — side-by-side component, 8-criteria table, and what Safdar Ali ships in production.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Jun 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/free-video-thumbnail-generator-online-no-upload" rel="noopener noreferrer"&gt; &lt;h3&gt;Free Video Thumbnail Generator Online — No Upload&lt;/h3&gt;
&lt;p&gt;Free video thumbnail generator online — extract frames from video without upload. Browser-based, no watermark, YouTube thumbnail from MP4 workflow.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Jun 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>zustand</category>
      <category>redux</category>
      <category>react</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Tailwind CSS vs CSS Modules — What I Use in Production</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Mon, 06 Jul 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/safdarali25/tailwind-css-vs-css-modules-what-i-use-in-production-3cap</link>
      <guid>https://dev.to/safdarali25/tailwind-css-vs-css-modules-what-i-use-in-production-3cap</guid>
      <description>&lt;p&gt;Every new project sparks the same debate: utility classes or scoped CSS files? I ship &lt;strong&gt;Tailwind CSS&lt;/strong&gt; on most Next.js marketing sites — including &lt;a href="https://safdarali.in/" rel="noopener noreferrer"&gt;safdarali.in&lt;/a&gt; — but I still reach for &lt;strong&gt;CSS Modules&lt;/strong&gt; when design tokens, animations, or designer handoff demand real stylesheets. This &lt;strong&gt;tailwind vs css modules&lt;/strong&gt; comparison uses the same profile card in both approaches so you judge ergonomics, not different UIs.&lt;/p&gt;
&lt;h2 id="tailwind-version"&gt;Same component — Tailwind version&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// components/ProfileCard.tsx&lt;br&gt;
type Props = { name: string; role: string; avatarUrl: string };

&lt;p&gt;export function ProfileCardTailwind({ name, role, avatarUrl }: Props) {&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;article className="flex gap-4 rounded-2xl border border-neutral-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-neutral-900"&amp;gt;&lt;br&gt;
      &amp;lt;img&lt;br&gt;
        src={avatarUrl}&lt;br&gt;
        alt=""&lt;br&gt;
        className="h-14 w-14 rounded-full object-cover ring-2 ring-neutral-200 dark:ring-white/20"&lt;br&gt;
      /&amp;gt;&lt;br&gt;
      &amp;lt;div&amp;gt;&lt;br&gt;
        &amp;lt;h3 className="font-semibold text-neutral-950 dark:text-white"&amp;gt;{name}&amp;lt;/h3&amp;gt;&lt;br&gt;
        &amp;lt;p className="text-sm text-neutral-600 dark:text-neutral-400"&amp;gt;{role}&amp;lt;/p&amp;gt;&lt;br&gt;
      &amp;lt;/div&amp;gt;&lt;br&gt;
    &amp;lt;/article&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No context switch — layout, colour, and dark mode sit on the JSX. Colocation is excellent when you iterate alone. Class strings get long on complex grids; I break into smaller components instead of one 200-character line.&lt;/p&gt;
&lt;p&gt;On a Bengaluru startup landing page last quarter, the designer changed card padding three times in one sprint. With Tailwind I adjusted utilities in the same PR as copy changes — no hunting a separate CSS file. That speed is why I default to utilities for marketing. The tradeoff is readability in code review: reviewers must know common class names or rely on preview deploys.&lt;/p&gt;
&lt;h2 id="modules-version"&gt;Same component — CSS Modules version&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// components/ProfileCard.module.css&lt;br&gt;
.card {&lt;br&gt;
  display: flex;&lt;br&gt;
  gap: 1rem;&lt;br&gt;
  padding: 1.25rem;&lt;br&gt;
  border-radius: 1rem;&lt;br&gt;
  border: 1px solid var(--border);&lt;br&gt;
  background: var(--surface);&lt;br&gt;
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.06);&lt;br&gt;
}&lt;br&gt;
.avatar {&lt;br&gt;
  width: 3.5rem;&lt;br&gt;
  height: 3.5rem;&lt;br&gt;
  border-radius: 9999px;&lt;br&gt;
  object-fit: cover;&lt;br&gt;
}&lt;br&gt;
.name { font-weight: 600; }&lt;br&gt;
.role { font-size: 0.875rem; color: var(--muted); }&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// components/ProfileCard.tsx&lt;br&gt;
import styles from "./ProfileCard.module.css";

&lt;p&gt;export function ProfileCardModules({ name, role, avatarUrl }: Props) {&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;article className={styles.card}&amp;gt;&lt;br&gt;
      &amp;lt;img src={avatarUrl} alt="" className={styles.avatar} /&amp;gt;&lt;br&gt;
      &amp;lt;div&amp;gt;&lt;br&gt;
        &amp;lt;h3 className={styles.name}&amp;gt;{name}&amp;lt;/h3&amp;gt;&lt;br&gt;
        &amp;lt;p className={styles.role}&amp;gt;{role}&amp;lt;/p&amp;gt;&lt;br&gt;
      &amp;lt;/div&amp;gt;&lt;br&gt;
    &amp;lt;/article&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Designers reading Figma specs often prefer this — class names map to a stylesheet they can search. Theme variables live in one CSS file instead of duplicating dark: prefixes.&lt;/p&gt;
&lt;p&gt;CSS Modules shine when you inherit a brand system documented as SCSS or plain CSS. I once joined a project where every spacing token lived in variables — rewriting into Tailwind would have taken weeks for zero user benefit. Modules let us wrap legacy styles with scoped class names while new React components shipped.&lt;/p&gt;
&lt;h2 id="comparison"&gt;8-criteria comparison table&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;Tailwind CSS&lt;/th&gt;
&lt;th&gt;CSS Modules&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Colocation with JSX&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Split files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design system tokens&lt;/td&gt;
&lt;td&gt;tailwind.config theme&lt;/td&gt;
&lt;td&gt;CSS variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle size&lt;/td&gt;
&lt;td&gt;Purged utilities (small)&lt;/td&gt;
&lt;td&gt;Only used classes ship&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complex animations&lt;/td&gt;
&lt;td&gt;Verbose in utilities&lt;/td&gt;
&lt;td&gt;Natural in CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onboarding juniors&lt;/td&gt;
&lt;td&gt;Learn utility names&lt;/td&gt;
&lt;td&gt;Learn CSS + scoping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readable diffs&lt;/td&gt;
&lt;td&gt;Long class strings&lt;/td&gt;
&lt;td&gt;Cleaner CSS diffs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third-party overrides&lt;/td&gt;
&lt;td&gt;
&lt;a class="mentioned-user" href="https://dev.to/apply"&gt;@apply&lt;/a&gt; or arbitrary&lt;/td&gt;
&lt;td&gt;:global() when needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js default vibe&lt;/td&gt;
&lt;td&gt;Very common&lt;/td&gt;
&lt;td&gt;Built-in support&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2 id="when-tailwind-loses"&gt;When Tailwind loses&lt;/h2&gt;
&lt;p&gt;Tailwind is not weak — it is the wrong tool when the stylesheet is the product. Long-form editorial layouts, printable invoices, and white-label themes where each tenant ships different CSS files are easier to reason about in modules or global layers than in thousands of arbitrary utility strings.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keyframe-heavy animations&lt;/strong&gt; — staggered reveals and complex hover chains read better in a module file. &lt;strong&gt;Print stylesheets&lt;/strong&gt; — Tailwind can do it; CSS is clearer. &lt;strong&gt;Legacy design systems&lt;/strong&gt; already documented in SCSS variables — rewriting into utilities is waste. &lt;strong&gt;Highly bespoke art-directed pages&lt;/strong&gt; where every section has unique spacing not in your token scale — fighting arbitrary values is slower than writing CSS.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* ProfileCard.module.css — animation Tailwind would fight */&lt;br&gt;
@keyframes riseIn {&lt;br&gt;
  from { opacity: 0; transform: translateY(12px); }&lt;br&gt;
  to { opacity: 1; transform: translateY(0); }&lt;br&gt;
}&lt;br&gt;
.cardAnimated {&lt;br&gt;
  animation: riseIn 0.4s ease-out both;&lt;br&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="before-after"&gt;Migrating styles — before and after&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — global CSS leaked into production&lt;br&gt;
.button {&lt;br&gt;
  background: blue;&lt;br&gt;
}&lt;br&gt;
// Every &amp;lt;button&amp;gt; on the site turned blue

&lt;p&gt;// AFTER — CSS Modules scope automatically&lt;br&gt;
// Button.module.css&lt;br&gt;
.root { background: var(--brand); }&lt;br&gt;
// Only &amp;lt;button className={styles.root}&amp;gt;&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tailwind avoids global leakage differently — utilities are atomic. Both beat unstructured global CSS from 2018 Create React App projects.&lt;/p&gt;
&lt;h2 id="hybrid"&gt;The hybrid I use on client sites&lt;/h2&gt;
&lt;p&gt;In production: Tailwind for layout, spacing, typography, responsive grids. CSS Modules (or a single globals.css) for animations, rare third-party overrides, and print rules. On a recent marketing rebuild, that split kept Lighthouse CSS payload small while designers still got a module for the hero animation — details in &lt;a href="https://safdarali.in/blog/nextjs-performance-60-percent" rel="noopener noreferrer"&gt;performance case study&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="production"&gt;My production setup&lt;/h2&gt;
&lt;p&gt;Default stack: Next.js + TypeScript strict + Tailwind + &lt;code&gt;cn()&lt;/code&gt; helper for conditional classes. I add modules per feature folder when a component accrues more than ~15 lines of custom CSS. At my day job, we banned new global selectors except resets.&lt;/p&gt;
&lt;p&gt;Neither choice fixes bad component architecture — see &lt;a href="https://safdarali.in/blog/nextjs-project-structure-guide-2026" rel="noopener noreferrer"&gt;Next.js folder structure guide&lt;/a&gt; for how I colocate styles with features.&lt;/p&gt;
&lt;p&gt;I also run Prettier with the Tailwind class sorter plugin on teams that use utilities — consistent order makes long class strings diff cleanly. CSS Module projects get stylelint for nesting and variable naming instead.&lt;/p&gt;
&lt;h2 id="closing"&gt;The single takeaway&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Tailwind for speed; CSS Modules for complexity.&lt;/strong&gt; Pick per component, not per religion. Same card, two implementations — choose the file you will actually maintain six months from now.&lt;/p&gt;
&lt;p&gt;Interview tip I give juniors in India: neither answer is wrong in isolation. Ask what the team already uses, whether designers pair with you daily, and whether the page is marketing or a long-lived dashboard. Match the toolchain to the delivery cadence, not Twitter polls.&lt;/p&gt;
&lt;p&gt;Related: &lt;a href="https://safdarali.in/projects" rel="noopener noreferrer"&gt;Projects&lt;/a&gt;. &lt;a href="https://safdarali.in/contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If this helped you&lt;/p&gt;
&lt;p&gt;I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;span&gt;→ &lt;/span&gt;&lt;a href="https://buymeacoffee.com/safdarali" rel="noopener noreferrer"&gt;Buy me a coffee at buymeacoffee.com/safdarali&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;👉 &lt;/span&gt;&lt;a href="https://www.youtube.com/@safdarali_?sub_confirmation=1" rel="noopener noreferrer"&gt;Subscribe to my YouTube channel&lt;/a&gt;&lt;span&gt; — it's free; 70+ React &amp;amp; Next.js tutorials&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="related-posts-heading"&gt;Related reading&lt;/h2&gt;
&lt;p&gt;More guides on safdarali.in — same author, production-focused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/zustand-vs-redux-toolkit-2026" rel="noopener noreferrer"&gt; &lt;h3&gt;Zustand vs Redux Toolkit — Which State Manager in 2026?&lt;/h3&gt;
&lt;p&gt;Zustand vs Redux Toolkit 2026 — comparison table, side-by-side code, bundle size, and production recommendation.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Jun 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/free-video-thumbnail-generator-online-no-upload" rel="noopener noreferrer"&gt; &lt;h3&gt;Free Video Thumbnail Generator Online — No Upload&lt;/h3&gt;
&lt;p&gt;Free video thumbnail generator online — extract frames from video without upload. Browser-based, no watermark, YouTube thumbnail from MP4 workflow.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Jun 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tailwindcss</category>
      <category>css</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>React 19 Features — What Actually Changed and What I Use</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Sun, 05 Jul 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/safdarali25/react-19-features-what-actually-changed-and-what-i-use-m8l</link>
      <guid>https://dev.to/safdarali25/react-19-features-what-actually-changed-and-what-i-use-m8l</guid>
      <description>&lt;p&gt;React 19 shipped a laundry list of features. Twitter threads treated every hook like mandatory. In production on client sites and this portfolio, I adopted a subset — the ones that remove real bugs or UX jank — and ignored the rest until the ecosystem caught up. This is my honest &lt;strong&gt;react 19 features&lt;/strong&gt; guide: what changed, code you can paste, and what I am still waiting on.&lt;/p&gt;
&lt;h2 id="headline-changes"&gt;What actually changed at a high level&lt;/h2&gt;
&lt;p&gt;React 19 stabilised the Actions model (forms and mutations with pending state), added &lt;code&gt;useOptimistic&lt;/code&gt; for instant UI feedback, introduced the &lt;code&gt;use()&lt;/code&gt; hook for reading promises and context, improved hydration error messages, and made ref-as-prop cleaner. The compiler (React Forget) is separate — exciting, not required to upgrade.&lt;/p&gt;
&lt;p&gt;Upgrade path: Next.js 15 projects already pin compatible React versions. Read &lt;a href="https://safdarali.in/blog/rsc-vs-client-components" rel="noopener noreferrer"&gt;RSC vs client components&lt;/a&gt; before mixing Actions with Server Components — boundaries still matter.&lt;/p&gt;
&lt;h2 id="hooks-table"&gt;React 19 hooks and APIs — quick reference table&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Client / Server&lt;/th&gt;
&lt;th&gt;I use in prod?&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;useOptimistic&lt;/td&gt;
&lt;td&gt;Optimistic UI while mutation runs&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;use()&lt;/td&gt;
&lt;td&gt;Read promise or context&lt;/td&gt;
&lt;td&gt;Both (with Suspense)&lt;/td&gt;
&lt;td&gt;Yes (with RSC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useActionState&lt;/td&gt;
&lt;td&gt;Form action state&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useFormStatus&lt;/td&gt;
&lt;td&gt;Pending from parent form&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ref as prop&lt;/td&gt;
&lt;td&gt;No forwardRef boilerplate&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;Gradual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document metadata&lt;/td&gt;
&lt;td&gt;title, meta in components&lt;/td&gt;
&lt;td&gt;Client (limited)&lt;/td&gt;
&lt;td&gt;Prefer Next.js metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2 id="use-optimistic"&gt;useOptimistic — instant feedback without lying to the user forever&lt;/h2&gt;
&lt;p&gt;Cart quantity updates, like buttons, todo toggles — users expect instant UI. &lt;code&gt;useOptimistic&lt;/code&gt; shows the next state while the server catches up, then reconciles on success or rolls back on error.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;"use client";&lt;br&gt;
import { useOptimistic, useTransition } from "react";&lt;br&gt;
import { updateQuantity } from "./actions";

&lt;p&gt;type Item = { id: string; qty: number };&lt;/p&gt;

&lt;p&gt;export function CartLine({ item }: { item: Item }) {&lt;br&gt;
  const [optimisticQty, setOptimisticQty] = useOptimistic(item.qty);&lt;br&gt;
  const [isPending, startTransition] = useTransition();&lt;/p&gt;

&lt;p&gt;function changeQty(next: number) {&lt;br&gt;
    startTransition(async () =&amp;gt; {&lt;br&gt;
      setOptimisticQty(next);&lt;br&gt;
      await updateQuantity(item.id, next);&lt;br&gt;
    });&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;return (&lt;br&gt;
    &amp;lt;div&amp;gt;&lt;br&gt;
      &amp;lt;button onClick={() =&amp;gt; changeQty(optimisticQty + 1)} disabled={isPending}&amp;gt;&lt;br&gt;
        +&lt;br&gt;
      &amp;lt;/button&amp;gt;&lt;br&gt;
      &amp;lt;span&amp;gt;{optimisticQty}&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;/div&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — manual optimistic state with footguns&lt;br&gt;
const [qty, setQty] = useState(item.qty);&lt;br&gt;
const [pending, setPending] = useState(false);

&lt;p&gt;async function bump() {&lt;br&gt;
  const prev = qty;&lt;br&gt;
  setQty(qty + 1); // optimistic&lt;br&gt;
  setPending(true);&lt;br&gt;
  try {&lt;br&gt;
    await updateQuantity(item.id, qty + 1);&lt;br&gt;
  } catch {&lt;br&gt;
    setQty(prev); // easy to forget rollback paths&lt;br&gt;
  } finally {&lt;br&gt;
    setPending(false);&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// AFTER — useOptimistic + transition: rollback wired correctly&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="use-hook"&gt;use() — promises and context without useEffect hacks&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Server Component passes a promise to client child&lt;br&gt;
import { use } from "react";

&lt;p&gt;type Product = { id: string; name: string };&lt;/p&gt;

&lt;p&gt;function ProductList({ productsPromise }: { productsPromise: Promise&amp;lt;Product[]&amp;gt; }) {&lt;br&gt;
  const products = use(productsPromise); // suspends until resolved&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;ul&amp;gt;&lt;br&gt;
      {products.map((p) =&amp;gt; (&lt;br&gt;
        &amp;lt;li key={p.id}&amp;gt;{p.name}&amp;lt;/li&amp;gt;&lt;br&gt;
      ))}&lt;br&gt;
    &amp;lt;/ul&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// Parent (Server Component) creates the promise once&lt;br&gt;
export default function Page() {&lt;br&gt;
  const productsPromise = getProducts(); // do not await here&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading products…&amp;lt;/p&amp;gt;}&amp;gt;&lt;br&gt;
      &amp;lt;ProductList productsPromise={productsPromise} /&amp;gt;&lt;br&gt;
    &amp;lt;/Suspense&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In production I use &lt;code&gt;use()&lt;/code&gt; with Server Components streaming data — same mental model as async server fetch, less client &lt;code&gt;useEffect&lt;/code&gt; spaghetti.&lt;/p&gt;
&lt;h2 id="actions"&gt;Actions and forms — less boilerplate than manual fetch&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;"use client";&lt;br&gt;
import { useActionState } from "react";&lt;br&gt;
import { subscribe } from "./actions";

&lt;p&gt;export function NewsletterForm() {&lt;br&gt;
  const [state, formAction, pending] = useActionState(subscribe, { ok: false, message: "" });&lt;/p&gt;

&lt;p&gt;return (&lt;br&gt;
    &amp;lt;form action={formAction}&amp;gt;&lt;br&gt;
      &amp;lt;input name="email" type="email" required /&amp;gt;&lt;br&gt;
      &amp;lt;button disabled={pending}&amp;gt;{pending ? "Sending…" : "Subscribe"}&amp;lt;/button&amp;gt;&lt;br&gt;
      {state.message &amp;amp;&amp;amp; &amp;lt;p&amp;gt;{state.message}&amp;lt;/p&amp;gt;}&lt;br&gt;
    &amp;lt;/form&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pair with Next.js Server Actions for mutations without a separate API route file — still validate on the server, still treat client state as untrusted.&lt;/p&gt;
&lt;h2 id="adopted"&gt;What I immediately adopted&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;useOptimistic&lt;/strong&gt; on any user-facing mutation where latency is felt on Indian mobile networks. &lt;strong&gt;useActionState / useFormStatus&lt;/strong&gt; on marketing forms — fewer lines than custom pending flags. &lt;strong&gt;use()&lt;/strong&gt; with Suspense boundaries on catalog sections fed from server promises. &lt;strong&gt;Better hydration errors&lt;/strong&gt; — saved me an hour debugging a client-only chart imported into a Server Component (fixed by splitting the leaf).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ref as prop — dropped forwardRef on new components&lt;br&gt;
type ButtonProps = React.ComponentProps&amp;lt;"button"&amp;gt; &amp;amp; { ref?: React.Ref&amp;lt;HTMLButtonElement&amp;gt; };

&lt;p&gt;export function Button({ ref, ...props }: ButtonProps) {&lt;br&gt;
  return &amp;lt;button ref={ref} {...props} /&amp;gt;;&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="waiting"&gt;What I'm waiting on&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;React Compiler (Forget)&lt;/strong&gt; — I will enable it per-route after stable Next.js integration docs, not on day one of React 19. &lt;strong&gt;Document metadata in client trees&lt;/strong&gt; — I still use Next.js &lt;code&gt;generateMetadata&lt;/code&gt; for SEO. &lt;strong&gt;Full ecosystem typings&lt;/strong&gt; — some third-party libs lagged React 19 types for weeks; I pinned versions until they caught up.&lt;/p&gt;
&lt;p&gt;I am also not rewriting every &lt;code&gt;forwardRef&lt;/code&gt; component overnight — new code uses ref-as-prop; old code migrates on touch.&lt;/p&gt;
&lt;p&gt;Waiting is a strategy, not laziness. The compiler will change how much manual memo we write — see my &lt;a href="https://safdarali.in/blog/usecallback-vs-usememo-react-guide" rel="noopener noreferrer"&gt;useCallback vs useMemo guide&lt;/a&gt; for why I am not adding more memo hooks while the ecosystem catches up.&lt;/p&gt;
&lt;h2 id="production"&gt;My production setup&lt;/h2&gt;
&lt;p&gt;In production: React 19 + Next.js 15, Server Components by default, React 19 Actions on forms that need pending UX, optimistic updates on commerce interactions. Performance work still lives in caching and bundle size — see &lt;a href="https://safdarali.in/blog/nextjs-performance-60-percent" rel="noopener noreferrer"&gt;Next.js performance case study&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When experimenting, I use the workflow in &lt;a href="https://safdarali.in/blog/cursor-claude-react-workflow-2026" rel="noopener noreferrer"&gt;Cursor + Claude for React&lt;/a&gt; — AI suggests React 19 APIs quickly, but I verify against official release notes before merge.&lt;/p&gt;
&lt;h2 id="closing"&gt;The single takeaway&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;React 19 is not a rewrite mandate.&lt;/strong&gt; Adopt optimistic UI, Actions, and &lt;code&gt;use()&lt;/code&gt; where they solve problems you already have. Wait on compiler and metadata experiments until your stack documents them.&lt;/p&gt;
&lt;p&gt;Related: &lt;a href="https://safdarali.in/blog/nextjs-vs-react-which-to-learn-2026" rel="noopener noreferrer"&gt;Next.js vs React learning path&lt;/a&gt;. &lt;a href="https://safdarali.in/contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If this helped you&lt;/p&gt;
&lt;p&gt;I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;span&gt;→ &lt;/span&gt;&lt;a href="https://buymeacoffee.com/safdarali" rel="noopener noreferrer"&gt;Buy me a coffee at buymeacoffee.com/safdarali&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;👉 &lt;/span&gt;&lt;a href="https://www.youtube.com/@safdarali_?sub_confirmation=1" rel="noopener noreferrer"&gt;Subscribe to my YouTube channel&lt;/a&gt;&lt;span&gt; — it's free; 70+ React &amp;amp; Next.js tutorials&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="related-posts-heading"&gt;Related reading&lt;/h2&gt;
&lt;p&gt;More guides on safdarali.in — same author, production-focused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/frontend-developer-portfolio-guide-india-2026" rel="noopener noreferrer"&gt; &lt;h3&gt;How to Build a Frontend Developer Portfolio That Stands Out&lt;/h3&gt;
&lt;p&gt;Frontend developer portfolio guide for India — sections, React/Next.js examples, SEO, performance, personal branding, FAQ, and checklist to build and rank.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;May 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/rsc-vs-client-components" rel="noopener noreferrer"&gt; &lt;h3&gt;React Server Components vs Client Components — When to Use Which&lt;/h3&gt;
&lt;p&gt;Practical RSC vs client guide for Next.js App Router — when to use each, real code, bundle before/after, and performance impact.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;May 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>SSR vs SSG vs ISR in Next.js — Plain English Explanation</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Sun, 05 Jul 2026 15:42:29 +0000</pubDate>
      <link>https://dev.to/safdarali25/ssr-vs-ssg-vs-isr-in-nextjs-plain-english-explanation-12mo</link>
      <guid>https://dev.to/safdarali25/ssr-vs-ssg-vs-isr-in-nextjs-plain-english-explanation-12mo</guid>
      <description>&lt;p&gt;I'm &lt;a href="https://safdarali.in/about" rel="noopener noreferrer"&gt;Safdar Ali&lt;/a&gt;. For six months I treated every Next.js page like SSR because "server" sounded safest. Marketing pages were slower than they needed to be; dashboard pages were over-cached. &lt;strong&gt;SSR vs SSG in Next.js&lt;/strong&gt; is not a popularity contest — it is a delivery choice: when HTML is built, how often it refreshes, and who pays the compute bill. This article is the plain-English map I wish I had, with code for all three modes and a flowchart you can screenshot.&lt;/p&gt;
&lt;h2 id="definitions"&gt;Three acronyms, one question: when is HTML built?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;SSR (Server-Side Rendering)&lt;/strong&gt; builds HTML on each request (or per-request cache miss). Fresh data, higher server load.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SSG (Static Site Generation)&lt;/strong&gt; builds HTML at deploy time. Fastest CDN delivery; stale until you redeploy unless you add revalidation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ISR (Incremental Static Regeneration)&lt;/strong&gt; is SSG plus background refresh — static speed with a TTL. In App Router this is mostly &lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;next.revalidate&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Mental model — same page, three delivery timings

&lt;p&gt;// SSG:  HTML built at deploy ──────────────────► CDN serves file&lt;br&gt;
// ISR:  HTML built at deploy ──► stale ──► regen in background after N sec&lt;br&gt;
// SSR:  HTML built when user hits URL ─────────► server sends fresh HTML&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;None of these replace React. They describe when your React tree becomes HTML the browser can paint before hydration.&lt;/p&gt;
&lt;h2 id="flowchart"&gt;Decision flowchart — which mode to pick&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;                    START: New Next.js page&lt;br&gt;
                              |&lt;br&gt;
                              v&lt;br&gt;
                    Does Google need HTML&lt;br&gt;
                    with real content on&lt;br&gt;
                    first request?&lt;br&gt;
                         /        \&lt;br&gt;
                       NO          YES&lt;br&gt;
                       |            |&lt;br&gt;
                       v            v&lt;br&gt;
              Authenticated      Is data identical&lt;br&gt;
              dashboard / SPA?   for all users?&lt;br&gt;
                    |                 /      \&lt;br&gt;
                   YES               NO      YES&lt;br&gt;
                    |                |        |&lt;br&gt;
                    v                v        v&lt;br&gt;
            "use client" +       SSR or      Changes&lt;br&gt;
            client fetch         per-user    every hour+&lt;br&gt;
            (no SSR win)         data?          /    \&lt;br&gt;
                                /    \        NO    YES&lt;br&gt;
                              YES    NO        |      |&lt;br&gt;
                               |      |        v      v&lt;br&gt;
                               v      v       SSG    ISR&lt;br&gt;
                              SSR   ISR      (rare) (revalidate)&lt;br&gt;
                                    or SSG&lt;br&gt;
                                    + short&lt;br&gt;
                                    revalidate&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Print that flowchart. When you are unsure, default marketing to ISR with a sensible &lt;code&gt;revalidate&lt;/code&gt;, default logged-in dashboards to client rendering.&lt;/p&gt;
&lt;h2 id="ssg-code"&gt;SSG — build once, serve from the edge&lt;/h2&gt;
&lt;p&gt;Use SSG for pages where data changes only when you deploy — legal pages, about pages, rarely updated landing heroes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/about/page.tsx — static by default (no fetch cache opts)&lt;br&gt;
export default function AboutPage() {&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;main&amp;gt;&lt;br&gt;
      &amp;lt;h1&amp;gt;About Safdar Ali&amp;lt;/h1&amp;gt;&lt;br&gt;
      &amp;lt;p&amp;gt;Frontend engineer, Bengaluru — available for React / Next.js work.&amp;lt;/p&amp;gt;&lt;br&gt;
    &amp;lt;/main&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}

&lt;p&gt;// Dynamic routes can still be SSG if you export generateStaticParams&lt;br&gt;
export async function generateStaticParams() {&lt;br&gt;
  const slugs = await getAllBlogSlugs();&lt;br&gt;
  return slugs.map((slug) =&amp;gt; ({ slug }));&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tradeoff: a price change on an e-commerce SKU list will not appear until redeploy or ISR kicks in. For truly static content, that is a feature — zero origin load.&lt;/p&gt;
&lt;h2 id="isr-code"&gt;ISR — static speed, controlled freshness&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// app/products/page.tsx — ISR via fetch revalidate (App Router)&lt;br&gt;
type Product = { id: string; name: string; price: number };

&lt;p&gt;async function getProducts(): Promise&amp;lt;Product[]&amp;gt; {&lt;br&gt;
  const res = await fetch("&lt;a href="https://api.example.com/products" rel="noopener noreferrer"&gt;https://api.example.com/products&lt;/a&gt;", {&lt;br&gt;
    next: { revalidate: 3600 }, // ISR: refresh at most every hour&lt;br&gt;
  });&lt;br&gt;
  if (!res.ok) throw new Error("Failed to load products");&lt;br&gt;
  return res.json();&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;export default async function ProductsPage() {&lt;br&gt;
  const products = await getProducts();&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;ul&amp;gt;&lt;br&gt;
      {products.map((p) =&amp;gt; (&lt;br&gt;
        &amp;lt;li key={p.id}&amp;gt;{p.name} — ₹{p.price}&amp;lt;/li&amp;gt;&lt;br&gt;
      ))}&lt;br&gt;
    &amp;lt;/ul&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First visitor after TTL might still see stale HTML while regeneration runs — acceptable for catalog pages, unacceptable for live stock tickers. Know your freshness requirement before picking ISR.&lt;/p&gt;
&lt;h2 id="ssr-code"&gt;SSR — per-request HTML when data must be fresh&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// app/dashboard/summary/page.tsx — SSR: no cache on fetch&lt;br&gt;
async function getSummary(userId: string) {&lt;br&gt;
  const res = await fetch("&lt;a href="https://api.example.com/summary/" rel="noopener noreferrer"&gt;https://api.example.com/summary/&lt;/a&gt;" + userId, {&lt;br&gt;
    cache: "no-store", // SSR — always fresh for this user&lt;br&gt;
  });&lt;br&gt;
  return res.json();&lt;br&gt;
}

&lt;p&gt;export default async function SummaryPage() {&lt;br&gt;
  const session = await getSession();&lt;br&gt;
  const summary = await getSummary(session.userId);&lt;br&gt;
  return &amp;lt;SummaryClient data={summary} /&amp;gt;;&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SSR costs more at scale. Use it when personalisation or real-time correctness beats CDN economics — not because "server sounds professional."&lt;/p&gt;
&lt;h2 id="comparison"&gt;SSR vs SSG vs ISR — comparison table&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;SSG&lt;/th&gt;
&lt;th&gt;ISR&lt;/th&gt;
&lt;th&gt;SSR&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;When HTML is built&lt;/td&gt;
&lt;td&gt;Deploy time&lt;/td&gt;
&lt;td&gt;Deploy + periodic regen&lt;/td&gt;
&lt;td&gt;Each request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFB on CDN&lt;/td&gt;
&lt;td&gt;Fastest&lt;/td&gt;
&lt;td&gt;Fast (often cached)&lt;/td&gt;
&lt;td&gt;Slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data freshness&lt;/td&gt;
&lt;td&gt;Stale until deploy&lt;/td&gt;
&lt;td&gt;TTL-based&lt;/td&gt;
&lt;td&gt;Real-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server cost&lt;/td&gt;
&lt;td&gt;Lowest&lt;/td&gt;
&lt;td&gt;Low–medium&lt;/td&gt;
&lt;td&gt;Highest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-user data&lt;/td&gt;
&lt;td&gt;Poor fit&lt;/td&gt;
&lt;td&gt;Poor fit&lt;/td&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App Router signal&lt;/td&gt;
&lt;td&gt;Default static page&lt;/td&gt;
&lt;td&gt;revalidate: N&lt;/td&gt;
&lt;td&gt;cache: no-store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2 id="marketing-vs-dashboard"&gt;Marketing site vs dashboard — what I actually ship&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Marketing site&lt;/strong&gt; (public, SEO, shared content): ISR for blog and product listings, SSG for about/legal, Server Components for HTML. On a client rebuild I moved the blog to &lt;code&gt;revalidate: 86400&lt;/code&gt; and saw origin requests drop 70% — part of the story in &lt;a href="https://safdarali.in/blog/nextjs-performance-60-percent" rel="noopener noreferrer"&gt;my Next.js performance case study&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dashboard&lt;/strong&gt; (authenticated, personal): client components, SWR or WebSockets, no ISR fantasy. Trying to ISR user-specific charts is how teams waste a sprint.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — marketing page forced to SSR "just because"&lt;br&gt;
export default async function PricingPage() {&lt;br&gt;
  const plans = await fetch("&lt;a href="https://api.example.com/plans" rel="noopener noreferrer"&gt;https://api.example.com/plans&lt;/a&gt;", { cache: "no-store" });&lt;br&gt;
  // Every visitor hits origin — same JSON for everyone&lt;br&gt;
}

&lt;p&gt;// AFTER — ISR: shared plans, CDN-friendly&lt;br&gt;
export default async function PricingPage() {&lt;br&gt;
  const res = await fetch("&lt;a href="https://api.example.com/plans" rel="noopener noreferrer"&gt;https://api.example.com/plans&lt;/a&gt;", { next: { revalidate: 3600 } });&lt;br&gt;
  const plans = await res.json();&lt;br&gt;
  // ...&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="personal"&gt;My production setup&lt;/h2&gt;
&lt;p&gt;In production I label routes in file comments — &lt;code&gt;ISR 1h&lt;/code&gt; or &lt;code&gt;SSR session&lt;/code&gt; — so the next developer does not "optimise" a dashboard into ISR by accident. Pair this with &lt;a href="https://safdarali.in/blog/rsc-vs-client-components" rel="noopener noreferrer"&gt;RSC vs client components&lt;/a&gt; — rendering mode and component boundary are two separate decisions.&lt;/p&gt;
&lt;p&gt;At my day job, the mistake I see most is SSR everywhere because the team learned Pages Router &lt;code&gt;getServerSideProps&lt;/code&gt; first. App Router caching is more granular — use it.&lt;/p&gt;
&lt;h2 id="closing"&gt;The single takeaway&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Pick rendering by freshness and audience, not acronym prestige.&lt;/strong&gt; Marketing: ISR + SSG. Dashboards: client. Personalised SSR only when you mean it.&lt;/p&gt;
&lt;p&gt;Related: &lt;a href="https://safdarali.in/blog/nextjs-app-router-complete-guide-2026" rel="noopener noreferrer"&gt;App Router complete beginner guide&lt;/a&gt;. &lt;a href="https://safdarali.in/contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If this helped you&lt;/p&gt;
&lt;p&gt;I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;span&gt;→ &lt;/span&gt;&lt;a href="https://buymeacoffee.com/safdarali" rel="noopener noreferrer"&gt;Buy me a coffee at buymeacoffee.com/safdarali&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;👉 &lt;/span&gt;&lt;a href="https://www.youtube.com/@safdarali_?sub_confirmation=1" rel="noopener noreferrer"&gt;Subscribe to my YouTube channel&lt;/a&gt;&lt;span&gt; — it's free; 70+ React &amp;amp; Next.js tutorials&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="related-posts-heading"&gt;Related reading&lt;/h2&gt;
&lt;p&gt;More guides on safdarali.in — same author, production-focused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/frontend-developer-portfolio-guide-india-2026" rel="noopener noreferrer"&gt; &lt;h3&gt;How to Build a Frontend Developer Portfolio That Stands Out&lt;/h3&gt;
&lt;p&gt;Frontend developer portfolio guide for India — sections, React/Next.js examples, SEO, performance, personal branding, FAQ, and checklist to build and rank.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;May 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://safdarali.in/blog/rsc-vs-client-components" rel="noopener noreferrer"&gt; &lt;h3&gt;React Server Components vs Client Components — When to Use Which&lt;/h3&gt;
&lt;p&gt;Practical RSC vs client guide for Next.js App Router — when to use each, real code, bundle before/after, and performance impact.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;May 2026&lt;/span&gt;&lt;span&gt; · &lt;/span&gt;Read article →&lt;/p&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ssr</category>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
