<?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: Likhit Kumar V P</title>
    <description>The latest articles on DEV Community by Likhit Kumar V P (@likhit).</description>
    <link>https://dev.to/likhit</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F799950%2Fc3fc85c6-ef32-4728-8c3e-7cd8881ae786.jpg</url>
      <title>DEV Community: Likhit Kumar V P</title>
      <link>https://dev.to/likhit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/likhit"/>
    <language>en</language>
    <item>
      <title>I Built an MCP Server That Lets AI Autonomously Debug Salesforce - Here's How</title>
      <dc:creator>Likhit Kumar V P</dc:creator>
      <pubDate>Sun, 05 Apr 2026 12:54:29 +0000</pubDate>
      <link>https://dev.to/likhit/i-built-an-mcp-server-that-lets-ai-autonomously-debug-salesforce-heres-how-382f</link>
      <guid>https://dev.to/likhit/i-built-an-mcp-server-that-lets-ai-autonomously-debug-salesforce-heres-how-382f</guid>
      <description>&lt;p&gt;I built &lt;code&gt;sf-log-mcp&lt;/code&gt;, an open-source MCP server that gives AI assistants (Claude, Copilot, Cursor) the ability to autonomously fetch, analyze, and manage Salesforce debug logs. It detects "silent failures" that Salesforce marks as "Success" but are actually broken. Published on npm, 9 tools, 7 parsers, 101 tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Likhit-Kumar/SF-Logs" rel="noopener noreferrer"&gt;github.com/Likhit-Kumar/SF-Logs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;npm:&lt;/strong&gt; &lt;code&gt;npx sf-log-mcp&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;If you've ever debugged a Salesforce integration, you know the drill:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Open Setup &amp;gt; Debug Logs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stare at a wall of logs showing &lt;strong&gt;Status = "Success"&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Manually download each &lt;code&gt;.log&lt;/code&gt; file&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ctrl+F through 50,000 lines looking for what went wrong&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Find out the "successful" callout actually returned &lt;code&gt;{"error":"rate_limit_exceeded"}&lt;/code&gt; inside an HTTP 200&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Status field lies.&lt;/strong&gt; In my experience, over 90% of real production issues are &lt;em&gt;silent failures&lt;/em&gt; the code didn't crash, Apex didn't throw an unhandled exception, but the &lt;em&gt;right thing didn't happen&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here's what "Success" actually hides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;HTTP 200 with &lt;code&gt;{"error":"rate_limit"}&lt;/code&gt; in body&lt;/strong&gt; - Integration silently failing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Exception caught by try-catch&lt;/strong&gt; - Error swallowed, moved on&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SOQL returned 0 rows&lt;/strong&gt; - Wrong filter, no data processed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Governor limits at 95%&lt;/strong&gt; - Works now, breaks at scale&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flow path skipped entirely&lt;/strong&gt; - Expected automation never fired&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now here's the kicker: &lt;strong&gt;no existing MCP server can even fetch these logs&lt;/strong&gt;, let alone analyze them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Gap in the Ecosystem
&lt;/h2&gt;

&lt;p&gt;I spent weeks researching the Salesforce MCP landscape. Here's what I found:&lt;/p&gt;

&lt;h3&gt;
  
  
  Certinia's &lt;code&gt;@certinia/apex-log-mcp&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A solid parser, great for performance profiling and bottleneck detection. But 3 out of 4 tools require a &lt;strong&gt;local &lt;code&gt;.log&lt;/code&gt; file path&lt;/strong&gt; as input. It cannot list, fetch, or download logs from an org. You still have to manually download them first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Salesforce's &lt;code&gt;@salesforce/mcp&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The official MCP server with 60+ tools across metadata, data, testing, DevOps, and code analysis. Impressive scope. But: &lt;strong&gt;zero debug log tools.&lt;/strong&gt; No &lt;code&gt;list_logs&lt;/code&gt;, no &lt;code&gt;fetch_log&lt;/code&gt;, no &lt;code&gt;manage_trace_flags&lt;/code&gt;. Nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Workflow Today (Even With MCP)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
You: "Something's wrong with our Vendor X integration"

AI: "I'd love to help! Can you download the debug log
and give me the file path?"

You: *opens Setup, clicks Debug Logs, downloads file,
saves to Desktop, types the path*

AI: "Thanks, here's the analysis..."

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI is supposed to be autonomous. But it can't even get the logs it needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;sf-log-mcp&lt;/code&gt;&lt;/strong&gt;, a standalone MCP server that fills the missing layer between your Salesforce org and your AI assistant.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Philosophy
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't filter. Fetch. Read. Reason.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of filtering logs by status (which catches &amp;lt;10% of real issues), sf-log-mcp downloads the raw log content and lets the AI reason about what &lt;em&gt;actually happened&lt;/em&gt; such as callout responses, DML results, swallowed exceptions, governor limits, flow paths.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Workflow Now
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;You:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Something's off with our Vendor X integration — check the recent logs"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;AI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;autonomously:&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;manage_trace_flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;checks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;logging&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;active&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fetch_latest_logs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;downloads&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;logs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;analyze_log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;health&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;score:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(CRITICAL)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get_log_content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;drills&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;into&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;callouts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;section&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get_log_content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;checks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;exceptions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;section&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;AI:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Found it. Log 07L... shows the callout to
api.vendorx.com/sync returned HTTP 200, but the
response body contains {"&lt;/span&gt;&lt;span class="err"&gt;error&lt;/span&gt;&lt;span class="s2"&gt;":"&lt;/span&gt;&lt;span class="err"&gt;rate_limit_exceeded&lt;/span&gt;&lt;span class="s2"&gt;"}.
This happened in 3 of the 5 recent logs. The
integration looks healthy from the Status field
but is actually being rate-limited."&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero manual steps. The AI fetches, reads, reasons, and explains.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
You (natural language)
│
v
AI Assistant (Claude / Copilot / Cursor)
│ MCP tool calls via stdio
v
sf-log-mcp (this project)
│ Salesforce Tooling API (REST)
v
Your Salesforce Org (auth via SF CLI)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Design Decisions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Direct Tooling API, not CLI subprocesses&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of shelling out to &lt;code&gt;sf apex list log&lt;/code&gt; (which spawns a subprocess, has CLI version dependencies, and limited filtering), I use &lt;code&gt;@salesforce/core&lt;/code&gt; to make direct REST calls to the Salesforce Tooling API. This gives fine-grained query control and eliminates subprocess overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Reuse SF CLI auth&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No new credentials, no OAuth setup, no tokens to configure. If &lt;code&gt;sf org list&lt;/code&gt; shows your org, sf-log-mcp can connect to it. It reads from &lt;code&gt;~/.sf/&lt;/code&gt; - the same auth your Salesforce CLI already uses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Standalone, not bundled&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;sf-log-mcp runs alongside Certinia's parser, not replacing it. You get the best of both: sf-log-mcp fetches and analyzes for silent failures, Certinia does deep performance profiling. The AI combines both results.&lt;/p&gt;




&lt;h2&gt;
  
  
  9 Tools, 4 Tiers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tier 1: Log Acquisition
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;list_debug_logs&lt;/code&gt;&lt;/strong&gt; - List logs with rich filtering (user, operation, date range, size)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;fetch_debug_log&lt;/code&gt;&lt;/strong&gt; - Download a specific log by ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;fetch_latest_logs&lt;/code&gt;&lt;/strong&gt; - Batch-download the N most recent logs&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tier 2: Content Intelligence
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;get_log_content&lt;/code&gt;&lt;/strong&gt; - Extract structured sections (callouts, exceptions, SOQL, DML, governor limits, flows, debug messages)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;analyze_log&lt;/code&gt;&lt;/strong&gt; - One-call health analysis with a 0-100 score&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;search_logs&lt;/code&gt;&lt;/strong&gt; - Regex search across all downloaded logs&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tier 3: Lifecycle Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;manage_trace_flags&lt;/code&gt;&lt;/strong&gt; - Create, list, update, delete trace flags&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;delete_debug_logs&lt;/code&gt;&lt;/strong&gt; - Delete logs (with dry-run mode)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tier 4: Cross-Log Intelligence
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;compare_logs&lt;/code&gt;&lt;/strong&gt; - Side-by-side diff of two logs for regression detection&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Health Score: Diagnosing Logs in One Call
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;analyze_log&lt;/code&gt; tool is the entry point for debugging. It returns a health score from 0-100:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Health Score: 65/100 — DEGRADED

Critical Issues:

- Silent callout failure: HTTP 200 with error in body (api.vendorx.com)

Warnings:

- 2 handled exceptions (verify error handling is correct)

- Governor limit: SOQL queries at 82% (approaching limit)

- Zero-row SOQL: SELECT Id FROM Account WHERE ExternalId__c = '...'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it's calculated:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="nx"&gt;healthScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;

&lt;span class="nx"&gt;healthScore&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;critical&lt;/span&gt; &lt;span class="nx"&gt;issues&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;healthScore&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;warnings&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Health Ratings:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;HEALTHY (90-100)&lt;/strong&gt; - No significant issues&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WARNING (70-89)&lt;/strong&gt; - Minor concerns worth checking&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DEGRADED (50-69)&lt;/strong&gt; - Multiple issues, needs attention&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CRITICAL (0-49)&lt;/strong&gt; - Serious failures detected&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI uses this score to decide what to drill into next callouts? Exceptions? Governor limits? It's the triage step that makes the whole workflow efficient.&lt;/p&gt;




&lt;h2&gt;
  
  
  Detecting Silent Failures: The 7 Parsers
&lt;/h2&gt;

&lt;p&gt;Each parser is purpose-built to extract and warn about a specific class of silent failure:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Callout Parser - The HTTP 200 Lie Detector
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;
&lt;span class="n"&gt;CALLOUT_REQUEST&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;HttpCallout&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nl"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//api.vendor.com/sync]&lt;/span&gt;

&lt;span class="n"&gt;CALLOUT_RESPONSE&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;HttpCallout&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"rate_limit_exceeded"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most monitoring checks the HTTP status code. 200 = good, right? &lt;strong&gt;Wrong.&lt;/strong&gt; The callout parser pairs every request with its response and scans the body for error keywords. This catches the most common class of integration failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Exception Parser - Handled vs. Unhandled
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;
&lt;span class="n"&gt;EXCEPTION_THROWN&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nl"&gt;NullPointerException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Attempt&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;reference&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="n"&gt;object&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Salesforce only flags &lt;em&gt;unhandled&lt;/em&gt; exceptions in the Status field. But most production code wraps everything in try-catch. The exception parser uses a &lt;strong&gt;10-line lookahead&lt;/strong&gt; : if &lt;code&gt;EXCEPTION_THROWN&lt;/code&gt; is followed by &lt;code&gt;FATAL_ERROR&lt;/code&gt;, it's unhandled. If followed by &lt;code&gt;METHOD_EXIT&lt;/code&gt;, it was caught. Both are reported, because a caught &lt;code&gt;NullPointerException&lt;/code&gt; is still a bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. SOQL Parser - The Zero-Row Detector
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="n"&gt;SOQL_EXECUTE_BEGIN&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ExternalId__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'VND-001'&lt;/span&gt;

&lt;span class="n"&gt;SOQL_EXECUTE_END&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="k"&gt;Rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A query that returns 0 rows isn't an error. But if your integration expects to find a matching record and doesn't, the entire downstream process silently does nothing. The SOQL parser flags zero-row results as data issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Governor Limits Parser - The Time Bomb Detector
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Number of SOQL queries: 82 out of 100 (82%) → WARNING

Number of DML rows: 9,800 out of 10,000 (98%) → CRITICAL

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 95% of governor limits, everything works. At 101%, everything breaks. The governor parser calculates percentages and flags anything over 80% as a warning.&lt;/p&gt;

&lt;h3&gt;
  
  
  5-7. DML, Flow, and Debug Message Parsers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DML Parser&lt;/strong&gt;: Flags bulk operations (&amp;gt;200 rows) that might cause partial failures&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flow Parser&lt;/strong&gt;: Tracks 16 flow event types, flags &lt;code&gt;FLOW_ELEMENT_ERROR&lt;/code&gt; and &lt;code&gt;FLOW_ELEMENT_FAULT&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debug Messages&lt;/strong&gt;: Extracts &lt;code&gt;System.debug()&lt;/code&gt; output where developers log errors the system doesn't track&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Smart Error Handling
&lt;/h2&gt;

&lt;p&gt;Salesforce API errors are notoriously cryptic. sf-log-mcp classifies them into 9 categories with actionable messages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Session expired&lt;/strong&gt; → "Re-authenticate with: &lt;code&gt;sf org login web --alias &amp;lt;org&amp;gt;&lt;/code&gt;"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;API limit exceeded&lt;/strong&gt; → "Wait and retry, or check API usage in Setup"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Insufficient permissions&lt;/strong&gt; → "User needs View All Data or Manage Users"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Entity already traced&lt;/strong&gt; → "Use manage_trace_flags to find the existing flag"&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No more googling Salesforce error codes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security Model
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No credentials stored&lt;/strong&gt; - Reuses SF CLI auth from &lt;code&gt;~/.sf/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Org allowlist&lt;/strong&gt; - &lt;code&gt;--allowed-orgs&lt;/code&gt; restricts which orgs the server can access&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stdio transport&lt;/strong&gt; - No HTTP server, no open ports&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SOQL injection protection&lt;/strong&gt; - All user inputs are escaped&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read-only by default&lt;/strong&gt; - Only &lt;code&gt;delete_debug_logs&lt;/code&gt; and &lt;code&gt;manage_trace_flags&lt;/code&gt; modify state (and only debug infrastructure, not business data)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;9&lt;/strong&gt; MCP Tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7&lt;/strong&gt; Log Parsers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2,488&lt;/strong&gt; Source Lines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1,069&lt;/strong&gt; Test Lines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;101&lt;/strong&gt; Tests Passing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15&lt;/strong&gt; Test Suites&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3&lt;/strong&gt; Production Dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;44.8 KB&lt;/strong&gt; npm Package Size&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3&lt;/strong&gt; Node.js Versions Supported (18, 20, 22)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It in 2 Minutes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Node.js &amp;gt;= 18&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Salesforce CLI (&lt;code&gt;sf&lt;/code&gt;) authenticated to an org&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
npx sf-log-mcp &lt;span class="nt"&gt;--allowed-orgs&lt;/span&gt; ALLOW_ALL_ORGS

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure Your AI Client
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Claude Desktop:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="nl"&gt;"sf-log-mcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

            &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sf-log-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--allowed-orgs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ALLOW_ALL_ORGS"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;VS Code / Cursor:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="nl"&gt;"sf-log-mcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

            &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sf-log-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--allowed-orgs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ALLOW_ALL_ORGS"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask your AI: &lt;em&gt;"List my recent Salesforce debug logs"&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-Server Setup
&lt;/h2&gt;

&lt;p&gt;sf-log-mcp is designed to complement, not replace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
AI Client (Claude Desktop / VS Code / Cursor)
│
├── sf-log-mcp (this project)
│ Fetch, analyze, search debug logs
│ Detect silent failures
│
├── @certinia/apex-log-mcp (optional)
│ Deep performance profiling
│ CPU bottleneck detection
│
└── @salesforce/mcp (optional)

SOQL queries, metadata, test runs

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;sf-log-mcp fetches the log and saves it to disk. Certinia's tools read the same file for performance analysis. The AI combines both results -&amp;gt; silent failure detection + performance profiling in one conversation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned Building This
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Status Field is a Lie
&lt;/h3&gt;

&lt;p&gt;This was the core insight that shaped the entire architecture. Filtering by &lt;code&gt;Status = 'Fatal Error'&lt;/code&gt; catches maybe 5-10% of real issues. The rest are silent : HTTP 200s with error bodies, caught exceptions, empty query results, skipped flow paths. The only way to find them is to read the actual log content.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. MCP is the Right Abstraction
&lt;/h3&gt;

&lt;p&gt;Before MCP, I would have built a CLI tool or a VS Code extension. MCP means I build once and it works everywhere (Claude Desktop, VS Code, Cursor, Windsurf, any future client). The AI decides when and how to use the tools. I just expose the capabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Parsers Need to Be Opinionated
&lt;/h3&gt;

&lt;p&gt;A generic parser that returns "here are all the events" is useless to an AI. The parsers need to &lt;em&gt;warn&lt;/em&gt; - "this callout returned 200 but the body contains an error keyword." That opinion is what makes the AI's analysis actionable.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Health Scores Drive Efficient Debugging
&lt;/h3&gt;

&lt;p&gt;Without the health score, the AI would analyze every section of every log. With it, the AI triages first: "This log is CRITICAL, let me check callouts and exceptions." It cuts the number of tool calls in half.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Three Dependencies is Enough
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt; for MCP, &lt;code&gt;@salesforce/core&lt;/code&gt; for auth + API, &lt;code&gt;zod&lt;/code&gt; for validation. That's it. No express, no axios, no lodash. The entire package is 44.8 KB.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windsurf testing&lt;/strong&gt; - Verifying compatibility with the Windsurf AI IDE&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time log tailing&lt;/strong&gt; - Stream logs as they're generated (SSE transport)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom analysis rules&lt;/strong&gt; - User-defined patterns for domain-specific silent failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certinia integration guide&lt;/strong&gt; - Step-by-step workflow combining both servers&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Likhit-Kumar/SF-Logs" rel="noopener noreferrer"&gt;github.com/Likhit-Kumar/SF-Logs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/sf-log-mcp" rel="noopener noreferrer"&gt;npmjs.com/package/sf-log-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install:&lt;/strong&gt; &lt;code&gt;npx sf-log-mcp --allowed-orgs ALLOW_ALL_ORGS&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you debug Salesforce integrations, give it a try. If you've been burned by "Status = Success" hiding real failures, you know why this exists.&lt;/p&gt;

&lt;p&gt;Star the repo if it helps. PRs welcome.&lt;/p&gt;




