<?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>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>
    <item>
      <title>TypeScript Strict Mode — Why I Use It in Every Project</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Sun, 05 Jul 2026 14:07:05 +0000</pubDate>
      <link>https://dev.to/safdarali25/typescript-strict-mode-why-i-use-it-in-every-project-457</link>
      <guid>https://dev.to/safdarali25/typescript-strict-mode-why-i-use-it-in-every-project-457</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. Three years ago I turned off strict mode on a client dashboard because the migration felt noisy. Two weeks later, a user with an empty profile name crashed the settings page in production — exactly the class of bug &lt;code&gt;strictNullChecks&lt;/code&gt; would have flagged at compile time. Since then, &lt;strong&gt;typescript strict mode&lt;/strong&gt; is non-negotiable on every React and Next.js repo I touch. This guide is the tsconfig I copy, the flags I care about, and the errors that actually save you hours.&lt;/p&gt;
&lt;h2 id="what-strict"&gt;What "strict mode" actually means in TypeScript&lt;/h2&gt;
&lt;p&gt;Strict mode is not one switch — it is a family of compiler checks bundled under &lt;code&gt;strict: true&lt;/code&gt; in &lt;code&gt;tsconfig.json&lt;/code&gt;. When enabled, TypeScript refuses code that relies on implicit any, unchecked null access, or loose function types.&lt;/p&gt;
&lt;p&gt;The tradeoff is upfront friction: your first week on an old JavaScript codebase will surface hundreds of errors. The payoff is fewer 2am Slack messages and safer refactors when you rename a prop across forty components.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// tsconfig.json — baseline I paste into new Next.js projects&lt;br&gt;
{&lt;br&gt;
  "compilerOptions": {&lt;br&gt;
    "target": "ES2017",&lt;br&gt;
    "lib": ["dom", "dom.iterable", "esnext"],&lt;br&gt;
    "allowJs": true,&lt;br&gt;
    "skipLibCheck": true,&lt;br&gt;
    "strict": true,&lt;br&gt;
    "noEmit": true,&lt;br&gt;
    "esModuleInterop": true,&lt;br&gt;
    "module": "esnext",&lt;br&gt;
    "moduleResolution": "bundler",&lt;br&gt;
    "resolveJsonModule": true,&lt;br&gt;
    "isolatedModules": true,&lt;br&gt;
    "jsx": "preserve",&lt;br&gt;
    "incremental": true,&lt;br&gt;
    "plugins": [{ "name": "next" }],&lt;br&gt;
    "paths": { "@/&lt;em&gt;": ["./&lt;/em&gt;"] }&lt;br&gt;
  },&lt;br&gt;
  "include": ["next-env.d.ts", "&lt;strong&gt;/*.ts", "&lt;/strong&gt;/&lt;em&gt;.tsx", ".next/types/&lt;/em&gt;&lt;em&gt;/&lt;/em&gt;.ts"],&lt;br&gt;
  "exclude": ["node_modules"]&lt;br&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;strict: true&lt;/code&gt; turns on &lt;code&gt;strictNullChecks&lt;/code&gt;, &lt;code&gt;noImplicitAny&lt;/code&gt;, and several related flags. You can enable them individually, but I have never found a good reason to run half-strict on a greenfield app.&lt;/p&gt;
&lt;h2 id="flags-table"&gt;Strict flags — what each one does&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;th&gt;Pain level&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;strictNullChecks&lt;/td&gt;
&lt;td&gt;null / undefined used as values&lt;/td&gt;
&lt;td&gt;High on legacy APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;noImplicitAny&lt;/td&gt;
&lt;td&gt;Parameters without types&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;strictFunctionTypes&lt;/td&gt;
&lt;td&gt;Unsafe callback assignments&lt;/td&gt;
&lt;td&gt;Low–medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;strictBindCallApply&lt;/td&gt;
&lt;td&gt;Wrong bind/call/apply args&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;noImplicitThis&lt;/td&gt;
&lt;td&gt;Ambiguous this in functions&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alwaysStrict&lt;/td&gt;
&lt;td&gt;Emits "use strict" per file&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useUnknownInCatchVariables&lt;/td&gt;
&lt;td&gt;catch (e) typed as any&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;If you are migrating an old repo, enable flags one at a time in CI — fix &lt;code&gt;noImplicitAny&lt;/code&gt; first, then &lt;code&gt;strictNullChecks&lt;/code&gt;. That order reduces the simultaneous error flood.&lt;/p&gt;
&lt;h2 id="three-errors"&gt;The 3 errors strict mode catches most often&lt;/h2&gt;
&lt;p&gt;After dozens of code reviews, these three patterns account for most of the bugs strict mode prevented before merge — not theoretical type pedantry, real user-facing failures.&lt;/p&gt;
&lt;h3&gt;1. Possibly undefined property access&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type User = { id: string; displayName?: string };

&lt;p&gt;function greet(user: User) {&lt;br&gt;
  // Error under strictNullChecks: 'displayName' is possibly 'undefined'&lt;br&gt;
  return "Hello, " + user.displayName.toUpperCase();&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// Fix — narrow before use&lt;br&gt;
function greetSafe(user: User) {&lt;br&gt;
  const name = user.displayName ?? "Guest";&lt;br&gt;
  return "Hello, " + name.toUpperCase();&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Implicit any on event handlers&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — noImplicitAny flags the parameter&lt;br&gt;
function handleChange(e) {&lt;br&gt;
  setQuery(e.target.value);&lt;br&gt;
}

&lt;p&gt;// AFTER — explicit React type&lt;br&gt;
import type { ChangeEvent } from "react";&lt;/p&gt;

&lt;p&gt;function handleChange(e: ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) {&lt;br&gt;
  setQuery(e.target.value);&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Wrong API response shape assumed&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type ApiOrder = { id: string; total: number; discount?: number };

&lt;p&gt;async function getOrder(id: string): Promise&amp;lt;ApiOrder&amp;gt; {&lt;br&gt;
  const res = await fetch("/api/orders/" + id);&lt;br&gt;
  return res.json();&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;function OrderSummary({ order }: { order: ApiOrder }) {&lt;br&gt;
  // strictNullChecks: discount may be undefined&lt;br&gt;
  const saved = order.discount ?? 0;&lt;br&gt;
  return &amp;lt;p&amp;gt;Total: ₹{order.total - saved}&amp;lt;/p&amp;gt;;&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Number three is why I type API responses at the boundary — not inside every child component. One wrong assumption on &lt;code&gt;fetch&lt;/code&gt; results propagates silently without strict types.&lt;/p&gt;
&lt;h2 id="bug-story"&gt;The production bug strict mode would have caught&lt;/h2&gt;
&lt;p&gt;At my day job, a checkout helper read &lt;code&gt;user.address.line2&lt;/code&gt; without checking if address existed. Indian users who signed up with phone-only onboarding had &lt;code&gt;address: null&lt;/code&gt;. The page white-screened on submit.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — passed review, failed in production&lt;br&gt;
type User = { id: string; address: { line1: string; line2?: string } | null };

&lt;p&gt;function formatShipping(user: User) {&lt;br&gt;
  return user.address.line1 + ", " + user.address.line2;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// AFTER — compiler forces the guard&lt;br&gt;
function formatShipping(user: User) {&lt;br&gt;
  if (!user.address) return "Address required";&lt;br&gt;
  const line2 = user.address.line2 ?? "";&lt;br&gt;
  return user.address.line1 + (line2 ? ", " + line2 : "");&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We added the guard in a hotfix. With strict null checks enabled from day one, the first version would never have compiled. That single incident paid for every strict migration I have done since.&lt;/p&gt;
&lt;h2 id="before-after"&gt;Migrating a component — before and after strict&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — loose props, any sneaks in&lt;br&gt;
export function ProductCard(props) {&lt;br&gt;
  const { title, price, onAdd } = props;&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;button onClick={() =&amp;gt; onAdd(title)}&amp;gt;&lt;br&gt;
      {title} — ₹{price}&lt;br&gt;
    &amp;lt;/button&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}

&lt;p&gt;// AFTER — explicit contract&lt;br&gt;
type ProductCardProps = {&lt;br&gt;
  title: string;&lt;br&gt;
  price: number;&lt;br&gt;
  onAdd: (title: string) =&amp;gt; void;&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;export function ProductCard({ title, price, onAdd }: ProductCardProps) {&lt;br&gt;
  return (&lt;br&gt;
    &amp;lt;button onClick={() =&amp;gt; onAdd(title)}&amp;gt;&lt;br&gt;
      {title} — ₹{price}&lt;br&gt;
    &amp;lt;/button&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The after version is longer by four lines. It is also grep-friendly: rename a prop and TypeScript lists every broken callsite instead of leaving undefined at runtime.&lt;/p&gt;
&lt;h2 id="with-nextjs"&gt;Strict mode with Next.js App Router&lt;/h2&gt;
&lt;p&gt;Next.js 15 projects ship with TypeScript by default. Server Components add one wrinkle: props and &lt;code&gt;params&lt;/code&gt; are often &lt;code&gt;Promise&lt;/code&gt;-wrapped. Strict typing there prevents awaiting the wrong shape.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/blog/[slug]/page.tsx&lt;br&gt;
type PageProps = { params: Promise&amp;lt;{ slug: string }&amp;gt; };

&lt;p&gt;export default async function BlogPostPage({ params }: PageProps) {&lt;br&gt;
  const { slug } = await params;&lt;br&gt;
  const post = await getPost(slug);&lt;br&gt;
  if (!post) return null;&lt;br&gt;
  return &amp;lt;Article post={post} /&amp;gt;;&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pair strict TypeScript with Server Components discipline from my &lt;a href="https://safdarali.in/blog/rsc-vs-client-components" rel="noopener noreferrer"&gt;RSC vs client components guide&lt;/a&gt; — types tell you what runs where; boundaries tell you what ships to the browser.&lt;/p&gt;
&lt;h2 id="when-relax"&gt;When I temporarily relax a rule (rarely)&lt;/h2&gt;
&lt;p&gt;Third-party libraries with broken types sometimes need &lt;code&gt;@ts-expect-error&lt;/code&gt; on one line — not &lt;code&gt;strict: false&lt;/code&gt; for the whole project. I also use &lt;code&gt;skipLibCheck: true&lt;/code&gt; so node_modules type noise does not block builds — that is standard, not cheating.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Scoped escape — document why&lt;br&gt;
// @ts-expect-error legacy chart lib ships wrong types until v4&lt;br&gt;
&amp;lt;LegacyChart data={metrics} /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What I do not do: disable strict for "speed." Typing saves more calendar time than it costs once the team is past the first migration week.&lt;/p&gt;
&lt;h2 id="production-setup"&gt;My production setup&lt;/h2&gt;
&lt;p&gt;In production I run &lt;code&gt;strict: true&lt;/code&gt;, ESLint &lt;code&gt;@typescript-eslint/recommended&lt;/code&gt;, and &lt;code&gt;tsc --noEmit&lt;/code&gt; in CI before every merge. On this portfolio and client marketing sites, that combo caught unused env vars and wrong metadata types before they hit Vercel.&lt;/p&gt;
&lt;p&gt;When I use AI-assisted refactors, I still read the diff — see my &lt;a href="https://safdarali.in/blog/cursor-claude-react-workflow-2026" rel="noopener noreferrer"&gt;Cursor + Claude workflow&lt;/a&gt; — but TypeScript strict mode is the safety net when autocomplete hallucinates a prop name.&lt;/p&gt;
&lt;p&gt;Junior developers in Bengaluru often ask if strict is "for senior devs." It is the opposite: strict mode teaches you the shape of data before runtime teaches you with user complaints.&lt;/p&gt;
&lt;h2 id="closing"&gt;The single takeaway&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;TypeScript strict mode is cheap insurance.&lt;/strong&gt; Turn it on in &lt;code&gt;tsconfig.json&lt;/code&gt;, fix errors in order, and let the compiler catch null access, implicit any, and API drift before your users do.&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 — what to learn first&lt;/a&gt;. Performance: &lt;a href="https://safdarali.in/blog/nextjs-performance-60-percent" rel="noopener noreferrer"&gt;How I cut load time by 60% with Next.js&lt;/a&gt;. Questions: &lt;a href="https://safdarali.in/contact" rel="noopener noreferrer"&gt;safdarali.in/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>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>Next.js vs React — Which Should You Learn First in 2026?</title>
      <dc:creator>Safdar Ali</dc:creator>
      <pubDate>Sun, 05 Jul 2026 13:44:22 +0000</pubDate>
      <link>https://dev.to/safdarali25/nextjs-vs-react-which-should-you-learn-first-in-2026-2g0m</link>
      <guid>https://dev.to/safdarali25/nextjs-vs-react-which-should-you-learn-first-in-2026-2g0m</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. Last month a junior dev on LinkedIn asked me the same question I heard in 2022, 2024, and again this week: "Should I learn React or jump straight to Next.js?" After four years shipping both — marketing sites, dashboards, and this portfolio on &lt;a href="https://safdarali.in" rel="noopener noreferrer"&gt;safdarali.in&lt;/a&gt; — my answer is not "always Next.js." It's learn React first, then Next.js fast. Here's why, with code you can paste today.&lt;/p&gt;
&lt;h2 id="not-the-same"&gt;React and Next.js are not the same thing — stop comparing them like they are&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;React&lt;/strong&gt; is a UI library. It handles components, state, and rendering. It does not ship routing, data fetching conventions, or a production server model out of the box.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next.js&lt;/strong&gt; is a React framework. It sits on top of React and adds file-based routing, server rendering, image optimisation, and deployment defaults. When people search "nextjs vs react 2026," they usually mean "plain React + Vite" vs "Next.js App Router."&lt;/p&gt;
&lt;p&gt;The tradeoff is clear: React alone gives you control. Next.js gives you speed to production — if you already understand what React is doing under the hood.&lt;/p&gt;
&lt;p&gt;Think of it like driving. React is learning how an engine, steering, and brakes work. Next.js is a car that already has those parts assembled — plus GPS, airbags, and a service schedule. You can build a car from parts (React + Vite + React Router + your own SSR layer). Most product teams in 2026 don't — they buy the assembled car and focus on where they're going.&lt;/p&gt;
&lt;p&gt;That analogy breaks if you don't know what an engine is. Jump into Next.js without JSX, props, or state fundamentals and every error message feels like framework magic instead of JavaScript.&lt;/p&gt;
&lt;h2 id="same-feature"&gt;Same feature, two stacks — a product list page side by side&lt;/h2&gt;
&lt;p&gt;Take a simple product list: fetch data, render cards, link to detail pages. Here's the split between a typical React + client fetch setup and Next.js App Router with a Server Component.&lt;/p&gt;
&lt;h3&gt;React (Vite + client fetch) — you wire everything&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/pages/ProductList.tsx — runs in the browser only&lt;br&gt;
import { useEffect, useState } from "react";

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

&lt;p&gt;export function ProductList() {&lt;br&gt;
  const [products, setProducts] = useState&amp;lt;Product[]&amp;gt;([]);&lt;br&gt;
  const [loading, setLoading] = useState(true);&lt;/p&gt;

&lt;p&gt;useEffect(() =&amp;gt; {&lt;br&gt;
    // SEO problem: crawlers see empty HTML until JS runs&lt;br&gt;
    fetch("/api/products")&lt;br&gt;
      .then((res) =&amp;gt; res.json())&lt;br&gt;
      .then(setProducts)&lt;br&gt;
      .finally(() =&amp;gt; setLoading(false));&lt;br&gt;
  }, []);&lt;/p&gt;

&lt;p&gt;if (loading) return &amp;lt;p&amp;gt;Loading…&amp;lt;/p&amp;gt;;&lt;/p&gt;

&lt;p&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;h3&gt;Next.js App Router — server fetch, less client JS&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// app/products/page.tsx — Server Component by default&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-style cache — fresh enough for catalog pages&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;/p&gt;

&lt;p&gt;// HTML arrives with content — better LCP, better SEO&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;Same UI. Different delivery model. On a client marketing site I migrated to App Router, that shift alone contributed to LCP dropping from 4.2s to 1.7s — I break down the full stack in my &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;With the React version, you also own routing. You'd add React Router, configure a layout wrapper, and probably a loading skeleton component. None of that is hard — it's just three extra files before you write business logic. Next.js collapses routing into folder names: &lt;code&gt;app/products/page.tsx&lt;/code&gt; is both the route and the page. After four years of shipping React, that convention still saves me time on every new project.&lt;/p&gt;
&lt;p&gt;The React approach wins when your API is a separate team's problem and you only care about the browser. Next.js wins when HTML on first request matters — SEO, social previews, slow 4G in tier-2 Indian cities.&lt;/p&gt;
&lt;h2 id="comparison-table"&gt;Next.js vs React — 10 criteria compared&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;React (Vite / CRA-style)&lt;/th&gt;
&lt;th&gt;Next.js (App Router)&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;UI library&lt;/td&gt;
&lt;td&gt;Full React framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routing&lt;/td&gt;
&lt;td&gt;You add React Router&lt;/td&gt;
&lt;td&gt;File-based, built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO / first paint&lt;/td&gt;
&lt;td&gt;CSR unless you add SSR&lt;/td&gt;
&lt;td&gt;SSR, SSG, ISR native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data fetching&lt;/td&gt;
&lt;td&gt;useEffect + fetch (client)&lt;/td&gt;
&lt;td&gt;async Server Components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Lower entry, higher assembly&lt;/td&gt;
&lt;td&gt;Steeper, fewer decisions later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle size control&lt;/td&gt;
&lt;td&gt;You own the whole graph&lt;/td&gt;
&lt;td&gt;RSC reduces client JS by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Static host + separate API&lt;/td&gt;
&lt;td&gt;Vercel/Node edge-ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for SPAs&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Works, often overkill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for marketing / content sites&lt;/td&gt;
&lt;td&gt;Extra work&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job market in India (2026)&lt;/td&gt;
&lt;td&gt;Still required everywhere&lt;/td&gt;
&lt;td&gt;Listed on most product roles&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Takeaway: React teaches you the language. Next.js teaches you how product teams ship that language at scale.&lt;/p&gt;
&lt;p&gt;Neither row is a knockout punch. The job market row matters in India — recruiters still filter on "React" in CVs, but product job descriptions increasingly say "Next.js" or "React with SSR experience." Learning both in sequence covers either filter without lying on your resume.&lt;/p&gt;
&lt;h2 id="when-react"&gt;When plain React still wins&lt;/h2&gt;
&lt;p&gt;I still reach for React without Next.js when the app is a true SPA — authenticated dashboards, internal tools, or embeddable widgets inside another product. No SEO requirement, no server HTML, no file-based routing benefit.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// When this is your whole app, Next.js adds ceremony you won't use:&lt;br&gt;
// - Single route shell&lt;br&gt;
// - Auth gate on the client&lt;br&gt;
// - WebSocket or polling for live data

&lt;p&gt;export function AnalyticsDashboard() {&lt;br&gt;
  // 100% client-side — Server Components buy you nothing here&lt;br&gt;
  return &amp;lt;LiveChart streamUrl="/ws/metrics" /&amp;gt;;&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What nobody tells you is that forcing App Router patterns onto a dashboard can slow you down — you fight the framework instead of shipping features.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — over-engineered dashboard page in Next.js&lt;br&gt;
export default async function DashboardPage() {&lt;br&gt;
  const session = await getServerSession(); // re-fetch every navigation&lt;br&gt;
  const metrics = await fetchMetrics(session.userId);&lt;br&gt;
  return &amp;lt;DashboardClient initial={metrics} /&amp;gt;; // still hydrates everything&lt;br&gt;
}

&lt;p&gt;// AFTER — honest SPA inside Next.js (or plain React if you prefer)&lt;br&gt;
"use client";&lt;br&gt;
export default function DashboardPage() {&lt;br&gt;
  const metrics = useLiveMetrics(); // WebSocket / SWR — belongs on client&lt;br&gt;
  return &amp;lt;DashboardClient metrics={metrics} /&amp;gt;;&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I've shipped internal analytics tools as Create React App SPAs that ran for years without a rewrite. The users were logged-in employees on fast office networks — SEO was irrelevant. Pick the stack that matches the delivery model, not the hype cycle.&lt;/p&gt;
&lt;h2 id="when-nextjs"&gt;When Next.js is the obvious pick in 2026&lt;/h2&gt;
&lt;p&gt;Public websites, blogs, e-commerce, landing pages, anything that needs Google to see real HTML on first request. Also any full-stack product where API routes and server actions live next to UI — one repo, one deploy.&lt;/p&gt;
&lt;p&gt;If you're building like &lt;a href="https://safdarali.in/projects" rel="noopener noreferrer"&gt;the client sites in my portfolio&lt;/a&gt;— marketing-first, performance-sensitive, mobile-heavy — Next.js is the default I recommend in 2026.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/products/[slug]/page.tsx — metadata + SSR in one file&lt;br&gt;
import type { Metadata } from "next";

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

&lt;p&gt;export async function generateMetadata({ params }: Props): Promise&amp;lt;Metadata&amp;gt; {&lt;br&gt;
  const { slug } = await params;&lt;br&gt;
  const product = await getProduct(slug);&lt;br&gt;
  return {&lt;br&gt;
    title: product.name + " — Shop",&lt;br&gt;
    description: product.summary, // Google + WhatsApp previews use this&lt;br&gt;
    openGraph: { images: [product.imageUrl] },&lt;br&gt;
  };&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;export default async function ProductPage({ params }: Props) {&lt;br&gt;
  const { slug } = await params;&lt;br&gt;
  const product = await getProduct(slug);&lt;br&gt;
  return &amp;lt;ProductDetail product={product} /&amp;gt;;&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Try doing that cleanly in a client-only React app. You can — with react-helmet, a meta tag library, and probably a pre-render step — but you're rebuilding what Next.js ships on day one. For public pages, that assembly tax adds up across every route.&lt;/p&gt;
&lt;h2 id="starting-today"&gt;What I'd choose if starting today&lt;/h2&gt;
&lt;p&gt;Week 1–4: React fundamentals — components, props, state, effects, lists, forms. Build small UI without a framework. I cover this path on my &lt;a href="https://www.youtube.com/@safdarali_?sub_confirmation=1" rel="noopener noreferrer"&gt;YouTube channel&lt;/a&gt; — 70+ free tutorials aimed at developers in India learning their first production stack.&lt;/p&gt;
&lt;p&gt;Week 5–8: TypeScript basics, then Next.js App Router — one page, one layout, one Server Component fetch. Read my &lt;a href="https://safdarali.in/blog/rsc-vs-client-components" rel="noopener noreferrer"&gt;RSC vs client components guide&lt;/a&gt; before you sprinkle "use client" everywhere.&lt;/p&gt;
&lt;p&gt;Month 3: Ship one real project — portfolio, blog, or small product — deploy to Vercel. Employers don't hire tutorial completers. They hire people who shipped.&lt;/p&gt;
&lt;p&gt;Free resources that worked for me: official React docs (beta.react.dev), Next.js learn course, and building in public on GitHub. Paid is optional until you need structured accountability — a ₹2,000 Udemy course is fine; a ₹2 lakh bootcamp is not required to land a first frontend role in India if your GitHub shows real work.&lt;/p&gt;
&lt;p&gt;If you only have time for one thing this month: learn React. If you have two: add Next.js immediately after — not instead.&lt;/p&gt;
&lt;h2 id="react-19-2026"&gt;Does React 19 change the nextjs vs react 2026 decision?&lt;/h2&gt;
&lt;p&gt;React 19 stabilised features that used to be Next.js-only experiments — improved hydration, the &lt;code&gt;use()&lt;/code&gt; hook, and better form actions. That does not make Next.js optional. It makes React itself more capable while Next.js still owns routing, caching, and deployment.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// React 19 — use() for promises inside components (still client or RSC)&lt;br&gt;
import { use } from "react";

&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can learn this API in a plain React sandbox. In production, I still reach for Next.js to wire the promise on the server and stream HTML. The library got sharper; the framework still saves calendar time.&lt;/p&gt;
&lt;p&gt;Takeaway: React 19 is a reason to learn modern React, not a reason to skip Next.js on public web apps.&lt;/p&gt;
&lt;h2 id="production-setup"&gt;My production setup&lt;/h2&gt;
&lt;p&gt;In production I default to Next.js App Router with TypeScript strict mode, Tailwind CSS, and Server Components for anything public-facing. Client components are leaves — charts, modals, forms — not entire pages.&lt;/p&gt;
&lt;p&gt;On a recent marketing rebuild, that discipline plus &lt;code&gt;next/image&lt;/code&gt; and route caching moved Lighthouse performance from 54 to 91. The bundle analyser showed 38% less client JavaScript after we moved data fetching to the server — same pattern I document in the performance case study linked above.&lt;/p&gt;
&lt;p&gt;Plain React stays in my toolbox for isolated widgets and legacy SPAs. Next.js is where I ship anything a user or crawler hits on the open web.&lt;/p&gt;
&lt;p&gt;When I interview junior candidates in Bengaluru, I ask them to explain one component they wrote — props in, state updates, why a list needs keys. I don't start with middleware or ISR. If they learned only Next.js templates, they often cannot answer. If they learned React first, the Next.js-specific questions become teachable in a week.&lt;/p&gt;
&lt;h2 id="mistake"&gt;The mistake I see most often&lt;/h2&gt;
&lt;p&gt;Beginners skip React and copy-paste Next.js templates. They can deploy, but they cannot debug hydration errors, cannot explain why a component is client-only, and panic when the build fails on server/client boundaries.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// BEFORE — copied from a template, broken mental model&lt;br&gt;
"use client";&lt;br&gt;
export default function Page() {&lt;br&gt;
  useEffect(() =&amp;gt; {&lt;br&gt;
    fetch("/api/data").then(/* ... */); // could be server-side&lt;br&gt;
  }, []);&lt;br&gt;
}

&lt;p&gt;// AFTER — fetch on server, interactivity only where needed&lt;br&gt;
export default async function Page() {&lt;br&gt;
  const data = await getData(); // runs once on server&lt;br&gt;
  return &amp;lt;ProductGrid data={data} /&amp;gt;;&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Learn React so the second snippet feels natural — not magic.&lt;/p&gt;
&lt;p&gt;Bootcamps that advertise "Full Stack Next.js in 4 weeks" without JSX fundamentals produce developers who can clone a Vercel deploy — until the first production bug. I'd rather you ship one ugly React todo app you fully understand than a polished template you cannot modify.&lt;/p&gt;
&lt;h2 id="closing"&gt;The single takeaway&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Next.js vs React in 2026 is the wrong either/or.&lt;/strong&gt; React is the skill. Next.js is how most product teams apply that skill on the web. Learn React first, ship one Next.js project second, and measure everything — LCP, bundle size, time-to-first-commit.&lt;/p&gt;
&lt;p&gt;I still get DMs asking which certificate to buy. Buy none until you've built something. Certificates prove attendance; GitHub repos prove competence. Start with React this week, add Next.js next month, and publish the result — even if it's rough.&lt;/p&gt;
&lt;p&gt;Related reading: &lt;a href="/blog/nextjs-performance-60-percent"&gt;How I cut load time by 60% with Next.js App Router&lt;/a&gt;. More case studies: &lt;a href="https://safdarali.in/projects" rel="noopener noreferrer"&gt;safdarali.in/projects&lt;/a&gt;. Questions: &lt;a href="https://safdarali.in/contact" rel="noopener noreferrer"&gt;safdarali.in/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; 

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