&lt;h3&gt;
  
  
  Tags
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;#salesforce&lt;/code&gt; &lt;code&gt;#mcp&lt;/code&gt; &lt;code&gt;#ai&lt;/code&gt; &lt;code&gt;#debugging&lt;/code&gt; &lt;code&gt;#typescript&lt;/code&gt; &lt;code&gt;#opensource&lt;/code&gt; &lt;code&gt;#claude&lt;/code&gt; &lt;code&gt;#devtools&lt;/code&gt; &lt;code&gt;#modelcontextprotocol&lt;/code&gt; &lt;code&gt;#apexlogs&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Agentforce Scripts: Hybrid Reasoning, Action Chaining, and What It Actually Looks Like in Practice</title>
      <dc:creator>Likhit Kumar V P</dc:creator>
      <pubDate>Sat, 28 Mar 2026 13:37:38 +0000</pubDate>
      <link>https://dev.to/likhit/agentforce-scripts-hybrid-reasoning-action-chaining-and-what-it-actually-looks-like-in-practice-32jg</link>
      <guid>https://dev.to/likhit/agentforce-scripts-hybrid-reasoning-action-chaining-and-what-it-actually-looks-like-in-practice-32jg</guid>
      <description>&lt;p&gt;&lt;strong&gt;Salesforce gave agents a scripting language. I tested it by building something that matters.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;If you've been following the Salesforce ecosystem lately, you've probably heard the buzz around &lt;strong&gt;Agentforce&lt;/strong&gt; (Salesforce's AI agent platform). But buried inside that buzz is something genuinely powerful that doesn't get talked about enough: &lt;strong&gt;Agent Script&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Agent Script is a high-level, declarative scripting language designed specifically for controlling Agentforce agents. It lets you combine &lt;strong&gt;deterministic business logic&lt;/strong&gt; (IF conditions, guaranteed action sequences, hard rules) with &lt;strong&gt;generative AI reasoning&lt;/strong&gt; (LLM-powered natural language, contextual responses, empathetic communication).&lt;/p&gt;

&lt;p&gt;Salesforce calls this &lt;strong&gt;hybrid reasoning&lt;/strong&gt;. And after spending time building with it, I think it's the most important feature Agentforce has shipped.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk through the key Agentforce features what they do, how they work together and show how I put them to the test by building an Employee Burnout Prevention Agent for the Agent Script Quest.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Agent Script and Why Should You Care?
&lt;/h2&gt;

&lt;p&gt;Before Agent Script, building Agentforce agents meant configuring topics and actions, then hoping the LLM would reason through them correctly. The LLM was both the brain and the decision-maker. That works for simple use cases, but the moment you need &lt;strong&gt;guaranteed behaviour&lt;/strong&gt; "if this condition is true, you MUST do this, no exceptions" you were stuck. LLMs are probabilistic. Business rules are not.&lt;/p&gt;

&lt;p&gt;Agent Script changes this entirely.&lt;/p&gt;

&lt;p&gt;It's a compiled language that generates an &lt;strong&gt;Agent Graph&lt;/strong&gt; specification consumed by the &lt;strong&gt;Atlas Reasoning Engine&lt;/strong&gt;. You write instructions that blend two layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deterministic layer&lt;/strong&gt; — IF/ELSE conditions, action calls, variable assignments. These execute exactly as written. The LLM cannot override them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generative layer&lt;/strong&gt; — Prompts that let the LLM generate natural language responses, adapt tone, personalise messages. The LLM gets creative freedom here.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: agents that are &lt;strong&gt;predictable where it matters&lt;/strong&gt; and &lt;strong&gt;human where it counts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This isn't a theoretical concept. The &lt;a href="https://developer.salesforce.com/sample-apps/agent-script-recipes" rel="noopener noreferrer"&gt;Agent Script Recipes library&lt;/a&gt; on Salesforce Developers provides working patterns for multi-action chaining, conditional routing, variable passing, and more. These recipes are what inspired me to go further and apply them to a real-world use case.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Agentforce Features — Explained
&lt;/h2&gt;

&lt;p&gt;Let me break down each feature I explored and what it brings to the table.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Agentforce Builder — Canvas View
&lt;/h3&gt;

&lt;p&gt;When you open an agent in Agentforce Builder, you land in &lt;strong&gt;Canvas view&lt;/strong&gt;. This is the visual layer, it summarises your Agent Script into easily understandable blocks that you can expand to see the underlying script.&lt;/p&gt;

&lt;p&gt;What makes Canvas view useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You get a bird's-eye view of your agent's structure — topics, actions, and how they connect&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;/&lt;/code&gt; to quickly add expressions for common patterns (if-else conditionals, loops)&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;@&lt;/code&gt; to add resources like topics, actions, and variables&lt;/li&gt;
&lt;li&gt;Non-technical stakeholders can read and review agent logic without touching code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Canvas view is the &lt;strong&gt;communication layer&lt;/strong&gt;. It's how you show your team what the agent does without drowning them in script syntax.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Agentforce Builder — Script View
&lt;/h3&gt;

&lt;p&gt;Switch to &lt;strong&gt;Script view&lt;/strong&gt;, and you're looking at what's happening under the hood — the actual Agent Script instructions that drive the agent's reasoning.&lt;/p&gt;

&lt;p&gt;Script view is a code editor interface where you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write Agent Script directly with full syntax support&lt;/li&gt;
&lt;li&gt;Make fast, precise changes&lt;/li&gt;
&lt;li&gt;Dig into error messages when things break&lt;/li&gt;
&lt;li&gt;See the deterministic and generative layers side by side&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The critical thing: &lt;strong&gt;Canvas view and Script view are always in sync&lt;/strong&gt;. Edit in one, and it reflects in the other. This means you can prototype visually in Canvas and fine-tune in Script — or vice versa.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Agent Script — Hybrid Reasoning
&lt;/h3&gt;

&lt;p&gt;This is the core. Agent Script enables what Salesforce officially calls &lt;strong&gt;hybrid reasoning&lt;/strong&gt; — the configurable Atlas Reasoning Engine balancing LLM creativity with structured business logic.&lt;/p&gt;

&lt;p&gt;Here's what this looks like conceptually:&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="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;DETERMINISTIC&lt;/span&gt; &lt;span class="n"&gt;LAYER&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;Hard&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Non&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;negotiable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;riskLevel&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Red&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;Schedule_Manager_Meeting&lt;/span&gt;
&lt;span class="n"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;Create_HR_Alert&lt;/span&gt;

&lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;riskLevel&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Amber&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="n"&gt;No&lt;/span&gt; &lt;span class="n"&gt;escalation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;riskLevel&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Green&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="n"&gt;No&lt;/span&gt; &lt;span class="n"&gt;escalation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Just&lt;/span&gt; &lt;span class="n"&gt;acknowledgement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;GENERATIVE&lt;/span&gt; &lt;span class="n"&gt;LAYER&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;LLM&lt;/span&gt; &lt;span class="n"&gt;takes&lt;/span&gt; &lt;span class="n"&gt;over&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;GENERATE&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;personalised&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;empathetic&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;

&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Use&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s first name
- Reference their situation without exposing internal scores
- Speak like a caring colleague, not a system notification
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The IF conditions are &lt;strong&gt;business rule guarantees&lt;/strong&gt;. The LLM cannot skip them, reinterpret them, or override them. They fire deterministically, every single time.&lt;/p&gt;

&lt;p&gt;The message generation below is pure LLM. It gets creative freedom to write something warm, contextual, and human.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hard rules in code. Warmth in language.&lt;/strong&gt; That's hybrid reasoning.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Action Chaining
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developer.salesforce.com/docs/ai/agentforce/guide/ascript-patterns-action-chaining.html" rel="noopener noreferrer"&gt;Action chaining&lt;/a&gt; is one of the most practical patterns from the Agent Script documentation. It enables multiple actions in a &lt;strong&gt;guaranteed sequence&lt;/strong&gt; — reliable multi-step workflows without relying on the LLM to remember multiple steps.&lt;/p&gt;

&lt;p&gt;There are several ways to chain actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sequential instructions&lt;/strong&gt; — Actions run deterministically before LLM reasoning occurs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variable passing&lt;/strong&gt; — Store the output of one action in a variable, use it as input to the next action&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning actions&lt;/strong&gt; — Define a follow-up action that automatically fires when the LLM calls a primary action&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional chaining&lt;/strong&gt; — Chain actions based on the results of previous actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transitions with actions&lt;/strong&gt; — Run an action and then automatically transition to another topic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key benefit: the &lt;strong&gt;sequence is guaranteed&lt;/strong&gt;. Action A always runs before Action B. Action B only fires if Action A's output meets a condition. No ambiguity.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Conditional Routing
&lt;/h3&gt;

&lt;p&gt;Conditional routing controls when actions fire and when topics transition, based on state and variables. In Agent Script, you write IF conditions that evaluate action outputs and route the agent's behaviour accordingly.&lt;/p&gt;

&lt;p&gt;This isn't just "if X then do Y." It's full control over the entire agent response:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which actions fire&lt;/li&gt;
&lt;li&gt;Which actions are blocked&lt;/li&gt;
&lt;li&gt;What tone the LLM takes in its response&lt;/li&gt;
&lt;li&gt;Whether the agent escalates, stays quiet, or celebrates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three different inputs can produce three completely different agent behaviours — all governed by the same script.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Apex Invocable Actions
&lt;/h3&gt;

&lt;p&gt;Agent Script doesn't do computation itself — it orchestrates. The actual logic lives in &lt;strong&gt;Apex Invocable classes&lt;/strong&gt; that the agent calls as actions.&lt;/p&gt;

&lt;p&gt;This is a clean separation of concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apex&lt;/strong&gt; handles the math, the data operations, the record creation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent Script&lt;/strong&gt; handles the decision-making, sequencing, and routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The LLM&lt;/strong&gt; handles the human communication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each Apex action is an &lt;code&gt;@InvocableMethod&lt;/code&gt; class that takes typed inputs and returns typed outputs. Agent Script stores those outputs in variables and uses them to drive conditional logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Preview Panel
&lt;/h3&gt;

&lt;p&gt;Inside Agentforce Builder, there's a &lt;strong&gt;Conversation Preview&lt;/strong&gt; panel where you can test your agent in real-time without deploying anything. Type a message, and the agent processes it live — firing actions, evaluating conditions, generating responses.&lt;/p&gt;

&lt;p&gt;This is the fastest feedback loop for agent development. Write script, test immediately, iterate.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Trace Panel
&lt;/h3&gt;

&lt;p&gt;This is arguably the most underrated feature in the entire builder.&lt;/p&gt;

&lt;p&gt;Click the &lt;strong&gt;Trace&lt;/strong&gt; link under any agent response, and you get a step-by-step breakdown of every reasoning step the agent took:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 1 — Topic classified as "Assess Wellbeing"

Step 2 — Calculate Burnout Score action called → returned "Red"

Step 3 — Schedule Manager Meeting action triggered

Step 4 — Create HR Alert action triggered

Step 5 — LLM generated personalised message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every action call. Every condition evaluation. Every LLM generation step. Full transparency.&lt;/p&gt;

&lt;p&gt;In enterprise contexts — especially anything touching HR, compliance, or finance — being able to prove &lt;strong&gt;why&lt;/strong&gt; the agent did what it did isn't optional. The Trace panel gives you that audit trail out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. The Atlas Reasoning Engine
&lt;/h3&gt;

&lt;p&gt;Under the hood, Agent Script compiles into an &lt;strong&gt;Agent Graph&lt;/strong&gt; consumed by the Atlas Reasoning Engine. This engine is now &lt;strong&gt;configurable&lt;/strong&gt; — you can balance the creativity of LLMs with the certainty of structured business logic.&lt;/p&gt;

&lt;p&gt;The Atlas Reasoning Engine handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reflective loops&lt;/li&gt;
&lt;li&gt;Multi-topic coordination&lt;/li&gt;
&lt;li&gt;Multi-agent coordination&lt;/li&gt;
&lt;li&gt;Action sequencing exactly as defined in your script&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't prompt-and-pray. It's a structured execution environment where deterministic and generative reasoning coexist by design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It All Together: The Burnout Prevention Agent
&lt;/h2&gt;

&lt;p&gt;To test these features end-to-end, I built an &lt;strong&gt;Employee Burnout Prevention Agent&lt;/strong&gt; for the Salesforce Agent Script Quest. The idea: an autonomous AI agent that monitors employee work patterns, detects burnout risk, and intervenes with the right action — without any human prompt.&lt;/p&gt;

&lt;p&gt;I was directly inspired by the multi-action chaining and conditional routing patterns from the &lt;a href="https://developer.salesforce.com/sample-apps/agent-script-recipes" rel="noopener noreferrer"&gt;Agent Script Recipes library&lt;/a&gt; — and pushed them into a real-world HR use case.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Data Model
&lt;/h3&gt;

&lt;p&gt;Three custom objects:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EmployeeActivity__c&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stores work pattern data — weekly hours, overdue tasks, weekend logins, after-hours emails, missed breaks. This is the input layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WellbeingScore__c&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agent's calculated output — burnout score and risk level (Red / Amber / Green). Internal only. Employees never see this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HR_Alert__c&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Confidential record created only for Red-risk employees. The employee never sees this. HR gets visibility without creating stigma.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Apex Actions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Calculate Burnout Score&lt;/strong&gt; — Weighted algorithm scoring 5 signals (login hours, overdue tasks, weekend logins, after-hours emails, missed breaks) and outputs a risk level. &lt;em&gt;Fires always — first in the chain.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schedule Manager Meeting&lt;/strong&gt; — Creates a confidential calendar event and a high-priority task for the employee's manager. &lt;em&gt;Fires for Red risk only.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create HR Alert&lt;/strong&gt; — Creates a silent, confidential &lt;code&gt;HR_Alert__c&lt;/code&gt; record. &lt;em&gt;Fires for Red risk only.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The scoring algorithm is pure deterministic Apex — no LLM decides someone's risk level. That's a business rule, not a creative task.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Agent Script Structure
&lt;/h3&gt;

&lt;p&gt;One topic: &lt;strong&gt;Assess Wellbeing&lt;/strong&gt;. I deliberately kept it single-topic because the power here is in the action chaining and conditional logic, not in complex topic routing.&lt;/p&gt;

&lt;p&gt;The script uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Action chaining&lt;/strong&gt; — Calculate Score runs first, its output (risk level) is stored in a variable, that variable feeds into conditional logic for the next actions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conditional routing&lt;/strong&gt; — Red triggers all three actions + urgent message. Amber triggers only the score + gentle check-in. Green triggers only the score + positive reinforcement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hybrid reasoning&lt;/strong&gt; — Deterministic IF conditions for action routing, LLM generation for empathetic employee messages&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Three Tests, Three Completely Different Outcomes
&lt;/h3&gt;

&lt;p&gt;I tested with three demo employees, each representing a different burnout profile:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leila Novak — Red Risk (67 hours/week, 13 overdue tasks, 4 weekend logins)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What the Trace panel showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Topic → Assess Wellbeing
Action → Calculate Burnout Score → Red
Action → Schedule Manager Meeting → Triggered
Action → Create HR Alert → Triggered
LLM → Generated warm, personal message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What Leila sees: A warm, personal message with no scores, no clinical language — just genuine care.&lt;/p&gt;

&lt;p&gt;What gets created silently: WellbeingScore (Red), HR Alert (confidential), Manager Task (high priority), Calendar Event (1:1 meeting).&lt;/p&gt;

&lt;p&gt;All in under 10 seconds. Leila didn't ask for help. Her manager didn't notice. Nobody typed anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Florence Stein — Amber Risk (52 hours/week, some weekend activity)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trace panel: Only the scoring action fired. No meeting. No HR alert. The deterministic rules correctly blocked escalation. Florence received a gentle, empathetic check-in — proportionate and intelligent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shane Marin — Green Risk (38 hours/week, zero weekend logins)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trace panel: Just the scoring action. No escalations. A positive, warm acknowledgement of healthy work habits.&lt;/p&gt;

&lt;p&gt;Three employees. Three completely different, perfectly proportionate responses. All autonomous.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Use Apex and Flows?
&lt;/h2&gt;

&lt;p&gt;Fair question. You could absolutely build a scoring engine, conditional record creation, and template emails with Apex and Flows alone. But here's what you'd lose:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The messages would be dead.&lt;/strong&gt; A template email saying "Your burnout score is Amber, please take care" is clinical, impersonal, and gets ignored. The LLM writes messages that feel like they come from a real person. Every message is different. Every message is tailored to the individual.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The logic would be fragile.&lt;/strong&gt; Hard-coding every conditional path in a Flow means every new risk factor requires a new Flow version. Agent Script's conditional routing is declarative and readable — the entire decision tree in one screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There would be no transparency.&lt;/strong&gt; The Trace panel gives you an audit trail of every decision. In an HR context, proving &lt;em&gt;why&lt;/em&gt; the agent escalated (or didn't) is a legal requirement in many jurisdictions. Flows don't give you that reasoning chain.&lt;/p&gt;

&lt;p&gt;Hybrid reasoning isn't about replacing Flows — it's about handling use cases where you need &lt;strong&gt;both guaranteed behaviour and human-quality communication&lt;/strong&gt; in the same workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Took Away from Building with Agent Script
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with one topic.&lt;/strong&gt; Don't over-engineer the topic structure. The power is in actions and conditional logic, not in topic routing complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let Apex handle the math, let the LLM handle the humans.&lt;/strong&gt; Scoring algorithms should be deterministic. Messages to real people should be generative. Don't mix these up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Trace panel is non-negotiable for enterprise use.&lt;/strong&gt; If you can't explain why the agent did something, you can't ship it. Build with traceability from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Study the Agent Script Recipes before building from scratch.&lt;/strong&gt; The multi-action chaining and conditional routing patterns saved me hours. Start there, then adapt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design for dignity.&lt;/strong&gt; If your agent touches anything sensitive — HR, health, performance — never expose internal scores to the people being assessed. The system should feel like a caring colleague, not a surveillance tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;Agentforce isn't just a chatbot platform with better branding. Agent Script, hybrid reasoning, the Atlas Reasoning Engine, action chaining, conditional routing — these are infrastructure for building agents that enterprises can actually trust.&lt;/p&gt;

&lt;p&gt;The Burnout Prevention Agent is one use case. But the same patterns — deterministic business rules + generative communication + full traceability — apply to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sales&lt;/strong&gt;: Lead scoring with personalised outreach&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support&lt;/strong&gt;: Ticket triage with empathetic customer responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finance&lt;/strong&gt;: Compliance checks with contextual explanations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operations&lt;/strong&gt;: Anomaly detection with proportionate escalation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tools are here. The recipes are documented. The builder makes it accessible.&lt;/p&gt;

&lt;p&gt;If you're in the Salesforce ecosystem and you haven't explored Agent Script yet — now is the time.&lt;/p&gt;




&lt;p&gt;You can find the demo recording on &lt;a href="https://x.com/likhitVP" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt;.*&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;#Salesforce&lt;/code&gt; &lt;code&gt;#Agentforce&lt;/code&gt; &lt;code&gt;#AgentScript&lt;/code&gt; &lt;code&gt;#HybridReasoning&lt;/code&gt; &lt;code&gt;#AI&lt;/code&gt; &lt;code&gt;#AtlasReasoningEngine&lt;/code&gt; &lt;code&gt;#AgentforceBuilder&lt;/code&gt; &lt;code&gt;#ActionChaining&lt;/code&gt; &lt;code&gt;#HRTech&lt;/code&gt; &lt;code&gt;#BurnoutPrevention&lt;/code&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;References:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.salesforce.com/blogs/2025/10/introducing-hybrid-reasoning-with-agent-script" rel="noopener noreferrer"&gt;Introducing Hybrid Reasoning with Agent Script — Salesforce Developers Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.salesforce.com/blogs/2026/02/agent-script-decoded-intro-to-agent-script-language-fundamentals" rel="noopener noreferrer"&gt;Agent Script Decoded: Language Fundamentals — Salesforce Developers Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.salesforce.com/docs/ai/agentforce/guide/ascript-patterns-action-chaining.html" rel="noopener noreferrer"&gt;Action Chaining Patterns — Agentforce Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.salesforce.com/sample-apps/agent-script-recipes" rel="noopener noreferrer"&gt;Agent Script Recipes — Salesforce Developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.salesforce.com/docs/ai/agentforce/guide/agent-script.html" rel="noopener noreferrer"&gt;Agent Script Documentation — Agentforce Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>salesforce</category>
      <category>development</category>
    </item>
    <item>
      <title>I Cut My Context-Switch Recovery From 23 Minutes to 5 Seconds</title>
      <dc:creator>Likhit Kumar V P</dc:creator>
      <pubDate>Fri, 13 Mar 2026 20:24:20 +0000</pubDate>
      <link>https://dev.to/likhit/i-cut-my-context-switch-recovery-from-23-minutes-to-5-seconds-4mo5</link>
      <guid>https://dev.to/likhit/i-cut-my-context-switch-recovery-from-23-minutes-to-5-seconds-4mo5</guid>
      <description>&lt;h2&gt;
  
  
  The most expensive thing in software isn't bugs. It's forgetting.
&lt;/h2&gt;

&lt;p&gt;A developer sits down at 9 AM. Fresh coffee. Terminal open. And then: &lt;em&gt;What was I doing?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;They run &lt;code&gt;git status&lt;/code&gt;. Then &lt;code&gt;git stash list&lt;/code&gt;. Grep for TODOs. Scroll shell history. Check which files are open in VS Code. Pull up GitHub for open PRs. Open Slack to see if anyone mentioned their branch.&lt;/p&gt;

&lt;p&gt;Ten minutes pass before they write a single character of code.&lt;/p&gt;

&lt;p&gt;This isn't a personal failing. It's a structural one. UC Irvine researchers found it takes &lt;strong&gt;23 minutes and 15 seconds&lt;/strong&gt; to fully regain deep focus after a single interruption. Let that sink in. An 8-hour workday produces 4.8 hours of actual output. The rest is recovery.&lt;/p&gt;

&lt;p&gt;So I built a tool to collapse that recovery time to 5 seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ snapctx

────────────────────────────────────────────────────────

SnapContext 8:45 AM · nemotron-3-nano-30b

────────────────────────────────────────────────────────

◉ IN PROGRESS branch: feat/notifications

confidence: ●●●●○ 80%

3 unstaged · 2 staged · 1 untracked · 1 stash · 2 TODOs

────────────────────────────────────────────────────────
You were building the notification routing system.
NotificationRouter.ts is created but not yet wired
into app.ts. Two TODOs remain: "wire websocket handler"
and "add error middleware". Your next structural step
is connecting the router to the entry point.
────────────────────────────────────────────────────────

4.5s

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command. 5 seconds. Plain English.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But doesn't Copilot already -" No.
&lt;/h2&gt;

&lt;p&gt;Copilot writes code. ChatGPT answers questions. Neither one tells you &lt;strong&gt;what you were doing when you left&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;SnapContext doesn't write code. It doesn't review code. It doesn't even &lt;em&gt;read&lt;/em&gt; code. It's a &lt;strong&gt;bookmark for your brain&lt;/strong&gt; - it reads your working tree structure, not your implementation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never reads file contents&lt;/strong&gt; - only file names and line-count stats&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never sends code to AI&lt;/strong&gt; - the prompt is pure structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets aren't redacted, they're EXCLUDED&lt;/strong&gt; - if a shell command contains an API key, the entire line is dropped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works fully offline&lt;/strong&gt; - &lt;code&gt;snapctx --provider ollama&lt;/code&gt; uses a local model, zero network calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your code never leaves your machine. Period.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually reads
&lt;/h2&gt;

&lt;p&gt;10 collectors run in parallel. Each gets 5 seconds. One failure never crashes the rest (&lt;code&gt;Promise.allSettled&lt;/code&gt;, not &lt;code&gt;Promise.all&lt;/code&gt;).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;What SnapContext sees&lt;/th&gt;
&lt;th&gt;What it NEVER sees&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Git working tree&lt;/td&gt;
&lt;td&gt;"3 files changed in src/auth/"&lt;/td&gt;
&lt;td&gt;The actual code changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git stash&lt;/td&gt;
&lt;td&gt;"stash@{0}: WIP on feat/auth (4 files)"&lt;/td&gt;
&lt;td&gt;The stashed content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TODOs&lt;/td&gt;
&lt;td&gt;"TODO: wire websocket handler" (from diff)&lt;/td&gt;
&lt;td&gt;Surrounding code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shell history&lt;/td&gt;
&lt;td&gt;"ran npm test 12 times in 30 min"&lt;/td&gt;
&lt;td&gt;Your secrets or env vars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDE open files&lt;/td&gt;
&lt;td&gt;"auth.ts, middleware.ts open in VS Code"&lt;/td&gt;
&lt;td&gt;File contents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub PRs&lt;/td&gt;
&lt;td&gt;"PR #42: 2 approvals, CI passing"&lt;/td&gt;
&lt;td&gt;Code diffs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser tabs&lt;/td&gt;
&lt;td&gt;"localhost:3000, jwt.io, Stack Overflow"&lt;/td&gt;
&lt;td&gt;Page content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI sessions&lt;/td&gt;
&lt;td&gt;"Claude Code session: 'auth refactor'"&lt;/td&gt;
&lt;td&gt;Conversation content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PM tickets&lt;/td&gt;
&lt;td&gt;"LINEAR-123: Implement OAuth"&lt;/td&gt;
&lt;td&gt;Description body&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the data diet. Structure, not substance. Enough for an AI to say "you were building OAuth middleware and got stuck on the token refresh flow" &lt;/p&gt;

&lt;h2&gt;
  
  
  The part that prevents hallucination
&lt;/h2&gt;

&lt;p&gt;Here's my favorite engineering decision: &lt;strong&gt;the confidence gate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;SnapContext has an engine called FIE (Feature Inference Engine) that scores every signal. It detects your state:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;What triggered it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;in-progress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;You have uncommitted changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blocked&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zero changes but heavy shell activity (debugging loop)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stashed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stashes exist, clean tree (you shelved work)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;context-switch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reflog shows you just switched branches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;conflict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Merge conflicts detected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nothing going on&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the confidence score drops below 25% and the state is &lt;code&gt;clean&lt;/code&gt;, SnapContext &lt;strong&gt;refuses to call the AI&lt;/strong&gt;. It says "nothing in progress" and exits. No hallucinated context. No invented narrative about what you "might" have been doing.&lt;/p&gt;

&lt;p&gt;In an era where every AI tool is racing to generate &lt;em&gt;more&lt;/em&gt;, this one knows when to shut up.&lt;/p&gt;

&lt;h2&gt;
  
  
  It remembers what you don't
&lt;/h2&gt;

&lt;p&gt;Every briefing gets saved to a local SQLite database (Node 23's built-in &lt;code&gt;node:sqlite&lt;/code&gt; — zero extra dependencies).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;snapctx &lt;span class="nb"&gt;history

&lt;/span&gt;Date State Module Conf Branch
just now ◉ progress &lt;span class="nb"&gt;history&lt;/span&gt; ●●●○○ feat/sqlite
2h ago ◉ progress fie ●●●●○ feat/sqlite
1d ago ✓ clean — ○○○○○ main

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See what changed since your last briefing — no AI call, instant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;snapctx diff

State &lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="nt"&gt;-progress&lt;/span&gt; → &lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="nt"&gt;-progress&lt;/span&gt;
Module fie → &lt;span class="nb"&gt;history
&lt;/span&gt;Unstaged files 3 → 7 &lt;span class="o"&gt;(&lt;/span&gt;+4&lt;span class="o"&gt;)&lt;/span&gt;
TODOs 1 → 0 &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate a standup summary from today's activity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;snapctx eod

5 commits · 12 files · +486 · &lt;span class="nt"&gt;-6&lt;/span&gt;

Today you shipped the browser tab collector and ticket
tracking system. Five commits across three features...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or open the web dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;snapctx web
● http://localhost:7700

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;History browser, live streaming briefings via WebSocket, diff viewer, dark theme. All served from &lt;code&gt;node:http&lt;/code&gt; — no React, no build step, no &lt;code&gt;node_modules&lt;/code&gt; explosion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero dependencies. On purpose.
&lt;/h2&gt;

&lt;p&gt;The entire dependency list: &lt;strong&gt;Node.js built-ins&lt;/strong&gt;. That's it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;node:sqlite&lt;/code&gt; for history&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;node:http&lt;/code&gt; for the web server&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;node:crypto&lt;/code&gt; for WebSocket handshake (raw RFC 6455)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;node:child_process&lt;/code&gt; for git and shell commands &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No Express. No better-sqlite3. No ws. No React. No Webpack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free. Actually free.
&lt;/h2&gt;

&lt;p&gt;Every default provider is genuinely free, no credit card, no trial that expires:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;What you need&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;openrouter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nemotron 30B&lt;/td&gt;
&lt;td&gt;Free key from &lt;a href="https://openrouter.ai/keys" rel="noopener noreferrer"&gt;openrouter.ai/keys&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ollama&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Llama 3.2&lt;/td&gt;
&lt;td&gt;Local install, fully offline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;groq&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Llama 3.3 70B&lt;/td&gt;
&lt;td&gt;Free key from &lt;a href="https://console.groq.com" rel="noopener noreferrer"&gt;console.groq.com&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;huggingface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mistral 7B&lt;/td&gt;
&lt;td&gt;Free token from &lt;a href="https://huggingface.co" rel="noopener noreferrer"&gt;huggingface.co&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;auto&lt;/code&gt; mode tries each in order. If you want to go fully air-gapped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
ollama serve &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ollama pull llama3.2
snapctx &lt;span class="nt"&gt;--provider&lt;/span&gt; ollama

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No internet required. No telemetry. No analytics. Just your git repo and a local model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in 60 seconds
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
git clone https://github.com/Likhit-Kumar/SnapContext.git

&lt;span class="nb"&gt;cd &lt;/span&gt;SnapContext &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Free API key (no credit card): https://openrouter.ai/keys&lt;/span&gt;

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-or-v1-...

&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;snapctx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bash &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/snapctx.sh"&lt;/span&gt;

&lt;span class="c"&gt;# Run from any git project&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; ~/your-project &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; snapctx

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Likhit-Kumar/SnapContext" rel="noopener noreferrer"&gt;github.com/Likhit-Kumar/SnapContext&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Star it if you've ever lost 20 minutes just remembering what you were doing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Zero dependencies. MIT licensed. &lt;a href="https://github.com/Likhit-Kumar/SnapContext" rel="noopener noreferrer"&gt;Try it.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>opensource</category>
      <category>ai</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I Migrated a React Native App from Redux to Zustand</title>
      <dc:creator>Likhit Kumar V P</dc:creator>
      <pubDate>Sat, 28 Feb 2026 10:54:18 +0000</pubDate>
      <link>https://dev.to/likhit/i-migrated-a-react-native-app-from-redux-to-zustand-4cdo</link>
      <guid>https://dev.to/likhit/i-migrated-a-react-native-app-from-redux-to-zustand-4cdo</guid>
      <description>&lt;p&gt;A real migration story: the 4 Zustand patterns I kept after moving a production React Native app off Redux including the v5 crash that burned us and the architectural boundary that changed how I think about state.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Environment:&lt;/strong&gt; React Native 0.76+ · Zustand 5.x · TypeScript&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you want the short version before diving in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Selectors + &lt;code&gt;useShallow&lt;/code&gt;&lt;/strong&gt; are non-negotiable in v5, object selectors without it cause an infinite loop crash, not just extra re-renders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The slice pattern&lt;/strong&gt; scales a Zustand store across multiple domains without coupling them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;devtools&lt;/code&gt; + &lt;code&gt;persist&lt;/code&gt; + &lt;code&gt;immer&lt;/code&gt;&lt;/strong&gt; are the three middleware that make Zustand production-ready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outside-React access&lt;/strong&gt; via &lt;code&gt;getState()&lt;/code&gt; and &lt;code&gt;subscribe()&lt;/code&gt; is Zustand's most underrated advantage in React Native&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real win&lt;/strong&gt; is the architectural boundary: Zustand for client state, React Query for server state and never crossing those lines&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Three months into migrating a production React Native app from Redux to Zustand, my colleague sent a Slack message that stopped me cold: &lt;em&gt;"why does the filter screen crash every time someone searches for something?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer turned out to be a single line: an object selector without &lt;code&gt;useShallow&lt;/code&gt;. In Zustand v5, that's not a performance problem, it's an infinite render loop that throws &lt;code&gt;Maximum update depth exceeded&lt;/code&gt; and unmounts your component tree. We caught it in staging. Barely.&lt;/p&gt;

&lt;p&gt;That moment crystallised something about Zustand: it rewards people who understand what's happening under the hood, and it punishes cargo-culting in a very specific, memorable way. After the migration, I came away with four patterns that genuinely changed how I think about client state architecture. This post is all of them plus where Zustand falls short, how it pairs with React Query, and the boundary every senior dev should draw before writing a single line of state.&lt;/p&gt;

&lt;p&gt;This isn't a beginner's tour. We'll go deep.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Zustand Actually Is (and Isn't)
&lt;/h2&gt;

&lt;p&gt;Before the patterns, the boundary that matters most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zustand is a client state manager.&lt;/strong&gt; It manages state that originates and lives inside your application UI state, navigation context, user preferences, ephemeral session data, filter selections. It has no concept of staleness, no cache invalidation, no background refetching. It doesn't know your backend exists.&lt;/p&gt;

&lt;p&gt;This distinction isn't pedantic. It's the most common architectural mistake made with Zustand: using it to store data fetched from an API and then manually wiring up all the synchronisation logic that a server state tool like React Query or SWR would give you for free. We'll revisit this in the architecture section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Zustand is:&lt;/strong&gt; a minimal, subscription-based state container built on a publish/subscribe pattern, with a React hook interface on top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it isn't:&lt;/strong&gt; a server cache, a Redux replacement for all use cases, or a global state dumping ground.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works Under the Hood
&lt;/h2&gt;

&lt;p&gt;Understanding Zustand's internals is what separates confident usage from cargo-culting and explains why some footguns are so easy to step on.&lt;/p&gt;

&lt;p&gt;At its core, Zustand creates a &lt;em&gt;store&lt;/em&gt;, a closure that holds state and a &lt;code&gt;setState&lt;/code&gt; function. This is framework-agnostic. The React integration is a thin layer on top using &lt;code&gt;useSyncExternalStore&lt;/code&gt;, React's official API for subscribing to external stores.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is essentially what Zustand's vanilla store does&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;partial&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subscribe&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a component calls &lt;code&gt;useStore(selector)&lt;/code&gt;, it subscribes to the store and re-renders only when the selected slice changes. This is the key performance insight: &lt;strong&gt;Zustand re-renders are selector-scoped, not store-wide.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is fundamentally different from React Context, where any context value change re-renders all consumers. It's also why Zustand outperforms naive Context usage at scale without requiring memoisation gymnastics.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Zustand v5 note:&lt;/strong&gt; v5 dropped the &lt;code&gt;use-sync-external-store&lt;/code&gt; shim package as a runtime dependency for the main &lt;code&gt;create&lt;/code&gt; path, using the native &lt;code&gt;useSyncExternalStore&lt;/code&gt; built into React 18 directly. This is why React 18 is now the minimum required version. The shim remains a peer dependency for &lt;code&gt;zustand/traditional&lt;/code&gt; (for &lt;code&gt;createWithEqualityFn&lt;/code&gt; and legacy equality patterns), but for standard usage you no longer need it. v5 is purely about dropping legacy support and tightening the foundation.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Installation is a single dependency with no peer requirements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;zustand
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No additional configuration for React Native, it works with both the old and new architecture (Bridgeless/JSI). TypeScript works out of the box with the curried &lt;code&gt;create&amp;lt;T&amp;gt;()()&lt;/code&gt; syntax, which is required for correct type inference with middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// store/useFilterStore.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FilterMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;FilterState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FilterMode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FilterMode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setSearchQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useFilterStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FilterState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()((&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;setMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;setSearchQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="c1"&gt;// store.getInitialState() — the v5-recommended reset pattern.&lt;/span&gt;
  &lt;span class="c1"&gt;// The older approach (capturing a separate `initialState` object and calling&lt;/span&gt;
  &lt;span class="c1"&gt;// set(initialState)) still works, but getInitialState() is cleaner because&lt;/span&gt;
  &lt;span class="c1"&gt;// the store itself guarantees the source of truth rather than a variable that&lt;/span&gt;
  &lt;span class="c1"&gt;// could drift.&lt;/span&gt;
  &lt;span class="na"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInitialState&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few deliberate choices here: state and actions live together in the same definition, and the interface is explicit rather than inferred. At scale, explicit types are worth the verbosity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 1: Selectors with &lt;code&gt;useShallow&lt;/code&gt; : The Performance Foundation (and the Crash We Almost Shipped)
&lt;/h2&gt;

&lt;p&gt;This is where most teams leave performance on the table and where we almost shipped a crash to production.&lt;/p&gt;

&lt;p&gt;Every store call in a component should use a selector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Subscribes to the entire store — re-renders on any state change&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFilterStore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Only re-renders when `mode` changes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFilterStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trap with object selectors in v5 is more severe than a performance issue, it causes React to throw &lt;code&gt;Maximum update depth exceeded&lt;/code&gt;, which unmounts your component tree. Zustand v5 uses &lt;code&gt;useSyncExternalStore&lt;/code&gt; directly, which requires stable selector references. A selector that returns a new object on every call creates a reference that never stabilises, triggering an infinite reconciliation loop before React bails out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Both of these import paths are valid in v5:&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useShallow&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand/react/shallow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// recommended in the prevent-rerenders guide&lt;/span&gt;
&lt;span class="c1"&gt;// import { useShallow } from 'zustand/shallow';    // used in the v5 migration guide&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Returns a new object on every render&lt;/span&gt;
&lt;span class="c1"&gt;// In v5 this doesn't cause extra re-renders — it throws:&lt;/span&gt;
&lt;span class="c1"&gt;// "Uncaught Error: Maximum update depth exceeded"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;searchQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFilterStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchQuery&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ useShallow stabilises the reference — only re-renders if values actually change&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;searchQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFilterStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;useShallow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchQuery&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;v5 breaking change, this is a render error, not just a perf issue:&lt;/strong&gt; In v4, you could pass &lt;code&gt;shallow&lt;/code&gt; as a second argument to the store hook (e.g. &lt;code&gt;useStore(selector, shallow)&lt;/code&gt;). In v5, this signature was removed. Object selectors without &lt;code&gt;useShallow&lt;/code&gt; now cause React to throw a maximum update depth error that unmounts the affected component. This is the single most common crash teams hit when migrating from v4 to v5. Don't skip it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In React Native, where component tree errors surface as blank screens, this distinction matters critically. &lt;code&gt;useShallow&lt;/code&gt; for all object selectors is non-negotiable in v5.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 2: The Slice Architecture : One Store, Multiple Concerns
&lt;/h2&gt;

&lt;p&gt;For large apps, a monolithic store with 40 fields and 30 actions becomes hard to reason about fast. The slice pattern lets you compose a single store from multiple logical domains while keeping each domain's code self-contained.&lt;/p&gt;

&lt;p&gt;The insight I didn't expect during the migration: decomposing the Redux store into slices was actually &lt;em&gt;easier&lt;/em&gt; than I anticipated, because the domains were already mostly independent. The coupling I'd assumed existed mostly didn't. That said, the key is typing each slice's &lt;code&gt;set&lt;/code&gt; with Zustand's &lt;code&gt;StateCreator&lt;/code&gt; type, something most examples skip but which is essential for correct TypeScript inference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// store/slices/uiSlice.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StateCreator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;UISlice&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;isBottomSheetOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;activeTab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;openBottomSheet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;closeBottomSheet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setActiveTab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// StateCreator&amp;lt;BoundStore, [], [], UISlice&amp;gt; gives set correct knowledge&lt;/span&gt;
&lt;span class="c1"&gt;// of the full composed store shape — essential when slices reference each other&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createUISlice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StateCreator&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="nx"&gt;UISlice&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;SessionSlice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;UISlice&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;isBottomSheetOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;activeTab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;openBottomSheet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;isBottomSheetOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;closeBottomSheet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;isBottomSheetOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;setActiveTab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;activeTab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tab&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// store/slices/sessionSlice.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StateCreator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SessionSlice&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setLocale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createSessionSlice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StateCreator&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="nx"&gt;UISlice&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;SessionSlice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;SessionSlice&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;setUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;setLocale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// store/useBoundStore.ts — composed store&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createUISlice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UISlice&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./slices/uiSlice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSessionSlice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SessionSlice&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./slices/sessionSlice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BoundStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UISlice&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;SessionSlice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useBoundStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BoundStore&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()((...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;createUISlice&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;createSessionSlice&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each slice owns its state shape and actions. Consuming components import from &lt;code&gt;useBoundStore&lt;/code&gt; with a selector scoped to the slice they need. No coupling between slices unless intentionally designed.&lt;/p&gt;

&lt;p&gt;The rule of thumb for when to use slices vs separate stores: if two domains need to read each other's state inside an action, they belong in the same store. If they're genuinely independent, separate stores are cleaner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// These are genuinely independent, separate stores are correct&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useThemeStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ThemeState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()(...);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useOnboardingStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OnboardingState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Pattern 3: The Middleware Stack : &lt;code&gt;devtools&lt;/code&gt; + &lt;code&gt;persist&lt;/code&gt; + &lt;code&gt;immer&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is where Zustand earns its production credibility. Three middleware pieces, each solving a distinct problem. What I discovered in the migration is that none of these are nice-to-haves.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;devtools&lt;/code&gt; : Non-Negotiable in Development
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;devtools&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand/middleware&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useFilterStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FilterState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;
  &lt;span class="nf"&gt;devtools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;setMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filter/setMode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filter/reset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FilterStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third argument to &lt;code&gt;set&lt;/code&gt; inside &lt;code&gt;devtools&lt;/code&gt; is the action name, it appears in Redux DevTools (which Zustand integrates with via the same browser extension). Named actions make debugging dramatically easier. Make them a habit, not an afterthought.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;persist&lt;/code&gt; : Surviving App Restarts
&lt;/h3&gt;

&lt;p&gt;For state that should survive app kills (theme, locale, onboarding status, form drafts):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createJSONStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand/middleware&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-native-async-storage/async-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ThemeState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;colorScheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setColorScheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useThemeStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ThemeState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;
  &lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;colorScheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;setColorScheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;colorScheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;scheme&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createJSONStorage&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="c1"&gt;// Persist only state fields, never actions — keeps storage lean&lt;/span&gt;
      &lt;span class="na"&gt;partialize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;colorScheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colorScheme&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;migrate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;persistedState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Migrate from v0 shape to v1 — add new field with default&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;persistedState&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ThemeState&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;colorScheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;persistedState&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ThemeState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things most teams skip until they're burned:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;partialize&lt;/code&gt;:&lt;/strong&gt; Persist only the state fields, never actions. Persisting functions causes serialisation errors and inflates storage unnecessarily.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;version&lt;/code&gt; + &lt;code&gt;migrate&lt;/code&gt;:&lt;/strong&gt; If you change the shape of persisted state in a new release, users on the old shape will hydrate into broken state on upgrade. Version from day one, it costs nothing upfront and saves a painful hotfix later.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;v5 persist change:&lt;/strong&gt; In v4 (up to 4.5.4), the initial state was automatically written to storage on store creation. In v5 this was removed. If you were relying on initial state being persisted without any user interaction, you'll need to explicitly write it after store creation. This is a silent migration gotcha that burned several teams.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;immer&lt;/code&gt; : Ergonomic Nested Updates
&lt;/h3&gt;

&lt;p&gt;For deeply nested state, Zustand's &lt;code&gt;immer&lt;/code&gt; middleware lets you write mutating syntax that produces immutable updates under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;immer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand/middleware/immer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;setFieldValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fields&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setFieldError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fields&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useFormStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;
  &lt;span class="nf"&gt;immer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;setFieldValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Looks mutating, produces immutable update&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;setFieldError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;immer&lt;/code&gt;, nested updates require verbose spread chains. Use it selectively, for flat state structures, it adds overhead with no benefit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 4: Outside-React Access : The React Native Superpower
&lt;/h2&gt;

&lt;p&gt;This pattern surprised me most during the migration. On Redux, accessing store state outside a component meant importing the store object and calling &lt;code&gt;store.getState()&lt;/code&gt; : possible, but architecturally messy because the Redux store is a singleton you're supposed to connect through middleware. Zustand is different in a meaningful way.&lt;/p&gt;

&lt;p&gt;React Native apps frequently need store access in non-component contexts : navigation handlers, push notification callbacks, Axios interceptors, analytics services. Every Zustand store exposes &lt;code&gt;getState()&lt;/code&gt;, &lt;code&gt;setState()&lt;/code&gt;, and &lt;code&gt;subscribe()&lt;/code&gt; directly on the store object, no Provider boundary to escape, no hook rules to worry about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSessionStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./store/useSessionStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --- In an Axios request interceptor (app init, not a component) ---&lt;/span&gt;
&lt;span class="nx"&gt;axiosInstance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interceptors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useSessionStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-User-Id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// --- In a push notification handler ---&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useUIStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./store/useUIStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addNotificationReceivedListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;useUIStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;isBottomSheetOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&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;// --- Subscribing to state changes outside React ---&lt;/span&gt;
&lt;span class="c1"&gt;// Requires subscribeWithSelector middleware on the store (see note below)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;subscribeWithSelector&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand/middleware&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useSessionStoreWithSelector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SessionState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;
  &lt;span class="nf"&gt;subscribeWithSelector&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;setUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useSessionStoreWithSelector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// selector&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;identify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// callback — fires only when userId changes&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Clean up when no longer needed&lt;/span&gt;
&lt;span class="c1"&gt;// unsubscribe();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; The two-argument &lt;code&gt;subscribe(selector, callback)&lt;/code&gt; signature is not available on a basic Zustand store. It requires the &lt;code&gt;subscribeWithSelector&lt;/code&gt; middleware to be applied during store creation. Without it, TypeScript will error and the selector argument will be silently ignored in JavaScript. If you only need the full-state listener (one argument), no middleware is needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This outside-React pattern is particularly valuable for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Attaching user context to API request interceptors at app init&lt;/li&gt;
&lt;li&gt;Syncing store state with deep-link handlers&lt;/li&gt;
&lt;li&gt;Reading store state inside notification handler callbacks&lt;/li&gt;
&lt;li&gt;Triggering analytics events when specific state slices change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The elegance here is that none of this requires any special Redux-style plumbing. The store is just a closure with a clean interface. That simplicity is Zustand's real design achievement.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architectural Boundary: Zustand vs React Query
&lt;/h2&gt;

&lt;p&gt;This is the decision that matters most, and it deserves direct treatment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────┐
│                     Application State                           │
├───────────────────────────┬─────────────────────────────────────┤
│       Client State        │          Server State               │
│       (Zustand)           │       (React Query / SWR)           │
├───────────────────────────┼─────────────────────────────────────┤
│ • Active filter/sort      │ • API response data                 │
│ • Selected item ID        │ • Paginated lists                   │
│ • Modal open/closed       │ • User profile from backend         │
│ • Theme / locale          │ • Notifications from API            │
│ • Onboarding step         │ • Any data with a TTL               │
│ • Form draft state        │ • Data shared across screens        │
│ • Navigation history      │ • Data that needs background sync   │
└───────────────────────────┴─────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test is simple: &lt;strong&gt;does this data have a source of truth on a server?&lt;/strong&gt; If yes, it belongs in a server state manager. If the source of truth is the user's current session and nothing more, it belongs in Zustand.&lt;/p&gt;

&lt;p&gt;Where teams go wrong is storing API responses in Zustand. You end up building a manual cache: loading flags, error states, refetch logic, staleness checks, deduplication. It works until it doesn't, and debugging cache consistency bugs across screens in production is genuinely unpleasant. I know because we had exactly that problem in the Redux version, one of the primary reasons for the migration.&lt;/p&gt;

&lt;p&gt;The two tools compose cleanly and coordinate naturally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useFilterStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../store/useFilterStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ItemListScreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Client state — Zustand owns this&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filterMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFilterStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setFilter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFilterStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setMode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Server state — React Query owns this&lt;/span&gt;
  &lt;span class="c1"&gt;// isPending is the correct v5 flag — isLoading was redefined in v5&lt;/span&gt;
  &lt;span class="c1"&gt;// as `isPending &amp;amp;&amp;amp; isFetching`, which returns false when enabled: false&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isPending&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filterMode&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filterMode&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// filterMode from Zustand flows into the React Query key.&lt;/span&gt;
  &lt;span class="c1"&gt;// When the user changes the filter, Zustand updates, the key changes,&lt;/span&gt;
  &lt;span class="c1"&gt;// React Query fires a new fetch automatically — no manual wiring needed.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The elegance of this composition Zustand filter state flowing directly into a React Query cache key is something I didn't fully appreciate until I saw it eliminate about 60 lines of manual sync logic from our old Redux middleware. That's the real win from drawing the boundary correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing Zustand Stores
&lt;/h2&gt;

&lt;p&gt;Stores are plain JavaScript objects testing them without React is one of Zustand's most underappreciated advantages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// __tests__/filterStore.test.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useFilterStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../store/useFilterStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FilterStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Reset store to initial state between tests&lt;/span&gt;
    &lt;span class="nx"&gt;useFilterStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updates filter mode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;useFilterStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;setMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;useFilterStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resets to initial state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;useFilterStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;useFilterStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;searchQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useFilterStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No mocking, no test renderers, no async ceremony. Pure store logic tests run fast and are straightforward to reason about. For components, test via &lt;code&gt;@testing-library/react-native&lt;/code&gt; as normal, Zustand stores hydrate naturally in the test environment without Provider wrappers. This alone made our test suite meaningfully faster after the migration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Over-globalising state.&lt;/strong&gt; Not every piece of state needs to be global. If state is only used by one screen, &lt;code&gt;useState&lt;/code&gt; is still the right call. Reach for Zustand when state genuinely needs to be shared across component trees or accessed outside components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storing server data in Zustand.&lt;/strong&gt; Already covered, it leads to a manual cache implementation that's harder to build and maintain than just using React Query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Object selectors without &lt;code&gt;useShallow&lt;/code&gt; in v5.&lt;/strong&gt; &lt;code&gt;const store = useStore()&lt;/code&gt; subscribes to every field. Object selectors without &lt;code&gt;useShallow&lt;/code&gt; cause React to throw a maximum update depth error in v5. Always use primitive selectors or &lt;code&gt;useShallow&lt;/code&gt; for multi-field selections. No exceptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not versioning persisted state.&lt;/strong&gt; The first time you change a persisted store's shape without a migration, a subset of users will see broken state on upgrade. Version from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;subscribe(selector, callback)&lt;/code&gt; without &lt;code&gt;subscribeWithSelector&lt;/code&gt;.&lt;/strong&gt; This is a silent failure in JS and a TypeScript error in TS. If you need selector-scoped subscriptions outside React, apply the middleware during store creation.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Reach for Something Else
&lt;/h2&gt;

&lt;p&gt;Zustand is not always the answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React Query / SWR&lt;/strong&gt; : for any server data. Full stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useState&lt;/code&gt; / &lt;code&gt;useReducer&lt;/code&gt;&lt;/strong&gt; : for local component state that doesn't need sharing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jotai&lt;/strong&gt; : if you prefer an atomic model and want fine-grained subscriptions without selectors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redux Toolkit&lt;/strong&gt; : if your team has deep Redux investment, needs time-travel debugging, or has complex cross-cutting middleware requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zustand sits in a sweet spot: minimal API, near-zero boilerplate, excellent performance, and just enough structure to scale. For most React Native apps managing client state, it's the right default in 2026.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Migration Actually Changed
&lt;/h2&gt;

&lt;p&gt;After going through this, the biggest shift wasn't in the code, it was in how we reason about state. Redux pushed us toward treating everything as events dispatched into a central pipeline. That's powerful, but it also meant state decisions lived far from the components that needed them, and debugging required jumping between actions, reducers, and selectors that were never in the same file.&lt;/p&gt;

&lt;p&gt;Zustand pushed state decisions back toward the components, while still giving us the global access we need. The result was a codebase that new engineers could read without a mental model of the entire event graph.&lt;/p&gt;

&lt;p&gt;If you're on Redux and feeling the friction, the migration is more mechanical than it seems. The patterns in this post should give you enough foundation to make that call confidently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Object selectors without &lt;code&gt;useShallow&lt;/code&gt; cause render errors in v5.&lt;/strong&gt; Zustand v5 uses native &lt;code&gt;useSyncExternalStore&lt;/code&gt;, which requires stable selector references. A selector returning a new object on every render triggers &lt;code&gt;Maximum update depth exceeded&lt;/code&gt;. Always use &lt;code&gt;useShallow&lt;/code&gt; for any multi-field object selector. This is the number one migration crash from v4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The slice pattern scales cleanly but type your &lt;code&gt;StateCreator&lt;/code&gt;.&lt;/strong&gt; Untyped &lt;code&gt;set&lt;/code&gt; in slice functions is a common gap in examples. Use &lt;code&gt;StateCreator&amp;lt;BoundStore, [], [], SliceType&amp;gt;&lt;/code&gt; for correct inference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The middleware stack is where Zustand earns production credibility.&lt;/strong&gt; &lt;code&gt;devtools&lt;/code&gt; with named actions, &lt;code&gt;persist&lt;/code&gt; with versioning, &lt;code&gt;immer&lt;/code&gt; for nested state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outside-React access is a genuine architectural advantage.&lt;/strong&gt; &lt;code&gt;getState()&lt;/code&gt;, &lt;code&gt;setState()&lt;/code&gt;, and &lt;code&gt;subscribe()&lt;/code&gt; work anywhere. For selector-scoped subscriptions outside React, apply &lt;code&gt;subscribeWithSelector&lt;/code&gt; middleware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The client/server boundary is the most important decision.&lt;/strong&gt; Zustand for client state, React Query for server state. Don't cross the streams.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What does your current React Native state setup look like, still on Redux, or already on Zustand? Did you hit the &lt;code&gt;useShallow&lt;/code&gt; crash in v5? I'm curious where other teams draw the line between client and server state. Drop a comment below.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>reactnative</category>
      <category>typescript</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Fixed Stale Data Across Every Screen in React Native with 3 React Query Patterns</title>
      <dc:creator>Likhit Kumar V P</dc:creator>
      <pubDate>Wed, 25 Feb 2026 15:40:26 +0000</pubDate>
      <link>https://dev.to/likhit/i-fixed-stale-data-across-every-screen-in-react-native-with-3-react-query-patterns-5fd5</link>
      <guid>https://dev.to/likhit/i-fixed-stale-data-across-every-screen-in-react-native-with-3-react-query-patterns-5fd5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Environment:&lt;/strong&gt; React Native 0.76+ · @tanstack/react-query v5 · TypeScript&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was building a React Native app where users could interact with the same data from multiple screens. Everything seemed fine during early testing until I noticed something deeply frustrating: &lt;strong&gt;updating data on one screen didn't reflect on another until I manually refreshed it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sound familiar? Here's how I diagnosed it, fixed it with 3 focused React Query patterns, and what I learned along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;The app had three screens showing overlapping data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Screen A&lt;/strong&gt; : Shows a list of items&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen B&lt;/strong&gt; : Shows the same items in a different view&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen C&lt;/strong&gt; : Shows item details with edit capability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a user marked an item complete on Screen A, navigating to Screen B still showed it as incomplete. Users had to pull-to-refresh on every screen after every action. The app felt broken, because it was.&lt;/p&gt;




&lt;h2&gt;
  
  
  Root Cause: Isolated State
&lt;/h2&gt;

&lt;p&gt;After some digging, the culprit was clear. Each screen used its own hook with independent &lt;code&gt;useState&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lib/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Screen A hook&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useListA&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setItems&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markComplete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;
    &lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="c1"&gt;// Other screens have no idea this happened&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Screen B hook — completely separate state&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useListB&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setItems&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="c1"&gt;// No connection to Screen A's state whatsoever&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The architecture looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Screen A          Screen B          Screen C
   │                 │                 │
   ▼                 ▼                 ▼
useState          useState          useState
(isolated)        (isolated)        (isolated)
   │                 │                 │
   └─────────────────┴─────────────────┘
                     │
                     ▼
                 Backend API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each screen fetched and managed data independently. When one mutated data, the others had no idea. This isn't a bug, it's the natural consequence of uncoordinated local state managing shared server data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Use Zustand?
&lt;/h2&gt;

&lt;p&gt;This question comes up a lot, and it's worth addressing directly before diving into the solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zustand is a client state manager.&lt;/strong&gt; It's excellent for data that lives purely inside your app UI state, filter selections, modal toggles, user preferences. It has no built-in concept of staleness, background refetching, or cache invalidation because it doesn't need one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React Query is a server state manager.&lt;/strong&gt; It's designed specifically for data that originates from a backend for fetching, caching, deduplicating requests, and keeping that data fresh across your app.&lt;/p&gt;

&lt;p&gt;Using Zustand to solve this problem would mean manually writing all the cache logic React Query gives you for free. In fact, the ideal architecture uses &lt;strong&gt;both&lt;/strong&gt; React Query for server data, Zustand for client-only state. That's a topic for another post. For now, our problem is server state sync, so React Query is the right tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: 3 React Query Patterns
&lt;/h2&gt;

&lt;p&gt;I had React Query installed but wasn't using it effectively each screen was still managing its own fetch lifecycle. The fix came down to three focused patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: A Query Keys Factory
&lt;/h3&gt;

&lt;p&gt;A centralised key system is what makes targeted cache invalidation possible. Instead of scattering string literals across your codebase, you define them once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/query-keys.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;detail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&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;// Define what needs invalidating after each action&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invalidationGroups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;itemUpdated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;queryKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;itemCreated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;queryKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;itemDeleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;queryKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;as const&lt;/code&gt; assertions give you full TypeScript inference downstream. Grouping by action means you invalidate precisely what's needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Mutations with Auto-Invalidation
&lt;/h3&gt;

&lt;p&gt;This is where the real magic happens. Mutation hooks that automatically invalidate related queries after every operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hooks/useMarkComplete.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useQueryClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lib/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;queryKeys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;invalidationGroups&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lib/query-keys&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// useSessionStore is a Zustand store holding auth state.&lt;/span&gt;
&lt;span class="c1"&gt;// Replace this with however your app stores the current user —&lt;/span&gt;
&lt;span class="c1"&gt;// Clerk, Firebase Auth, Supabase, or your own auth hook.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSessionStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../store/useSessionStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ...other fields&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useMarkComplete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSessionStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="c1"&gt;// Optimistic update — instant UI feedback before the API responds&lt;/span&gt;
    &lt;span class="na"&gt;onMutate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Cancel any in-flight queries to prevent race conditions&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;queryKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="c1"&gt;// Snapshot current data so we can roll back if needed&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getQueryData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

      &lt;span class="c1"&gt;// Optimistically update the cache right now&lt;/span&gt;
      &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setQueryData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;queryKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;old&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="c1"&gt;// Rollback on error — restore the snapshot&lt;/span&gt;
    &lt;span class="na"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setQueryData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="c1"&gt;// Surface the error to your UI here — toast, alert, inline message, etc.&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="c1"&gt;// Use onSettled (not onSuccess) so invalidation fires on both&lt;/span&gt;
    &lt;span class="c1"&gt;// success AND error — prevents stale data after a failed mutation&lt;/span&gt;
    &lt;span class="na"&gt;onSettled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;invalidationGroups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemUpdated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;onSettled&lt;/code&gt; instead of &lt;code&gt;onSuccess&lt;/code&gt;?&lt;/strong&gt; If we only invalidate on success, a failed mutation leaves the cache with the optimistically-updated (incorrect) data indefinitely. &lt;code&gt;onSettled&lt;/code&gt; runs regardless of outcome, so the cache always resolves to ground truth after any mutation. This is the React Query v5 recommended approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A common v5 confusion:&lt;/strong&gt; React Query v5 removed &lt;code&gt;onSuccess&lt;/code&gt;, &lt;code&gt;onError&lt;/code&gt;, and &lt;code&gt;onSettled&lt;/code&gt; callbacks from &lt;code&gt;useQuery&lt;/code&gt; and &lt;code&gt;useInfiniteQuery&lt;/code&gt; but they remain &lt;strong&gt;fully supported in &lt;code&gt;useMutation&lt;/code&gt;&lt;/strong&gt;. Mutations are imperative by nature, so callbacks are still the right model there. The switch to &lt;code&gt;onSettled&lt;/code&gt; here is a best-practice recommendation, not a required v5 migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;userId!&lt;/code&gt;?&lt;/strong&gt; The non-null assertion is safe here because the query is guarded with &lt;code&gt;enabled: !!userId&lt;/code&gt; (see Pattern 3 below). The query will never fire and therefore this mutation will never be callable when &lt;code&gt;userId&lt;/code&gt; is null.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Pattern 3: Connect to React Native App State
&lt;/h3&gt;

&lt;p&gt;React Query's &lt;code&gt;refetchOnWindowFocus&lt;/code&gt; works automatically in web browsers, but React Native needs an explicit bridge to the OS-level app state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/_layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;QueryClientProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;focusManager&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AppStateStatus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Note: In React Query v5, `cacheTime` was renamed to `gcTime`.&lt;/span&gt;
&lt;span class="c1"&gt;// If you're on v4, use `cacheTime` instead of `gcTime`.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;staleTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// Data is fresh for 2 minutes&lt;/span&gt;
      &lt;span class="na"&gt;gcTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Cache persists for 5 minutes after unmount&lt;/span&gt;
      &lt;span class="na"&gt;refetchOnWindowFocus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Don't auto-retry mutations — let the user decide&lt;/span&gt;
    &lt;span class="p"&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;// Bridges React Native's AppState to React Query's focus manager.&lt;/span&gt;
&lt;span class="c1"&gt;// Without this, refetchOnWindowFocus does nothing in a native context.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AppStateHandler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppStateStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;focusManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFocused&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;QueryClientProvider&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppStateHandler&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* rest of your app */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/QueryClientProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user backgrounds and returns to the app, &lt;code&gt;focusManager&lt;/code&gt; triggers a refetch of any stale queries keeping data fresh across app sessions, not just screen navigations.&lt;/p&gt;




&lt;h2&gt;
  
  
  The New Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Screen A          Screen B          Screen C
   │                 │                 │
   └─────────────────┴─────────────────┘
                     │
                     ▼
   ┌─────────────────────────────────────┐
   │     React Query Shared Cache        │
   │         ['items'] queries           │
   └─────────────────────────────────────┘
                     │
        ┌────────────┴────────────┐
        │                         │
        ▼                         ▼
   On Mutation:              All screens
   invalidateQueries()  →    auto-refetch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when any screen calls &lt;code&gt;markComplete()&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;UI updates instantly via the optimistic update&lt;/li&gt;
&lt;li&gt;API call happens in the background&lt;/li&gt;
&lt;li&gt;On settle (success or error), related queries are invalidated&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every screen subscribed to those queries auto-refreshes&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No manual refresh. No stale data. No coordination code between screens.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping It Backward Compatible
&lt;/h2&gt;

&lt;p&gt;One concern with refactoring is breaking existing screens. The cleanest approach is to wrap the new React Query internals behind the same hook interface your screens already use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hooks/useItems.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useQueryClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lib/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;queryKeys&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lib/query-keys&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useMarkComplete&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./useMarkComplete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSessionStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../store/useSessionStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useItems&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSessionStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markCompleteMutation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMarkComplete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;queryKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// Only fetches when userId exists — makes the userId! assertion above safe&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Same shape as before — existing screens need zero changes&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="c1"&gt;// isPending is the correct v5 flag for "data not loaded yet".&lt;/span&gt;
    &lt;span class="c1"&gt;// isLoading was redefined in v5 as isPending &amp;amp;&amp;amp; isFetching, which&lt;/span&gt;
    &lt;span class="c1"&gt;// returns false when enabled: false — causing a silent empty-state&lt;/span&gt;
    &lt;span class="c1"&gt;// bug on first render before userId is available.&lt;/span&gt;
    &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;refreshing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isRefetching&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// mutateAsync returns a Promise and throws on error — wrap in try/catch.&lt;/span&gt;
    &lt;span class="c1"&gt;// Use mutate instead if you prefer fire-and-forget without error throwing.&lt;/span&gt;
    &lt;span class="na"&gt;markComplete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;markCompleteMutation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;queryKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your screens call &lt;code&gt;useItems()&lt;/code&gt; exactly as they did before. The sync behaviour is just... there now.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Start Template
&lt;/h2&gt;

&lt;p&gt;If you're starting fresh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @tanstack/react-query
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useQueryClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lib/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Query keys&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&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;// 2. Query hook&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;staleTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Mutation with invalidation on settle&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useUpdateItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;onSettled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&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;// 4. Use in any screen — they'll all stay in sync&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AnyScreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useItems&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUpdateItem&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Replace data[0]?.id with however you get the target item's id&lt;/span&gt;
  &lt;span class="c1"&gt;// e.g. from route params, a selected state, a FlatList renderItem, etc.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;itemId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="nx"&gt;onPress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mark Complete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Multiple &lt;code&gt;useState&lt;/code&gt; hooks for the same server data will always drift.&lt;/strong&gt; It's not a discipline problem, it's an architectural one. Centralise with React Query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design your query keys for invalidation from the start.&lt;/strong&gt; Flat string keys are fine for small apps, but a factory pattern scales cleanly and eliminates typo bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;onSettled&lt;/code&gt;, not &lt;code&gt;onSuccess&lt;/code&gt;, for cache invalidation.&lt;/strong&gt; It guarantees the cache resolves to ground truth after every mutation, regardless of outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always guard queries with &lt;code&gt;enabled: !!userId&lt;/code&gt;.&lt;/strong&gt; In React Native, auth state often isn't available on the first render. Without this guard, your query fires with a null user ID and fails silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimistic updates are table stakes in 2026.&lt;/strong&gt; Users expect instant feedback. The rollback pattern handles failures gracefully without complex error state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backward-compatible migrations are always possible.&lt;/strong&gt; Wrap new internals behind existing interfaces and refactor incrementally without coordinating a team-wide rewrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React Native needs explicit AppState integration.&lt;/strong&gt; Don't skip the &lt;code&gt;focusManager&lt;/code&gt; setup without it, &lt;code&gt;refetchOnWindowFocus&lt;/code&gt; is a silent no-op in a native context.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This solves server state sync cleanly. But what about client-only state — filters, selections, UI flags — that also needs to be shared across screens?&lt;/p&gt;

&lt;p&gt;That's where Zustand comes in, and it pairs beautifully with React Query. Upcoming Post covers how to combine both tools into a complete React Native state architecture, with clear boundaries on what belongs where.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you been managing cross-screen state with manual refreshes? Or already using React Query, what patterns have worked for you? Drop a comment below.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>react</category>
      <category>development</category>
      <category>reactnative</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Tried to Send Emails Using Gmail SMTP, Here's What Actually Worked</title>
      <dc:creator>Likhit Kumar V P</dc:creator>
      <pubDate>Sun, 22 Feb 2026 10:37:01 +0000</pubDate>
      <link>https://dev.to/likhit/i-tried-to-send-emails-using-gmail-smtp-heres-what-actually-worked-2ec1</link>
      <guid>https://dev.to/likhit/i-tried-to-send-emails-using-gmail-smtp-heres-what-actually-worked-2ec1</guid>
      <description>&lt;p&gt;I was setting up email sending for a side project recently, nothing fancy, just a welcome email when a user signs up. Grabbed a Nodemailer tutorial, followed the steps, and ran straight into &lt;code&gt;Error: Invalid login: 535-5.7.8 Username and Password not accepted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Turns out the tutorial was outdated. Gmail's rules have changed, and a surprising amount of advice floating around the internet just doesn't work anymore.&lt;/p&gt;

&lt;p&gt;So here's what actually works in 2026.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Was Trying to Do
&lt;/h2&gt;

&lt;p&gt;My stack was simple: a Node.js backend, no paid services, and a Gmail account I already had. The plan was to use Gmail SMTP with Nodemailer that's free, no sign-ups, no credit cards. Should've been straightforward.&lt;/p&gt;

&lt;p&gt;Most tutorials I found were written in 2020 or 2021. Gmail's rules have changed a lot since then, and a good chunk of that advice is now broken. Let me clear up what's dead and what still works.&lt;/p&gt;




&lt;h2&gt;
  
  
  First, The Things That Are Dead (Don't Even Try)
&lt;/h2&gt;

&lt;p&gt;Before I tell you what works, let me tell you what I wasted time on so you don't have to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Less Secure Apps" toggle&lt;/strong&gt; - Every old tutorial mentioned this. Go to your Google account settings, flip this switch, done. Except when I looked for it, it simply didn't exist. Turns out Google permanently removed it back in May 2022. It's gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using my actual Gmail password&lt;/strong&gt; - My first instinct was just to throw my email and password into the Nodemailer config like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;me@gmail.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myActualPassword123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ❌ This will NOT work&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instant rejection. Google no longer accepts your real password over SMTP for accounts with 2-Step Verification. It feels like it should work but it doesn't.&lt;/p&gt;

&lt;p&gt;After burning time on both of those dead ends, I finally found the two approaches that actually work in 2026.&lt;/p&gt;




&lt;h2&gt;
  
  
  Method 1: App Passwords - The Quick Fix That Got Me Unblocked
&lt;/h2&gt;

&lt;p&gt;The first thing that actually worked for me was &lt;strong&gt;App Passwords&lt;/strong&gt;. An App Password is a special 16-character code that Google generates for your account. It's separate from your real password and designed specifically for apps and scripts that need SMTP access.&lt;/p&gt;

&lt;p&gt;Here's how I set it up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 : Enable 2-Step Verification
&lt;/h3&gt;

&lt;p&gt;App Passwords only exist on accounts with 2-Step Verification turned on. If you haven't done this already:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://myaccount.google.com/" rel="noopener noreferrer"&gt;myaccount.google.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Security&lt;/strong&gt; in the left sidebar&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;"How you sign in to Google"&lt;/strong&gt;, click &lt;strong&gt;2-Step Verification&lt;/strong&gt; and follow the setup&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2 : Generate an App Password
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Back in &lt;strong&gt;Security&lt;/strong&gt;, scroll down and click &lt;strong&gt;App passwords&lt;/strong&gt; &lt;em&gt;(Can't see it? 2-Step Verification isn't active yet)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name &lt;code&gt;"Nodemailer Side Project"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Copy the 16-character code. Write it down somewhere as &lt;strong&gt;you won't see it again&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3 : Wire It Into Nodemailer
&lt;/h3&gt;

&lt;p&gt;I created a &lt;code&gt;.env&lt;/code&gt; file first :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GMAIL_USER=me@gmail.com
GMAIL_APP_PASSWORD=abcdefghijklmnop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; Google displays the App Password with spaces like &lt;code&gt;abcd efgh ijkl mnop&lt;/code&gt;. Remove the spaces when you paste it into your &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then the Nodemailer config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodemailer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nodemailer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nodemailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gmail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GMAIL_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GMAIL_APP_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;transporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`"My App" &amp;lt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GMAIL_USER&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recipient@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;It works!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;Finally. &amp;lt;strong&amp;gt;It works.&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email sent! Message ID:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it worked. First email landed in the inbox cleanly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Gotcha That Caught Me (On My Cloud Server)
&lt;/h3&gt;

&lt;p&gt;Everything worked perfectly on my local machine. Then I deployed to my VPS and it broke again with same &lt;code&gt;Invalid login&lt;/code&gt; error as before. I nearly lost my mind.&lt;/p&gt;

&lt;p&gt;What was happening: Gmail's security systems saw a login attempt from a cloud server in a different country from my usual location and blocked it. The fix was weirdly simple, I visited &lt;a href="https://accounts.google.com/DisplayUnlockCaptcha" rel="noopener noreferrer"&gt;accounts.google.com/DisplayUnlockCaptcha&lt;/a&gt; while signed into my Google account, clicked Allow, and then the server could connect fine.&lt;/p&gt;

&lt;p&gt;I tried to set the &lt;code&gt;from&lt;/code&gt; field to &lt;code&gt;noreply@myapp.com&lt;/code&gt; to look more professional. Gmail silently replaced it with my actual &lt;code&gt;@gmail.com&lt;/code&gt; address on every single email. You cannot send as a custom &lt;code&gt;from&lt;/code&gt; address via Gmail SMTP on a personal account. Your emails will always come from your Gmail.&lt;/p&gt;




&lt;h2&gt;
  
  
  But Then I Read the Fine Print
&lt;/h2&gt;

&lt;p&gt;After the relief wore off, I went back and read Google's own documentation more carefully. And there it was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"App passwords are not recommended and are unnecessary in most cases."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Google isn't planning to keep App Passwords around forever. They're actively pushing developers toward OAuth 2.0, and at some point App Passwords will likely be retired with no concrete date given.&lt;/p&gt;

&lt;p&gt;For my side project at 3 AM, App Passwords were the right call to get unblocked. But I knew I needed to come back and do this properly. So the next day, I set up OAuth 2.0.&lt;/p&gt;




&lt;h2&gt;
  
  
  Method 2: OAuth 2.0
&lt;/h2&gt;

&lt;p&gt;OAuth 2.0 takes maybe 20 minutes to set up the first time, but it's Google's actual recommended path and won't suddenly break when they pull the plug on App Passwords. Instead of storing a password, you go through a one-time authorization flow and get a &lt;strong&gt;refresh token&lt;/strong&gt;. Nodemailer uses this to generate short-lived access tokens automatically.&lt;/p&gt;

&lt;p&gt;You'll need one extra package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;googleapis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1 : Create a Google Cloud Project
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;console.cloud.google.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Select a project&lt;/strong&gt; → &lt;strong&gt;New Project&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name it something like &lt;code&gt;my-app-email&lt;/code&gt; and hit &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Make sure it's selected in the top bar before continuing&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2 : Enable the Gmail API
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the left menu go to &lt;strong&gt;APIs &amp;amp; Services&lt;/strong&gt; → &lt;strong&gt;Library&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Search for &lt;strong&gt;Gmail API&lt;/strong&gt;, click it, and hit &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3 : Configure the OAuth Consent Screen
&lt;/h3&gt;

&lt;p&gt;This part tripped me up the first time because the UI is a bit overwhelming. Here's what actually matters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;APIs &amp;amp; Services&lt;/strong&gt; → &lt;strong&gt;OAuth consent screen&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;External&lt;/strong&gt;, click &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Fill in App name, support email, and developer contact email.&lt;/li&gt;
&lt;li&gt;On the &lt;strong&gt;Scopes&lt;/strong&gt; step, click &lt;strong&gt;Add or Remove Scopes&lt;/strong&gt;, manually type &lt;code&gt;https://mail.google.com/&lt;/code&gt; into the field, click &lt;strong&gt;Add to Table&lt;/strong&gt;, then &lt;strong&gt;Update&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;On the &lt;strong&gt;Test users&lt;/strong&gt; step, this is the part I initially skipped and then wondered why nothing worked. &lt;strong&gt;add your own Gmail address here&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Save and continue through to the end&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 4 : Create OAuth Credentials
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;APIs &amp;amp; Services&lt;/strong&gt; → &lt;strong&gt;Credentials&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+ Create Credentials&lt;/strong&gt; → &lt;strong&gt;OAuth client ID&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Web application&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Authorized redirect URIs&lt;/strong&gt; add exactly this: &lt;code&gt;https://developers.google.com/oauthplayground&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt; and copy your &lt;strong&gt;Client ID&lt;/strong&gt; and &lt;strong&gt;Client Secret&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 5 : Get Your Refresh Token via OAuth Playground
&lt;/h3&gt;

&lt;p&gt;This is the clever bit. Google has a tool called OAuth Playground that lets you authorize scopes and grab tokens without writing any extra code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://developers.google.com/oauthplayground" rel="noopener noreferrer"&gt;developers.google.com/oauthplayground&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click the ⚙️ &lt;strong&gt;gear icon&lt;/strong&gt; in the top right&lt;/li&gt;
&lt;li&gt;Check &lt;strong&gt;"Use your own OAuth credentials"&lt;/strong&gt; and enter your Client ID and Client Secret&lt;/li&gt;
&lt;li&gt;In the left panel, scroll to &lt;strong&gt;Gmail API v1&lt;/strong&gt; and select &lt;code&gt;https://mail.google.com/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Authorize APIs&lt;/strong&gt; → sign in with your Gmail account → allow access&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Exchange authorization code for tokens&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;Refresh Token&lt;/strong&gt; from the response&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important thing I learned the hard way:&lt;/strong&gt; While your app is in "Testing" mode on Google Cloud Console, refresh tokens expire after 7 days. You'll need to regenerate them periodically. For a personal or internal tool that's totally manageable just set a calendar reminder. To get permanent tokens, you'd need to publish the app, which for sensitive scopes like Gmail requires a Google review.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 6 : Update &lt;code&gt;.env&lt;/code&gt; and Write the Code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GMAIL_USER=me@gmail.com
GOOGLE_CLIENT_ID=your_client_id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your_client_secret
GOOGLE_REFRESH_TOKEN=your_refresh_token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodemailer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nodemailer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;googleapis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OAuth2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OAuth2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createTransporter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;oauth2Client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OAuth2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://developers.google.com/oauthplayground&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;oauth2Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCredentials&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_REFRESH_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;oauth2Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to get access token: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;nodemailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gmail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OAuth2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GMAIL_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_REFRESH_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createTransporter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;transporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Sanity check before sending&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Connected to Gmail ✓&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;transporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`"My App" &amp;lt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GMAIL_USER&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recipient@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello via OAuth 2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;Sent properly this time, with &amp;lt;strong&amp;gt;OAuth 2.0&amp;lt;/strong&amp;gt;.&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email sent:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Which One Should You Use?
&lt;/h2&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;App Password&lt;/th&gt; &lt;th&gt;OAuth 2.0&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;&lt;strong&gt;Setup time&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;~5 minutes&lt;/td&gt; &lt;td&gt;~20 minutes&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;&lt;strong&gt;Works in 2026?&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;✅ Yes&lt;/td&gt; &lt;td&gt;✅ Yes&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;&lt;strong&gt;Future-proof?&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;⚠️ Uncertain&lt;/td&gt; &lt;td&gt;✅ Google-recommended&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;&lt;strong&gt;Token expiry&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;Never&lt;/td&gt; &lt;td&gt;7 days (Testing mode)&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;Dev / quick prototypes&lt;/td&gt; &lt;td&gt;Anything in production&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My rule: App Password to get something working locally, OAuth 2.0 before anyone else touches it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Don't Forget This Before You Push to Git
&lt;/h2&gt;

&lt;p&gt;The first time I set all this up I nearly committed my &lt;code&gt;.env&lt;/code&gt; file. Do this immediately after creating it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your credentials ever get exposed, here's how to recover fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App Password&lt;/strong&gt; - Delete it at &lt;a href="https://myaccount.google.com/apppasswords" rel="noopener noreferrer"&gt;myaccount.google.com/apppasswords&lt;/a&gt; and generate a fresh one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth tokens&lt;/strong&gt; - Revoke them at &lt;a href="https://myaccount.google.com/permissions" rel="noopener noreferrer"&gt;myaccount.google.com/permissions&lt;/a&gt; and regenerate via OAuth Playground&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Know When to Move On From Gmail
&lt;/h2&gt;

&lt;p&gt;Gmail is brilliant for getting off the ground, but it has real ceilings around &lt;strong&gt;500 emails/day&lt;/strong&gt; on a free account. Each recipient counts individually, so one email to 5 people eats 5 of your daily quota. Hit the limit and you'll see SMTP error &lt;code&gt;454 4.7.0&lt;/code&gt; until the quota resets the next day.&lt;/p&gt;

&lt;p&gt;If you find yourself hitting that ceiling, or you need a custom &lt;code&gt;from&lt;/code&gt; address like &lt;code&gt;hello@yourapp.com&lt;/code&gt;, or you want delivery analytics, it's time to look at &lt;strong&gt;Resend&lt;/strong&gt;, &lt;strong&gt;Postmark&lt;/strong&gt;, or &lt;strong&gt;Brevo&lt;/strong&gt;. All have free tiers that are genuinely useful for indie hackers and small teams, and they won't quietly block your cloud server IP.&lt;/p&gt;




&lt;p&gt;Hope this saves you the 3 AM spiral. Drop a comment if you get stuck on any step I've probably hit that exact wall too.&lt;/p&gt;




</description>
      <category>node</category>
      <category>javascript</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
    <item>
      <title>Which Local LLM is Better? A Deep Dive into Open-Source AI Models in 2026 (Benchmarked)</title>
      <dc:creator>Likhit Kumar V P</dc:creator>
      <pubDate>Sat, 14 Feb 2026 12:37:47 +0000</pubDate>
      <link>https://dev.to/likhit/which-local-llm-is-better-a-deep-dive-into-open-source-ai-models-in-2026-benchmarked-1ni</link>
      <guid>https://dev.to/likhit/which-local-llm-is-better-a-deep-dive-into-open-source-ai-models-in-2026-benchmarked-1ni</guid>
      <description>&lt;p&gt;Here's the problem: Everyone claims their model is "The Best." No one tells you which specific model to use for which task. I've analyzed every major open-source LLM benchmark from February 2026 to answer one question: &lt;strong&gt;Which free AI model actually wins for your specific use case?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This isn't about vague claims. This is about hard data from SWE-bench (real GitHub issues), AIME 2025 (olympiad math), and agent benchmarks. Let me show you which open-source alternatives to ChatGPT and Claude actually work.&lt;/p&gt;

&lt;p&gt; - -&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "Best LLM" Is the Wrong Question
&lt;/h2&gt;

&lt;p&gt;Here's what no one tells you: &lt;strong&gt;there is no single "best" AI model&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A model that dominates coding benchmarks often fails at math. One that excels at tool use might struggle with pure reasoning. This is why you need to match the local LLM to your specific task.&lt;/p&gt;

&lt;p&gt;I've broken down the top open-source language models into three categories based on February 2026 benchmarks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Coding &amp;amp; Software Engineering&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reasoning&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agentic Workflows &amp;amp; Tool Use&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's see which free AI models win with proof.&lt;/p&gt;

&lt;p&gt; - -&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Open-Source LLM for Coding: The Competition
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Benchmark: SWE-bench Verified (Real Software Engineering)
&lt;/h3&gt;

&lt;p&gt;Forget "write a hello world function." SWE-bench Verified tests 500 real GitHub issues from production Python repositories. The AI model must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read the bug report&lt;/li&gt;
&lt;li&gt;Navigate the codebase&lt;/li&gt;
&lt;li&gt;Generate a working patch&lt;/li&gt;
&lt;li&gt;Pass all existing tests
This measures actual software engineering capability, not toy problems.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SWE-bench Verified Leaderboard (February 2026):
✓ Proprietary Models:
1. Claude Opus 4.5: 80.9%
2. Claude Opus 4.6: 80.8%
3. GPT-5.2: 80.0%
⭐ Open-Source Models:
4. Kimi K2.5: 76.8% ← HIGHEST OPEN-SOURCE
5. GLM-4.7: 73.8%
6. DeepSeek V3.2: 73.1%
7. Qwen3-Coder-Next: 70.6%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt; - -&lt;/p&gt;

&lt;h3&gt;
  
  
  Kimi K2.5 (Open-Weights)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Score: 76.8% on SWE-bench Verified - Highest Open-Source Score&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Kimi K2.5 Leads on Coding Benchmarks:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kimi K2.5, released January 27, 2026, achieves the highest open-source score on SWE-bench Verified at 76.8%. It's particularly strong at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visual-to-code generation (convert designs/screenshots to functional code)&lt;/li&gt;
&lt;li&gt;Front-end development with animations and interactivity&lt;/li&gt;
&lt;li&gt;Multi-step debugging workflows&lt;/li&gt;
&lt;li&gt;Terminal-based development tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Technical Specs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 trillion parameters (32B active per token)&lt;/li&gt;
&lt;li&gt;Native multimodal (text, images, video)&lt;/li&gt;
&lt;li&gt;256K context window&lt;/li&gt;
&lt;li&gt;Uses INT4 quantization natively&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License: MIT with commercial restrictions&lt;/strong&gt; (free for companies with under 100M monthly active users)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Additional Coding Benchmarks:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Kimi K2.5 Performance:
- SWE-bench Verified: 76.8% ← HIGHEST
- SWE-bench Multilingual: 73.0%
- LiveCodeBench v6: 85.0%
- Terminal-Bench 2.0: 40.45%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Special Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent Swarm&lt;/strong&gt;: Coordinates up to 100 specialized sub-agents for parallel task execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual Coding&lt;/strong&gt;: Converts images/videos into functional code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kimi Code&lt;/strong&gt;: Open-source terminal tool (rival to Claude Code)&lt;/li&gt;
&lt;li&gt;Four modes: Instant, Thinking, Agent, Agent Swarm (beta)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hardware Requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With native INT4: ~240GB VRAM minimum&lt;/li&gt;
&lt;li&gt;Practical: Cloud GPU rental or API access&lt;/li&gt;
&lt;li&gt;Speed: 44 tokens/second via API&lt;/li&gt;
&lt;li&gt;Cost: Competitive pricing with free tier available&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Important Note:&lt;/strong&gt; Kimi K2.5 uses MIT license with commercial restrictions. Companies with over 100 million monthly active users require special licensing. For most users and businesses, this is fully open-source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to Use Kimi K2.5:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Converting UI designs to code&lt;/li&gt;
&lt;li&gt;Front-end development with complex animations&lt;/li&gt;
&lt;li&gt;Multi-modal coding (working with images/videos)&lt;/li&gt;
&lt;li&gt;Agentic coding workflows requiring tool coordination&lt;/li&gt;
&lt;li&gt;Projects where visual understanding matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; - -&lt;/p&gt;

&lt;h3&gt;
  
  
  DeepSeek V3.2 (Open-Source)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Score: 73.0% on SWE-bench Verified&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why DeepSeek V3.2 Is Strong for Coding:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DeepSeek V3.2 (the current version as of February 2026) achieves one of the highest scores among open-source AI models on the industry-standard SWE-bench. Only 7–8% behind proprietary models like Claude Opus 4.5 (80.9%).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SWE-bench Verified Leaderboard (February 2026):
✓ Proprietary Models:
1. Claude Opus 4.5: 80.9%
2. Claude Opus 4.6: 80.8%
3. GPT-5.2: 80.0%
⭐ Open-Source Models:
4. Kimi K2.5: 76.8% ← HIGHEST OPEN-SOURCE
5. GLM-4.7: 73.8%
6. DeepSeek V3.2: 73.1%
7. Qwen3-Coder-Next: 70.6%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Technical Specs (DeepSeek V3.2):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;671 billion parameters (37B active per token)&lt;/li&gt;
&lt;li&gt;Mixture-of-Experts (MoE) architecture&lt;/li&gt;
&lt;li&gt;128K context window&lt;/li&gt;
&lt;li&gt;Trained on 14.8 trillion tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License: MIT&lt;/strong&gt; (fully free, commercial use allowed)&lt;/li&gt;
&lt;li&gt;Cost: ~$0.27–0.55 per million tokens (API)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hardware Requirements for Self-Hosting:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;336GB VRAM with 4-bit quantization&lt;/li&gt;
&lt;li&gt;Requires 4–5x NVIDIA H100 or H200 GPUs&lt;/li&gt;
&lt;li&gt;Practical reality: Most users access via API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-World Performance:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automated bug fixing: Excellent&lt;/li&gt;
&lt;li&gt;Code review and refactoring: Strong&lt;/li&gt;
&lt;li&gt;Multi-file modifications: Best-in-class for open source&lt;/li&gt;
&lt;li&gt;API latency: 20–40 tokens/second
 - -&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  GLM-4.7 - Best for AI Coding Agents
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Score: 73.8% on SWE-bench Verified&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GLM-4.7 technically scores 0.8% higher than DeepSeek V3.2, but this comes with a caveat: the score may include enhanced scaffolding or agentic frameworks. For direct model comparisons, DeepSeek V3.2 is more consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;However, GLM-4.7 has a killer feature: it runs on consumer hardware.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Choose GLM-4.7:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MIT License&lt;/strong&gt; (fully open-source)&lt;/li&gt;
&lt;li&gt;Runs on single RTX 4090 (24GB VRAM) using GLM-4.7-Flash variant&lt;/li&gt;
&lt;li&gt;Designed specifically for agentic coding (Claude Code, Cursor, Cline)&lt;/li&gt;
&lt;li&gt;"Preserved Thinking" architecture maintains reasoning across turns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Technical Specs (GLM-4.7-Flash):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;30B total parameters, 3B active (efficient!)&lt;/li&gt;
&lt;li&gt;128K context window&lt;/li&gt;
&lt;li&gt;Native tool calling&lt;/li&gt;
&lt;li&gt;Speed: 25–35 tokens/second on consumer GPU&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Additional Coding Benchmarks:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GLM-4.7 Performance:
- SWE-bench Multilingual: 66.7%
- Terminal-Bench 2.0: 41.0%
- LiveCodeBench: 84.9%
- Agent tool use (τ²-Bench): 87.4%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to Choose GLM-4.7 Over DeepSeek V3.2:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have consumer hardware (24GB GPU)&lt;/li&gt;
&lt;li&gt;You're building AI coding agents&lt;/li&gt;
&lt;li&gt;You need local inference without cloud dependency&lt;/li&gt;
&lt;li&gt;You want multi-turn coding sessions with context retention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; - -&lt;/p&gt;

&lt;h2&gt;
  
  
  Reasoning: Mathematical and Scientific Intelligence
&lt;/h2&gt;

&lt;p&gt;Reasoning isn't a single capability. It breaks down into distinct subcategories that test different cognitive abilities. Let's examine how open-source LLMs perform across mathematical and scientific domains.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subcategory: Mathematical Reasoning (AIME 2025 Benchmark)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Benchmark:&lt;/strong&gt; AIME 2025–30 problems from the American Invitational Mathematics Examination. These are competition-level math problems requiring multiple reasoning steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Data (from Artificial Analysis Intelligence Index):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AIME 2025 Leaderboard (February 2026):
✓ Proprietary Models:
1. GPT-5.2: 99.0%
2. Gemini 2.0 Flash Thinking: 97.0%
3. Gemini 2.0 Pro Thinking: 95.7%
⭐ Open-Source Models:
7. GLM-4.7: 95.7% ← TOP OPEN-SOURCE
8. DeepSeek V3.2: 93.1%
9. Qwen2.5-Max: 92.3%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GLM-4.7 (Open-Source) - Mathematical Reasoning Leader
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Score: 95.7% on AIME 2025&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why It Leads:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Highest verified open-source score on AIME 2025&lt;/li&gt;
&lt;li&gt;Matches proprietary Gemini 2.0 Pro Thinking at 95.7%&lt;/li&gt;
&lt;li&gt;Strong mathematical reasoning architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mathematical proof generation&lt;/li&gt;
&lt;li&gt;Physics problem solving&lt;/li&gt;
&lt;li&gt;Quantitative finance modeling&lt;/li&gt;
&lt;li&gt;STEM education applications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; - -&lt;/p&gt;

&lt;h3&gt;
  
  
  DeepSeek V3.2 (Open-Source) - Strong Math Performance
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Score: 93.1% on AIME 2025&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DeepSeek V3.2 achieves 93.1% on AIME 2025, placing it just behind GLM-4.7's 95.7% but still in frontier territory for open-source models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical Specs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;671B parameters (37B active via MoE)&lt;/li&gt;
&lt;li&gt;Thinking mode available&lt;/li&gt;
&lt;li&gt;MIT License&lt;/li&gt;
&lt;li&gt;Hardware: Requires cloud GPUs or API access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This is significant:&lt;/strong&gt; Near-frontier math performance with full MIT licensing and strong versatility across all benchmark categories.&lt;/p&gt;

&lt;p&gt; - -&lt;/p&gt;

&lt;h3&gt;
  
  
  Qwen2.5-Max (Open-Source) - Consumer-Friendly Math Option
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Score: 92.3% on AIME 2025&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Strong math performance with more accessible hardware requirements than DeepSeek.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical Specs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trillion-scale MoE architecture&lt;/li&gt;
&lt;li&gt;Apache 2.0 License&lt;/li&gt;
&lt;li&gt;Supports 119 languages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; - -&lt;/p&gt;

&lt;h3&gt;
  
  
  Subcategory: Scientific Reasoning (GPQA Diamond)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Benchmark:&lt;/strong&gt; GPQA Diamond - 198 PhD-level questions in physics, biology, chemistry. Designed to be "Google-proof" (even experts with web access only score 65–70%).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest Assessment:&lt;/strong&gt; Open-source models lag behind proprietary models by 2–4% in this category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Open-Source Performance:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GPQA Diamond Scores (February 2026):
✓ Proprietary Models:
1. Gemini 3 Pro: 90.8%
2. GPT-5.2: 90.3%
⭐ Open-Source Models:
1. GLM-4.7: 85.7%
2. DeepSeek V3.2: ~85–88% (estimated)
3. Qwen3 variants: ~84–87%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GLM-4.7 (Open-Source) - Best Available for Scientific Reasoning
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Score: 85.7% on GPQA Diamond&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GLM-4.7 leads open-source models on PhD-level scientific reasoning, though proprietary models maintain a 4–5% advantage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Reality:&lt;/strong&gt; For PhD-level scientific research requiring the absolute highest accuracy, proprietary models (Gemini 3 Pro, GPT-5) currently have an edge. However, for most scientific applications, the 4–5% gap isn't critical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When Open-Source Works Well:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;General scientific questions (undergraduate/Master's level)&lt;/li&gt;
&lt;li&gt;Scientific coding and data analysis&lt;/li&gt;
&lt;li&gt;Literature review and synthesis&lt;/li&gt;
&lt;li&gt;Research assistance (non-critical calculations)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to Consider Proprietary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High-stakes research decisions&lt;/li&gt;
&lt;li&gt;PhD dissertation-level work&lt;/li&gt;
&lt;li&gt;Peer-reviewed publication support&lt;/li&gt;
&lt;li&gt;Breakthrough discovery verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; - -&lt;/p&gt;

&lt;h3&gt;
  
  
  Subcategory: General Reasoning (MMLU, HLE)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Benchmarks:&lt;/strong&gt; MMLU (general knowledge across 57 subjects), HLE (Humanity's Last Exam - multi-domain expert knowledge)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Top Open-Source Models:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;General Reasoning Performance (February 2026):
1. DeepSeek V3.2: Strong across MMLU and expert domains
2. Qwen2.5-Max: MMLU: 84–86%
3. Kimi K2.5: HLE: 50.2% with tools (highest reported)
4. GLM-4.7: HLE: 42.8% with tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  DeepSeek V3.2 (Open-Source) - Most Well-Rounded Reasoner
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;MMLU and Other General Benchmarks: Competitive with Claude 3.5 Sonnet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DeepSeek V3.2 maintains strong general reasoning across diverse benchmarks, making it the most well-rounded open-source AI model for reasoning tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why It's Versatile:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consistent performance across 57 MMLU subjects&lt;/li&gt;
&lt;li&gt;Strong on both academic and practical knowledge&lt;/li&gt;
&lt;li&gt;Reliable for general-purpose reasoning applications
 - -&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Summary: Reasoning Category Winners
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Mathematical Reasoning:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Champion&lt;/strong&gt;: GLM-4.7 (95.7% AIME) - MIT License&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong Alternative&lt;/strong&gt;: DeepSeek V3.2 (93.1% AIME) - MIT License&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multilingual Option&lt;/strong&gt;: Qwen2.5-Max (92.3% AIME) - Apache 2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scientific Reasoning:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Open-Source&lt;/strong&gt;: GLM-4.7 (85.7% GPQA Diamond)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reality Check&lt;/strong&gt;: Proprietary models lead by 4–5%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;General Reasoning:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Most Versatile&lt;/strong&gt;: DeepSeek V3.2 (strong across all domains)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool-Augmented&lt;/strong&gt;: Kimi K2.5 (50.2% HLE with tools)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; - -&lt;/p&gt;

&lt;h2&gt;
  
  
  Agentic Workflows &amp;amp; Tool Use
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Benchmark: τ²-Bench (Agent Coordination)
&lt;/h3&gt;

&lt;p&gt;This benchmark tests how well AI models guide users through complex troubleshooting while coordinating tool usage in dual-control environments (both agent and user have tools).&lt;br&gt;
Most AI models that dominate coding collapse here. This tests real-world agentic capability.&lt;/p&gt;
&lt;h3&gt;
  
  
  GLM-4.7 (Open-Source) - Agentic Workflows Leader
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Score: 87.4% on τ²-Bench&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why It Wins:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Highest verified open-source score&lt;/strong&gt; on τ²-Bench&lt;/li&gt;
&lt;li&gt;Beats many proprietary models on agent coordination&lt;/li&gt;
&lt;li&gt;Designed specifically for agentic, tool-heavy workflows&lt;/li&gt;
&lt;li&gt;Runs on consumer hardware (16–18GB VRAM)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Verified Agent Benchmarks:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GLM-4.7 Agent Performance:
- τ²-Bench: 87.4% ← OPEN-SOURCE LEADER
- BrowseComp: 67.0 (web task evaluation)
- Terminal-Bench 2.0: 41.0%
- LiveCodeBench: 84.9%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why This Matters for AI Agents:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agentic workflows are where AI coding assistants (Claude Code, Cursor, Cline, Continue) operate. Strong tool use means the model can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Call APIs correctly&lt;/li&gt;
&lt;li&gt;Use search when needed&lt;/li&gt;
&lt;li&gt;Navigate file systems&lt;/li&gt;
&lt;li&gt;Execute terminal commands&lt;/li&gt;
&lt;li&gt;Coordinate multi-step tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Technical Specs (GLM-4.7-Flash):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;30B total, 3B active parameters&lt;/li&gt;
&lt;li&gt;128K context window&lt;/li&gt;
&lt;li&gt;Native tool calling support&lt;/li&gt;
&lt;li&gt;MIT License&lt;/li&gt;
&lt;li&gt;Hardware: 16–18GB VRAM (RTX 4090)&lt;/li&gt;
&lt;li&gt;Speed: 25–35 tokens/second&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to Use:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building AI coding assistants&lt;/li&gt;
&lt;li&gt;Customer service automation&lt;/li&gt;
&lt;li&gt;DevOps automation&lt;/li&gt;
&lt;li&gt;Multi-tool workflows&lt;/li&gt;
&lt;li&gt;Any task requiring extended agent coordination&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; - -&lt;/p&gt;

&lt;h2&gt;
  
  
  Category Winners: Quick Reference Table
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjr8s9asfd4vftq50i5rf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjr8s9asfd4vftq50i5rf.png" alt=" " width="768" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Choose the Right Open-Source LLM: Decision Tree
&lt;/h2&gt;

&lt;h3&gt;
  
  
  START: What's Your Primary Use Case?
&lt;/h3&gt;

&lt;h4&gt;
  
  
  If CODING:
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Have multiple H100 GPUs or API budget?&lt;/strong&gt; → &lt;strong&gt;DeepSeek V3.2&lt;/strong&gt; (73.1% SWE-bench, MIT license, $0.27/M tokens API)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want highest open-source performance?&lt;/strong&gt; → &lt;strong&gt;Kimi K2.5&lt;/strong&gt; (76.8% SWE-bench, visual coding capabilities)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have single RTX 4090 (24GB)?&lt;/strong&gt; → &lt;strong&gt;Qwen3-Coder-Next&lt;/strong&gt; (70.6%, runs locally, Apache 2.0)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building AI coding agents (Cursor, Cline)?&lt;/strong&gt; → &lt;strong&gt;GLM-4.7&lt;/strong&gt; (87.4% agent benchmark, 16GB VRAM, MIT)&lt;/p&gt;

&lt;h4&gt;
  
  
  If MATH/REASONING:
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Need highest accuracy?&lt;/strong&gt; → &lt;strong&gt;GLM-4.7&lt;/strong&gt; (95.7% AIME, MIT license)&lt;br&gt;
&lt;strong&gt;Want versatility + math?&lt;/strong&gt; → &lt;strong&gt;DeepSeek V3.2&lt;/strong&gt; (93.1% AIME, strong general reasoning, MIT)&lt;br&gt;
&lt;strong&gt;Need multilingual support?&lt;/strong&gt; → &lt;strong&gt;Qwen2.5-Max&lt;/strong&gt; (92.3% AIME, 119 languages, Apache 2.0)&lt;/p&gt;

&lt;h4&gt;
  
  
  If AGENTIC/TOOLS:
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;For AI agents and automation:&lt;/strong&gt; → &lt;strong&gt;GLM-4.7&lt;/strong&gt; (87.4% τ²-Bench, 16GB VRAM, MIT)&lt;/p&gt;

&lt;p&gt; - -&lt;/p&gt;

&lt;h2&gt;
  
  
  License Verification: Are These Really Open-Source?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fully Open-Source (Commercial Use Allowed):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DeepSeek V3.2&lt;/strong&gt;: MIT License - No restrictions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GLM-4.7&lt;/strong&gt;: MIT License - No restrictions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3-Coder-Next&lt;/strong&gt;: Apache 2.0 - Attribution required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Open-Source with Commercial Restrictions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kimi K2.5&lt;/strong&gt;: MIT License - Companies with 100M+ monthly active users require special licensing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Important Notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All licenses verified from official GitHub/Hugging Face repositories&lt;/li&gt;
&lt;li&gt;MIT is most permissive (no attribution needed)&lt;/li&gt;
&lt;li&gt;Apache 2.0 requires attribution but allows modification&lt;/li&gt;
&lt;li&gt;Kimi K2.5 is effectively fully open-source for the vast majority of users and companies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; - -&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Recommendations: Best Open-Source LLM for You
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For Most Developers (February 2026):
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Kimi K2.5 (Highest Coding Performance)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Highest open-source coding score (76.8% SWE-bench)&lt;/li&gt;
&lt;li&gt;Exceptional visual-to-code capabilities&lt;/li&gt;
&lt;li&gt;Agent Swarm for complex workflows&lt;/li&gt;
&lt;li&gt;MIT license (with 100M MAU restriction)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best choice for cutting-edge coding performance&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option 2: GLM-4.7 (Best All-Rounder for Consumer Hardware)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strong coding (73.8% SWE-bench)&lt;/li&gt;
&lt;li&gt;Best math reasoning (95.7% AIME)&lt;/li&gt;
&lt;li&gt;Best agentic workflows (87.4% τ²-Bench)&lt;/li&gt;
&lt;li&gt;Runs on single RTX 4090 (24GB VRAM)&lt;/li&gt;
&lt;li&gt;MIT license&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best choice if you have consumer GPU&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option 3: DeepSeek V3.2 (Most Well-Rounded)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Excellent coding (73.1%)&lt;/li&gt;
&lt;li&gt;Strong math (93.1%)&lt;/li&gt;
&lt;li&gt;Best general reasoning&lt;/li&gt;
&lt;li&gt;MIT license, API available&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best choice for versatility across tasks&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option 4: Qwen3-Coder-Next (Efficiency Champion)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Great efficiency (70.6% with only 3B active)&lt;/li&gt;
&lt;li&gt;Runs on single RTX 4090&lt;/li&gt;
&lt;li&gt;Apache 2.0 license&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best choice if hardware-limited&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Strategic Approach:
&lt;/h3&gt;

&lt;p&gt;Many professional developers use a &lt;strong&gt;hybrid strategy&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open-source models&lt;/strong&gt; for development, testing, and most tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proprietary models&lt;/strong&gt; (Claude/GPT) for critical production features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you the best of both worlds: freedom and control with open-source, reliability where it matters most.&lt;/p&gt;

&lt;p&gt; - -&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About This Analysis:&lt;/strong&gt; All benchmark data from Artificial Analysis Intelligence Index (AIME 2025), SWE-bench.com official leaderboards, τ²-Bench documentation, and verified model release announcements from DeepSeek, Zhipu AI, and Alibaba Cloud. Hardware requirements from official specifications and community testing. All licenses verified from GitHub/Hugging Face. Information current as of February 14, 2026.&lt;br&gt;
 - -&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Automating LeetCode to GitHub: I Built a Chrome Extension So You Never Lose Your Solutions Again</title>
      <dc:creator>Likhit Kumar V P</dc:creator>
      <pubDate>Tue, 10 Feb 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/likhit/automating-leetcode-to-github-i-built-a-chrome-extension-so-you-never-lose-your-solutions-again-3j1l</link>
      <guid>https://dev.to/likhit/automating-leetcode-to-github-i-built-a-chrome-extension-so-you-never-lose-your-solutions-again-3j1l</guid>
      <description>&lt;p&gt;Have you ever solved a LeetCode problem, felt proud of your solution, and then... forgot to save it to GitHub? Or maybe you have dozens of solutions scattered across different files with no organization?&lt;/p&gt;

&lt;p&gt;I got tired of manually copying my LeetCode solutions to GitHub after every "Accepted" verdict. So I built a Chrome extension that does it automatically. It's made my coding practice way more organized and saved me hours of tedious work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;As developers grinding LeetCode, we face a few annoying challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manual work is tedious&lt;/strong&gt;: Copy code → Open GitHub → Create file → Paste → Commit → Push. Every. Single. Time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No organization&lt;/strong&gt;: Files named randomly, no structure, difficult to find solutions later&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple approaches&lt;/strong&gt;: Solved the same problem differently? No easy way to compare solutions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something that would just... work. Hit "Submit", get "Accepted", and  solution automatically in GitHub with proper formatting and metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: LeetCode GitHub Sync Extension
&lt;/h2&gt;

&lt;p&gt;I built a Manifest V3 Chrome extension that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detects when you get "Accepted" on LeetCode&lt;/li&gt;
&lt;li&gt;Extracts your solution code automatically&lt;/li&gt;
&lt;li&gt;Pushes it to your GitHub repository&lt;/li&gt;
&lt;li&gt;Organizes files by difficulty (Easy/Medium/Hard)&lt;/li&gt;
&lt;li&gt;Supports multiple solutions for the same problem (Solution A, B, C...)&lt;/li&gt;
&lt;li&gt;Includes rich metadata (problem URL, difficulty, timestamps)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the best part? It has a floating ⚡ button as a reliable backup when auto-detection doesn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture &amp;amp; Tech Stack
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manifest V3&lt;/strong&gt; (Chrome's latest extension standard)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub REST API&lt;/strong&gt; (for file creation/updates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monaco Editor&lt;/strong&gt; interaction (for code extraction)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome Storage API&lt;/strong&gt; (for settings persistence)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vanilla JavaScript&lt;/strong&gt; (no frameworks needed!)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Key Components
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;leetcode-github-sync/
├── manifest.json       # Extension config (Manifest V3)
├── content.js         # Page interaction &amp;amp; detection
├── background.js      # GitHub API integration
├── popup.html/js      # Settings UI
└── debug-helper.js    # Diagnostic tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Detection System&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The extension watches for when you get "Accepted" on LeetCode using multiple methods. Since LeetCode updates their UI frequently, I built in several fallback approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DOM Mutation Observer&lt;/strong&gt; - Watches for new elements being added to the page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS Class Selectors&lt;/strong&gt; - Looks for success/green/accepted classes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Periodic Checking&lt;/strong&gt; - Checks every 3 seconds as a backup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text Content Scanning&lt;/strong&gt; - Searches for "Accepted" text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If auto-detection fails, there's always the ⚡ floating button you can click manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Code Extraction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LeetCode uses Monaco Editor (the same one VS Code uses). Getting code out of it requires trying multiple approaches since the editor's internal structure can vary:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Try the Monaco API methods (&lt;code&gt;getEditors()&lt;/code&gt;, &lt;code&gt;getModels()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Extract from DOM elements (&lt;code&gt;.view-line&lt;/code&gt; classes)&lt;/li&gt;
&lt;li&gt;Use alternative Monaco model APIs&lt;/li&gt;
&lt;li&gt;Fall back to manual sync button if all else fails&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The extension tries each method in sequence until one works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. GitHub Integration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The background script handles all communication with GitHub's API. When you get "Accepted", the content script sends a message to the background script with your code, problem details, and language. The background script then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checks if the file already exists&lt;/li&gt;
&lt;li&gt;Decides whether to create a new file or append to existing&lt;/li&gt;
&lt;li&gt;Formats your solution with metadata&lt;/li&gt;
&lt;li&gt;Pushes to GitHub using their REST API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;4. Multiple Solutions Support&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you solve the same problem again, the extension detects existing solution markers in your file and automatically assigns the next letter (B, C, D, etc.). It can optionally ask for your approval before appending, so you stay in control.&lt;/p&gt;

&lt;h3&gt;
  
  
  File Structure Example
&lt;/h3&gt;

&lt;p&gt;Your solutions get organized beautifully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;leetcode-solutions/
├── easy/
│   └── 9-palindrome-number.py
├── medium/
│   └── 2-add-two-numbers.cpp
└── hard/
    └── 4-median-of-two-sorted-arrays.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each file contains rich metadata:&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="c1"&gt;# 9. Palindrome Number
# Difficulty: Easy
# URL: https://leetcode.com/problems/palindrome-number/
# Date: 2/10/2026
&lt;/span&gt;
&lt;span class="c1"&gt;# ========== Solution A ==========
# Language: python3
# Date: 2/10/2026
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Solution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;isPalindrome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)[::&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# ========== Solution B ==========
# Language: python3
# Date: 2/11/2026
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Solution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;isPalindrome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;x&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="n"&gt;reversed_half&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;reversed_half&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;reversed_half&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reversed_half&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
            &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;//=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;reversed_half&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;reversed_half&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Challenges &amp;amp; Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenge 1: LeetCode UI Changes Frequently
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Selectors break when LeetCode updates their UI&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Multiple detection methods acting as fallbacks. The ⚡ floating button provides a reliable manual sync option that always works. Instead of relying on one way to detect "Accepted", the extension tries several different approaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 2: Code Extraction Reliability
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Monaco Editor API isn't always accessible&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: 5 different extraction methods, tried in sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;monaco.editor.getEditors()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;monaco.editor.getModels()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;DOM extraction from &lt;code&gt;.view-line&lt;/code&gt; elements&lt;/li&gt;
&lt;li&gt;Alternative Monaco model APIs&lt;/li&gt;
&lt;li&gt;Manual sync button as ultimate fallback&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Challenge 3: Manifest V3 Restrictions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Content scripts can't access Chrome storage directly&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: The extension uses message passing between the content script (running on LeetCode pages) and the background service worker (handling GitHub operations). The content script collects data, sends it to the background script, and the background script handles all the API calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 4: Handling Special Characters
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Unicode characters in code weren't uploading correctly to GitHub&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Proper Base64 encoding with UTF-8 support ensures that code with special characters, emojis, or non-ASCII text gets uploaded correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Technical Insights
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Manifest V3 is strict but better&lt;/strong&gt; - Service workers are more efficient than background pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple fallback strategies are essential&lt;/strong&gt; - When dealing with dynamic UIs like LeetCode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-controlled actions &amp;gt; Pure automation&lt;/strong&gt; - The ⚡ button gets used more than I expected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub API is straightforward&lt;/strong&gt; - The REST API is well-documented and reliable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monaco Editor is powerful&lt;/strong&gt; - But accessing it requires understanding its internal structure&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Product Insights
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Manual fallback is crucial&lt;/strong&gt; - Auto-detection fails ~20% of the time due to UI changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Users want control&lt;/strong&gt; - Approval dialog for multiple solutions is heavily used&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual feedback matters&lt;/strong&gt; - Notifications and loading states reduce user anxiety&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Organization is key&lt;/strong&gt; - Difficulty-based folders are the most popular option&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation is critical&lt;/strong&gt; - Included debug-helper.js and multiple .md files for troubleshooting&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Reflections
&lt;/h2&gt;

&lt;p&gt;Building this extension taught me that &lt;strong&gt;reliability beats perfection&lt;/strong&gt;. I initially wanted 100% automatic detection, but realized that a manual fallback (the ⚡ button) provides a better user experience than a flaky auto-detection system.&lt;/p&gt;

&lt;p&gt;The extension has saved me countless hours of manual work and made my LeetCode practice more organized. Sharing my solutions with recruiters is now as simple as sending a GitHub link.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository&lt;/strong&gt;: [[&lt;a href="https://github.com/Likhit-Kumar/Leetcode-Github-Sync%5D" rel="noopener noreferrer"&gt;https://github.com/Likhit-Kumar/Leetcode-Github-Sync]&lt;/a&gt;]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome Web Store&lt;/strong&gt;: [Coming soon]&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  💭 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;If you're grinding LeetCode, this extension can save you hours of manual work and keep your solutions organized forever. The combination of automatic syncing + reliable manual fallback has made it an essential part of my coding practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For the LeetCode community&lt;/strong&gt;: Feel free to fork, contribute, or suggest features!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags&lt;/strong&gt;: #leetcode #github #chrome-extension #automation #productivity #javascript #webdev #coding&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Questions or issues?&lt;/strong&gt; Drop a comment below!&lt;/p&gt;

</description>
      <category>automation</category>
      <category>leetcode</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Spent $47 testing OpenClaw for a week: Here's What's Actually Happening</title>
      <dc:creator>Likhit Kumar V P</dc:creator>
      <pubDate>Sun, 08 Feb 2026 17:13:08 +0000</pubDate>
      <link>https://dev.to/likhit/i-spent-47-testing-openclaw-for-a-week-heres-whats-actually-happening-4j2b</link>
      <guid>https://dev.to/likhit/i-spent-47-testing-openclaw-for-a-week-heres-whats-actually-happening-4j2b</guid>
      <description>&lt;p&gt;I Spent $47 testing OpenClaw for a week: Here's What's Actually Happening&lt;/p&gt;

&lt;p&gt;Three weeks ago, I'd never heard of OpenClaw. Last week, my feed was full of it. people calling it the future of work, others warning it's a security nightmare. The discourse was so polarized that I did what any developer would do: I set up a test environment and tried it myself.&lt;/p&gt;

&lt;p&gt;I spun up an old laptop, isolated it from my main network, and spent a week actually using OpenClaw for daily tasks. I also dove into the security reports, GitHub issues, and talked to people running it in production. What I found surprised me, not because it's revolutionary or terrible, but because it's both, depending on who's sitting at the keyboard.&lt;/p&gt;

&lt;p&gt;This isn't a hit piece. It's also not a love letter. It's what I learned after actually installing and testing this thing everyone's talking about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Actually Is
&lt;/h2&gt;

&lt;p&gt;Strip away the hype and OpenClaw is fundamentally this: &lt;strong&gt;an open-source bridge between LLMs and your computer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It runs on your own hardware and connects language models (Claude, GPT-4, DeepSeek, etc.) to your messaging apps. You text it on WhatsApp or Slack, and it executes tasks on your computer: manages your inbox, schedules meetings, browses websites, runs terminal commands, organizes files.&lt;/p&gt;

&lt;p&gt;Think of it like having a developer friend SSH'd into your machine who's always available via text message. Except it's an AI, it never sleeps, and it remembers every conversation you've ever had with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup Reality
&lt;/h2&gt;

&lt;p&gt;Here's where theory meets practice. The documentation says "installation is straightforward," and I can confirm - if you're comfortable with Node.js, Docker, and terminal commands, it is. For everyone else, not so much.&lt;/p&gt;

&lt;p&gt;My setup took about 30 minutes on an Ubuntu laptop. Here's what I had to navigate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installing Node.js version 22+ (had to upgrade from v20)&lt;/li&gt;
&lt;li&gt;Setting up Docker containers&lt;/li&gt;
&lt;li&gt;Managing API keys for Claude and GPT-4&lt;/li&gt;
&lt;li&gt;Configuring OAuth flows for WhatsApp and Telegram&lt;/li&gt;
&lt;li&gt;Setting environment variables and editing config files&lt;/li&gt;
&lt;li&gt;Understanding WSL2 requirements (if you're on Windows)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="[https://www.getopenclaw.ai/docs]"&gt;documentation&lt;/a&gt; is actually decent, but it assumes you already know what &lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;docker-compose up&lt;/code&gt;, and environment variables are. There's no Zapier-style wizard. You're reading YAML files and setting permissions manually.&lt;br&gt;
&lt;strong&gt;The marketing suggests "personal AI assistant" when the reality is "developer power tool."&lt;/strong&gt; That distinction matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Tested
&lt;/h2&gt;

&lt;p&gt;Once I got it running, I spent days using OpenClaw for real tasks. Here's what worked, what didn't, and what surprised me:&lt;/p&gt;

&lt;h3&gt;
  
  
  Email Automation (Worked Well)
&lt;/h3&gt;

&lt;p&gt;I connected it to a throwaway Gmail account with ~2,000 emails accumulated over six months. I asked it to: "Unsubscribe me from all newsletters and promotional emails, keep only transactional emails and personal correspondence."&lt;/p&gt;

&lt;p&gt;It processed everything in about 20 minutes. Out of 847 newsletter emails, it successfully unsubscribed from 203 lists and categorized the rest. I spot-checked about 50 emails - accuracy was around 90%. It missed a few legitimate emails from a client who uses a marketing platform, but overall, impressive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt;: ~$8 in API calls (using Claude Sonnet 3.5)&lt;/p&gt;

&lt;h3&gt;
  
  
  Calendar Management
&lt;/h3&gt;

&lt;p&gt;I tested having it schedule a meeting with three colleagues based on their availability. I gave it access to my test calendar and said: "Schedule a 1-hour team sync with Alex, Jordan, and Sam sometime next week, preferably mornings."&lt;/p&gt;

&lt;p&gt;It... kind of worked. It found a slot that was free for me and sent calendar invites. But it didn't actually check the other calendars (because I didn't set up those OAuth permissions). When I granted proper access and tried again, it worked but took few minutes to propose a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  File Organization (Surprisingly Good)
&lt;/h3&gt;

&lt;p&gt;I dumped 300 random files (PDFs, screenshots, documents) into a folder and asked it to: "Organize these by category and rename them with descriptive names."&lt;/p&gt;

&lt;p&gt;It created a folder structure (Documents/Work, Documents/Personal, Images/Screenshots, etc.) and renamed files with actually useful names. A screenshot of a GitHub issue became "github-issue-authentication-error-screenshot.png" instead of "Screenshot_2026_01_15.png".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This genuinely saved me time.&lt;/strong&gt; I'd use this feature regularly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web Research (The Expensive Part)
&lt;/h3&gt;

&lt;p&gt;I asked it to: "Research the top 5 project management tools for remote teams, compare pricing and features, and create a summary document."&lt;/p&gt;

&lt;p&gt;It spent the next 30 minutes browsing websites, taking screenshots, reading documentation, and compiling information. The output was actually useful, a well-structured comparison with pricing tiers and key features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt;: ~$22 (!!!) because of all the browser automation and vision model calls&lt;/p&gt;

&lt;p&gt;This is where the costs can spiral. Every screenshot it takes, every page it reads with vision models that adds up fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Review Automation (Didn't Work Well)
&lt;/h3&gt;

&lt;p&gt;I tried having it review a pull request and provide feedback. It read the diff, provided some generic comments about code structure, but missed an actual bug I'd intentionally introduced. It also suggested changes that would've broken the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern I Noticed
&lt;/h2&gt;

&lt;p&gt;After a week of testing, here's what I figured out: &lt;strong&gt;OpenClaw excels at structured, repetitive tasks with clear success criteria&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's not going to "figure out your business strategy". But for the boring, automatable stuff like email cleanup, file organization, research compilation,etc..  actually works.&lt;/p&gt;

&lt;p&gt;The failures came from tasks requiring judgment calls or deep domain knowledge. It's a tool, not a replacement for thinking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost Reality
&lt;/h2&gt;

&lt;p&gt;OpenClaw itself is free. But that free software isn't actually free to run.&lt;/p&gt;

&lt;p&gt;Over my five-day testing period, I spent approximately &lt;strong&gt;$47 in API costs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The expensive stuff? Anything involving browser automation with screenshots. Every time OpenClaw takes a screenshot to "see" what's on a page, that's a vision model API call. That $22 research task? It took 30 minutes and involved visiting 15+ websites with screenshots of each.&lt;/p&gt;

&lt;p&gt;I was using Claude Sonnet 3.5 for most tasks. If I'd used Opus for everything, those costs would've easily doubled or tripled.&lt;/p&gt;

&lt;p&gt;Here's what I learned: &lt;strong&gt;The model you choose dramatically affects costs.&lt;/strong&gt; For simple tasks (email sorting, file renaming), cheaper models work fine. For complex tasks (research, multi-step workflows), you need the expensive models - and the costs add up fast.&lt;/p&gt;

&lt;p&gt;I also discovered that runaway processes are a real concern. I set up a scheduled task to check my inbox every hour. It ran perfectly, but by morning it had made 24 API calls I hadn't budgeted for. Not catastrophically expensive (about $6), but it showed how costs creep up if you're not monitoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip from experience&lt;/strong&gt;: Set usage limits on your API keys from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Security Situation
&lt;/h2&gt;

&lt;p&gt;Here's the part that made me nervous during testing. I ran OpenClaw on an isolated laptop specifically because of what I'd read about security issues. After using it for a week, I understand why people are concerned.&lt;/p&gt;

&lt;p&gt;Let me be direct: the security concerns are real and architectural, not just implementation bugs.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Observed
&lt;/h3&gt;

&lt;p&gt;During setup, I noticed OpenClaw requested pretty extensive permissions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full filesystem access&lt;/li&gt;
&lt;li&gt;Ability to run shell commands&lt;/li&gt;
&lt;li&gt;Access to my messaging apps&lt;/li&gt;
&lt;li&gt;Browser control with full navigation rights&lt;/li&gt;
&lt;li&gt;Memory persistence across sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is necessary for it to work, but it also means OpenClaw has essentially root-level access to everything on that machine. If something goes wrong or if someone compromises the instance they have access to everything too.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem I see
&lt;/h3&gt;

&lt;p&gt;The fundamental issue isn't sloppy coding, it's architectural. OpenClaw needs system-level access to be useful. But giving an LLM that much power creates inherent risks.&lt;/p&gt;

&lt;p&gt;LLMs can be tricked through prompt injection. If OpenClaw visits a malicious website or reads a crafted document, hidden instructions can override your actual commands. I tested this lightly (in my isolated environment) and confirmed it's possible to confuse it with carefully worded instructions embedded in content.&lt;/p&gt;

&lt;p&gt;During my testing week, I kept OpenClaw strictly sandboxed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dedicated laptop with no personal data&lt;/li&gt;
&lt;li&gt;No connection to my main network&lt;/li&gt;
&lt;li&gt;No access to real email or calendar accounts (used throwaway accounts)&lt;/li&gt;
&lt;li&gt;Firewall rules preventing inbound connections&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Pattern
&lt;/h3&gt;

&lt;p&gt;Tasks with &lt;strong&gt;clear, verifiable outputs&lt;/strong&gt; worked reliably:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Unsubscribe from newsletters" → I can check my subscriptions&lt;/li&gt;
&lt;li&gt;"Rename files with descriptive names" → I can see the results&lt;/li&gt;
&lt;li&gt;"Sort emails by category" → Easy to verify&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tasks with &lt;strong&gt;ambiguous success criteria&lt;/strong&gt; were hit-or-miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Make this document more professional" → Subjective&lt;/li&gt;
&lt;li&gt;"Find the best options for X" → Depends on priorities I didn't specify&lt;/li&gt;
&lt;li&gt;"Schedule a convenient meeting" → Convenient for whom?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest issue: &lt;strong&gt;OpenClaw sometimes reports success when it hasn't actually completed the task&lt;/strong&gt;. That calendar scheduling failure? It told me everything worked. I only discovered the problem because I'm paranoid and double-check things.&lt;/p&gt;

&lt;p&gt;In a production environment, that's dangerous. You can't just trust the output, you need verification mechanisms. I agree with something I saw in another review: you're not removing human effort - you're changing it from execution to babysitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Honest Verdict
&lt;/h2&gt;

&lt;p&gt;After a week of actual hands-on testing, here's what I think:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The concept works&lt;/strong&gt;: That email cleanup saving me hours of manual work? That file organization actually being useful? Those are real wins. Personal AI assistants with system access aren't vaporware they exist and they can deliver value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The security model makes me nervous&lt;/strong&gt;: Even in my isolated test environment, I was constantly aware of how much access OpenClaw had. The architectural risks like prompt injection, malicious skills, exposed credentials aren't fixable with patches. They're inherent to giving an LLM system-level permissions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's genuinely useful... for specific tasks&lt;/strong&gt;: Structured, repetitive work with clear success criteria? OpenClaw handles it well. Anything requiring judgment, deep context, or nuanced decision-making? Not ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reliability needs verification&lt;/strong&gt;: The fact that it reported successful calendar scheduling when nothing actually happened concerns me. You can't just trust the output—you need to verify every automated action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Costs are manageable but need monitoring&lt;/strong&gt;: My $47 in five days extrapolates to roughly $280/month if I used it daily. For automation that saves 10+ hours monthly, maybe worth it. But runaway processes and unexpected bills are real risks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Would I Use It Going Forward?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For experimentation and learning?&lt;/strong&gt; Absolutely. It's fascinating technology and I learned a lot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For production automation on my main systems?&lt;/strong&gt; No way. Not with current security and reliability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On a dedicated, isolated machine for specific automation?&lt;/strong&gt; Possibly. If I had a clear, repetitive task that saved significant time, I might set it up carefully: dedicated hardware, whitelist-only skills, extensive monitoring, verification for every automated task.&lt;/p&gt;

&lt;p&gt;The gap between "this technically works" and "I trust this with my actual data" is still too wide for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Try It?
&lt;/h2&gt;

&lt;p&gt;Based on my testing experience:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider it if you:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are comfortable with Docker, security configs, and troubleshooting&lt;/li&gt;
&lt;li&gt;Have a dedicated machine you can isolate for testing&lt;/li&gt;
&lt;li&gt;Understand and accept the security risks deliberately&lt;/li&gt;
&lt;li&gt;Will audit any skills before installing them&lt;/li&gt;
&lt;li&gt;Have specific, repetitive automation needs&lt;/li&gt;
&lt;li&gt;Can monitor API costs actively&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;After spending a week actually using OpenClaw, I can say this: it's both more capable and problematic than the headlines suggest. Is it a breakthrough? For specific automation tasks, yes.&lt;/p&gt;

&lt;p&gt;Is it overhyped? Absolutely. The TikTok demos and viral tweets make it look safer and easier than the reality.&lt;/p&gt;

&lt;p&gt;Is it the future of work? Not yet. But it's showing us what that future might look like and more importantly, what problems we need to solve to get there. The conversations OpenClaw forces about security, reliability, governance, what we're comfortable delegating to AI are exactly the ones we need to have as autonomous agents become more capable.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was a hands-on test in a controlled environment. Your experience may vary. I really appreciate you taking the time to read through all of this!"&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>automation</category>
    </item>
    <item>
      <title>git commit -m "Initial Commit"</title>
      <dc:creator>Likhit Kumar V P</dc:creator>
      <pubDate>Mon, 02 Feb 2026 16:08:58 +0000</pubDate>
      <link>https://dev.to/likhit/git-commit-m-initial-commit-2dgb</link>
      <guid>https://dev.to/likhit/git-commit-m-initial-commit-2dgb</guid>
      <description>&lt;p&gt;Okay, this is obviously my first post and I have to say this upfront: &lt;strong&gt;I am very, very bad at writing and posting things.&lt;/strong&gt; I just want to skip the cringey "Welcome to my blog" dialogues and get straight to the point.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;"Why am I here?"&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Let’s be real: I've spent way too much time overthinking, calculating probabilities, and running mental statistics instead of &lt;strong&gt;just trying it out&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The things I learn usually disappear exactly when I need them, like Dr. Strange's spell at the end of &lt;em&gt;No Way Home&lt;/em&gt;. Poof. Gone.&lt;/p&gt;

&lt;p&gt;But today, the &lt;strong&gt;"Day 1"&lt;/strong&gt; energy is hitting different.&lt;/p&gt;

&lt;p&gt;I’m starting this blog because I realized I need a superpower. I realized that keeping my technical knowledge locked in my brain (or buried in messy &lt;code&gt;untitled.txt&lt;/code&gt; files) isn't helping anyone.&lt;/p&gt;

&lt;p&gt;This blog is my personal &lt;strong&gt;Consistency Meter&lt;/strong&gt;. I’m pushing my limits to see if I can actually maintain a streak that isn't just my Duolingo owl threatening me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnny48aawc8wcjzg8xs9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnny48aawc8wcjzg8xs9.png" alt=" " width="800" height="519"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is a scene in &lt;em&gt;Invincible&lt;/em&gt; where Omni-Man yells, &lt;strong&gt;"THINK MARK!! THINK!"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the core philosophy of this blog. I am trying to build a bridge between my &lt;strong&gt;Current Self&lt;/strong&gt; and my &lt;strong&gt;Future Self&lt;/strong&gt; (who hopefully knows better than I do today).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What can you expect?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I’m going to be posting about the technical issues I overcome and the solutions I find sometimes elegant, sometimes messy, but always working.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Code:&lt;/strong&gt; Snippets that actually do something useful.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Fixes:&lt;/strong&gt; The specific solutions I built to solve specific headaches.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Struggle:&lt;/strong&gt; The errors that made me question my life choices.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Growth:&lt;/strong&gt; Moving from "just coding" to shipping products that help people.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s going to be fun, and honestly? I'm really excited.&lt;/p&gt;

&lt;p&gt;Let’s see how long I can keep the Consistency Meter running.&lt;/p&gt;

</description>
      <category>blog</category>
    </item>
  </channel>
</rss>
