<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Yogesh Bhawsar]]></title><description><![CDATA[Yogesh Bhawsar - Full-Stack Developer in Pune. Expert in Node.js, React, AI integration, and scalable web apps. Focused on clean architecture and performance. yogesh@bhawsar.dev]]></description><link>https://yogeshbhawsar.com</link><generator>RSS for Node</generator><lastBuildDate>Tue, 14 Apr 2026 01:16:25 GMT</lastBuildDate><atom:link href="https://yogeshbhawsar.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Real-Time Inventory Management with Vision LLMs]]></title><description><![CDATA[Manual inventory management is a pain. Staff manually counting items, typing product names, and dealing with inevitable typos - it's slow, error-prone, and often means your inventory records are outdated before they're even saved. But what if you cou...]]></description><link>https://yogeshbhawsar.com/real-time-inventory-management-with-vision-llms</link><guid isPermaLink="true">https://yogeshbhawsar.com/real-time-inventory-management-with-vision-llms</guid><category><![CDATA[Vision]]></category><category><![CDATA[AI]]></category><category><![CDATA[React]]></category><category><![CDATA[gemini]]></category><category><![CDATA[nestjs]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[inventory management]]></category><category><![CDATA[ERP]]></category><dc:creator><![CDATA[Yogesh Bhawsar]]></dc:creator><pubDate>Wed, 11 Feb 2026 03:13:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770731913025/66e0f043-4ac3-4746-815a-cd78b4581b8d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Manual inventory management is a pain. Staff manually counting items, typing product names, and dealing with inevitable typos - it's slow, error-prone, and often means your inventory records are outdated before they're even saved. But what if you could just point a camera at your warehouse shelf and have everything automatically identified and logged into your ERP system?</p>
<p>That's exactly what we built. This project uses Vision Language Models (VLMs) to transform how businesses track inventory, turning a tedious manual process into something that happens in real-time with a quick photo.</p>
<h2 id="heading-why-this-matters">Why This Matters</h2>
<p>Traditional inventory management has three major problems:</p>
<ol>
<li><p><strong>Human Error</strong>: Typos in product names and miscounts create inconsistent data</p>
</li>
<li><p><strong>Time Lag</strong>: Records get updated hours or days after stock arrives</p>
</li>
<li><p><strong>Wasted Effort</strong>: Staff spend valuable time on data entry instead of core tasks</p>
</li>
</ol>
<p>By using computer vision, we eliminate all three issues. The system identifies products from images and syncs them instantly with your ERP.</p>
<h2 id="heading-how-it-works">How It Works</h2>
<p>The solution is a full-stack TypeScript application designed for reliability and real-world use. Here's the stack:</p>
<ul>
<li><p><strong>Frontend (React 19)</strong>: Built with Vite, uses the browser's Camera API for high-resolution captures. Clean, responsive UI with Tailwind CSS and shadcn/ui that works on mobile and desktop</p>
</li>
<li><p><strong>Backend (NestJS)</strong>: Handles image processing, API authentication, and ERP synchronization with robust business logic</p>
</li>
<li><p><strong>Database (PostgreSQL)</strong>: Uses the <code>pg_trgm</code> extension for fuzzy matching - critical for aligning AI-detected names with existing products</p>
</li>
<li><p><strong>AI (Vision LLM)</strong>: Currently using <strong>Gemini 3 Flash</strong> for exceptional speed and accuracy. With Google's free tier offering 1,500 requests/day, it's incredibly cost-effective for small to medium businesses</p>
</li>
</ul>
<h2 id="heading-the-secret-sauce-prompt-engineering">The Secret Sauce: Prompt Engineering</h2>
<p>Getting structured data from the AI is crucial. We use a carefully crafted prompt that enforces a strict JSON schema, making the response easy to parse without fragile string manipulation. This prompt works across industries - groceries, hardware, electronics, medical supplies, you name it.</p>
<pre><code class="lang-text">Analyze this image and identify all visible inventory items or products.
For each item, provide:
1. Product name (be specific, include size or variant if visible)
2. Brand (if visible on the packaging)
3. Category (e.g., "Industrial", "Dairy", "Office Supplies")
4. Quantity or Size (e.g., "1kg", "12 units", "Box of 50")
5. Confidence score (a number from 0 to 100 based on your certainty)

Return ONLY a valid JSON array. Do not include markdown formatting or extra text.
Structure:
[
  {
    "name": "string",
    "brand": "string or null",
    "category": "string",
    "quantity": "string or null",
    "confidence": number
  }
]
</code></pre>
<h2 id="heading-implementation-the-two-service-approach">Implementation: The Two-Service Approach</h2>
<h3 id="heading-vision-service">Vision Service</h3>
<p>The <code>VisionService</code> talks to the Vision LLM and processes base64-encoded images:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { GoogleGenAI } <span class="hljs-keyword">from</span> <span class="hljs-string">'@google/genai'</span>;

<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> VisionService {
  <span class="hljs-keyword">private</span> client: GoogleGenAI;

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) {
    <span class="hljs-built_in">this</span>.client = <span class="hljs-keyword">new</span> GoogleGenAI({ 
      apiKey: process.env.GEMINI_API_KEY 
    });
  }

  <span class="hljs-keyword">async</span> analyzeImage(imageBase64: <span class="hljs-built_in">string</span>) {
    <span class="hljs-keyword">const</span> base64Data = imageBase64.replace(<span class="hljs-regexp">/^data:image\/\w+;base64,/</span>, <span class="hljs-string">''</span>);

    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.client.models.generateContent({
      model: <span class="hljs-string">'gemini-3-flash'</span>,
      contents: [{
        role: <span class="hljs-string">'user'</span>,
        parts: [
          { text: VISION_PROMPT },
          { 
            inlineData: { 
              data: base64Data, 
              mimeType: <span class="hljs-string">'image/jpeg'</span> 
            } 
          }
        ]
      }]
    });

    <span class="hljs-keyword">const</span> text = result.text;
    <span class="hljs-keyword">if</span> (!text) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'No response from Vision LLM'</span>);

    <span class="hljs-keyword">const</span> jsonMatch = text.match(<span class="hljs-regexp">/\[[\s\S]*\]/</span>);
    <span class="hljs-keyword">if</span> (!jsonMatch) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Invalid JSON format'</span>);

    <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(jsonMatch[<span class="hljs-number">0</span>]);
  }
}
</code></pre>
<h3 id="heading-smart-product-matching">Smart Product Matching</h3>
<p>The AI rarely gives us exact database matches. The <code>ProductMatchingService</code> uses a three-tier approach to handle this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> findBestMatch(item: DetectedItem): <span class="hljs-built_in">Promise</span>&lt;ProductMatch&gt; {
  <span class="hljs-keyword">const</span> exactMatch = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.productRepository.findOne({
    where: { 
      name: item.name, 
      brand: item.brand 
    }
  });

  <span class="hljs-keyword">if</span> (exactMatch) {
    <span class="hljs-keyword">return</span> { 
      productId: exactMatch.id, 
      matchScore: <span class="hljs-number">100</span>, 
      isManualAddNeeded: <span class="hljs-literal">false</span> 
    };
  }

  <span class="hljs-keyword">const</span> fuzzyMatches = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.productRepository.query(
    <span class="hljs-string">`SELECT *, similarity(name, $1) as score 
     FROM products 
     WHERE similarity(name, $1) &gt; 0.3 
     ORDER BY score DESC 
     LIMIT 1`</span>,
    [item.name]
  );

  <span class="hljs-keyword">if</span> (fuzzyMatches.length &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">const</span> score = <span class="hljs-built_in">Math</span>.round(fuzzyMatches[<span class="hljs-number">0</span>].score * <span class="hljs-number">100</span>);
    <span class="hljs-keyword">return</span> { 
      productId: fuzzyMatches[<span class="hljs-number">0</span>].id, 
      matchScore: score, 
      isManualAddNeeded: score &lt; <span class="hljs-number">75</span> 
    };
  }

  <span class="hljs-keyword">return</span> { 
    productId: <span class="hljs-literal">null</span>, 
    matchScore: <span class="hljs-number">0</span>, 
    isManualAddNeeded: <span class="hljs-literal">true</span> 
  };
}
</code></pre>
<h2 id="heading-real-world-impact">Real-World Impact</h2>
<p>This isn't just a proof of concept - it's deployed and running in production. Here's what we've seen:</p>
<ul>
<li><p><strong>Massive Time Savings</strong>: Scanning a shelf takes seconds instead of minutes of manual entry</p>
</li>
<li><p><strong>Better Data Quality</strong>: AI + fuzzy matching reduces inventory "noise" significantly</p>
</li>
<li><p><strong>Smart Workflows</strong>: Items with confidence scores below 75% get flagged for human review automatically</p>
</li>
<li><p><strong>Real-Time Visibility</strong>: ERP reflects physical stock changes almost instantly, enabling better forecasting</p>
</li>
</ul>
<h2 id="heading-whats-next-going-local-with-ollama">What's Next: Going Local with Ollama</h2>
<p>While cloud-based Vision LLMs like Gemini 3 Flash are great for production, there's growing interest in running these models locally for privacy, cost control, or offline operation. That's where <strong>Ollama</strong> comes in.</p>
<h3 id="heading-why-run-vision-models-locally">Why Run Vision Models Locally?</h3>
<ul>
<li><p><strong>Privacy First</strong>: Your inventory data never leaves your servers</p>
</li>
<li><p><strong>Zero API Costs</strong>: No per-request charges or rate limits</p>
</li>
<li><p><strong>Offline Capability</strong>: Works without internet connectivity</p>
</li>
</ul>
<p>Use can refer to <a target="_blank" href="http://ollama.com/search?c=vision">ollama.com/search?c=vision</a> For vision LLMs</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Vision LLMs are transforming inventory management from a manual chore into an automated, real-time process. Whether you choose cloud-based models like Gemini 3 Flash for their speed and accuracy, or local models via Ollama for privacy and cost control, the technology is here and ready for production use.</p>
<p>The future? Even better. As models get smaller and faster, we'll see more edge deployment, multi-camera automation, and seamless ERP integration becoming the standard rather than the exception.</p>
<p><strong>Want to try it yourself?</strong> Try here at <a target="_blank" href="https://github.com/yo9e5h/inventory-vision-demo">github.com/yo9e5h/inventory-vision-demo</a></p>
]]></content:encoded></item><item><title><![CDATA[React 19.2: Activity Component and useEffectEvent Hook]]></title><description><![CDATA[React 19.2, released on October 1, 2025, introduced two APIs that solve problems developers have been wrestling with since hooks were first introduced: the Activity component and the useEffectEvent hook. This guide explores what these APIs do, why th...]]></description><link>https://yogeshbhawsar.com/react-activity-component-and-useeffectevent-hook</link><guid isPermaLink="true">https://yogeshbhawsar.com/react-activity-component-and-useeffectevent-hook</guid><category><![CDATA[useFocusEvent]]></category><category><![CDATA[React 19.2]]></category><category><![CDATA[React]]></category><category><![CDATA[Activity]]></category><dc:creator><![CDATA[Yogesh Bhawsar]]></dc:creator><pubDate>Tue, 10 Feb 2026 12:24:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770726187949/644f8c3b-7072-41f9-aade-814c1a23bd66.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>React 19.2, released on October 1, 2025, introduced two APIs that solve problems developers have been wrestling with since hooks were first introduced: the Activity component and the useEffectEvent hook. This guide explores what these APIs do, why they matter, and how to use them effectively in production. We also cover a critical security vulnerability (CVE-2025-55182) that was patched in React 19.2.1.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#the-activity-component">The Activity Component</a></p>
</li>
<li><p><a class="post-section-overview" href="#the-useeffectevent-hook">The useEffectEvent Hook</a></p>
</li>
<li><p><a class="post-section-overview" href="#security-update-cve-2025-55182">Security Update: CVE-2025-55182</a></p>
</li>
<li><p><a class="post-section-overview" href="#migration-guide">Migration Guide</a></p>
</li>
<li><p><a class="post-section-overview" href="#resources">Resources</a></p>
</li>
</ol>
<h2 id="heading-the-activity-component">The Activity Component</h2>
<h3 id="heading-the-problem-the-tab-state-dilemma">The Problem: The Tab State Dilemma</h3>
<p>React developers have faced an impossible choice when building tabbed interfaces, wizards, or multi-step forms.</p>
<p><strong>Option 1: Conditional Rendering</strong></p>
<pre><code class="lang-jsx"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [activeTab, setActiveTab] = useState(<span class="hljs-string">'home'</span>);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">TabButtons</span> <span class="hljs-attr">activeTab</span>=<span class="hljs-string">{activeTab}</span> <span class="hljs-attr">onChange</span>=<span class="hljs-string">{setActiveTab}</span> /&gt;</span>
      {activeTab === 'home' &amp;&amp; <span class="hljs-tag">&lt;<span class="hljs-name">HomeTab</span> /&gt;</span>}
      {activeTab === 'settings' &amp;&amp; <span class="hljs-tag">&lt;<span class="hljs-name">SettingsTab</span> /&gt;</span>}
      {activeTab === 'profile' &amp;&amp; <span class="hljs-tag">&lt;<span class="hljs-name">ProfileTab</span> /&gt;</span>}
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>The problem: Every time you switch tabs, the inactive component is completely destroyed. Form inputs are cleared, scroll positions are lost, and network requests are cancelled. Users type half a message in Settings, switch to Home, then come back to find their work gone.</p>
<p><strong>Option 2: CSS Hiding</strong></p>
<pre><code class="lang-jsx"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [activeTab, setActiveTab] = useState(<span class="hljs-string">'home'</span>);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">TabButtons</span> <span class="hljs-attr">activeTab</span>=<span class="hljs-string">{activeTab}</span> <span class="hljs-attr">onChange</span>=<span class="hljs-string">{setActiveTab}</span> /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">display:</span> <span class="hljs-attr">activeTab</span> === <span class="hljs-string">'home'</span> ? '<span class="hljs-attr">block</span>' <span class="hljs-attr">:</span> '<span class="hljs-attr">none</span>' }}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">HomeTab</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">display:</span> <span class="hljs-attr">activeTab</span> === <span class="hljs-string">'settings'</span> ? '<span class="hljs-attr">block</span>' <span class="hljs-attr">:</span> '<span class="hljs-attr">none</span>' }}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">SettingsTab</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">display:</span> <span class="hljs-attr">activeTab</span> === <span class="hljs-string">'profile'</span> ? '<span class="hljs-attr">block</span>' <span class="hljs-attr">:</span> '<span class="hljs-attr">none</span>' }}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">ProfileTab</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>The problem: All tabs remain mounted and their effects keep running. Hidden video players continue buffering, hidden dashboards keep polling APIs, and hidden forms keep validating. The browser's layout engine has to manage thousands of invisible DOM nodes. Performance degrades significantly.</p>
<h3 id="heading-the-solution-activity-component">The Solution: Activity Component</h3>
<p>React 19.2's Activity component gives you the best of both approaches.</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> { Activity, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [activeTab, setActiveTab] = useState(<span class="hljs-string">'home'</span>);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">TabButtons</span> <span class="hljs-attr">activeTab</span>=<span class="hljs-string">{activeTab}</span> <span class="hljs-attr">onChange</span>=<span class="hljs-string">{setActiveTab}</span> /&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Activity</span> <span class="hljs-attr">mode</span>=<span class="hljs-string">{activeTab</span> === <span class="hljs-string">'home'</span> ? '<span class="hljs-attr">visible</span>' <span class="hljs-attr">:</span> '<span class="hljs-attr">hidden</span>'}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">HomeTab</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Activity</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Activity</span> <span class="hljs-attr">mode</span>=<span class="hljs-string">{activeTab</span> === <span class="hljs-string">'settings'</span> ? '<span class="hljs-attr">visible</span>' <span class="hljs-attr">:</span> '<span class="hljs-attr">hidden</span>'}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">SettingsTab</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Activity</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Activity</span> <span class="hljs-attr">mode</span>=<span class="hljs-string">{activeTab</span> === <span class="hljs-string">'profile'</span> ? '<span class="hljs-attr">visible</span>' <span class="hljs-attr">:</span> '<span class="hljs-attr">hidden</span>'}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">ProfileTab</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Activity</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<h3 id="heading-how-activity-works">How Activity Works</h3>
<p>When you set an Activity to <code>mode="hidden"</code>, React performs several operations:</p>
<ol>
<li><p><strong>Preserves the Fiber Tree</strong>: React's internal representation of your component stays in memory, so all your useState and useReducer state is preserved</p>
</li>
<li><p><strong>Unmounts Effects</strong>: All useEffect cleanup functions are called, stopping subscriptions, timers, and network requests</p>
</li>
<li><p><strong>Hides the DOM</strong>: Sets display: none on the container, so the browser's layout engine doesn't process it</p>
</li>
<li><p><strong>Defers Updates</strong>: Any state updates that happen while hidden are batched and applied at low priority</p>
</li>
</ol>
<h3 id="heading-lifecycle-comparison"><strong>Lifecycle comparison:</strong></h3>
<p>Traditional Conditional Rendering:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770724927970/75815828-6c9d-4fb6-8636-f104e50a0947.png" alt class="image--center mx-auto" /></p>
<p>Activity Component:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770724955298/7a9e5d14-81a8-46b0-9eeb-a6b38f665464.png" alt class="image--center mx-auto" /></p>
<p>The key difference: Activity calls cleanup functions but doesn't destroy the Fiber nodes or state.</p>
<h3 id="heading-real-world-example-music-player-with-tabs">Real-World Example: Music Player with Tabs</h3>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> { Activity, useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MusicPlayer</span>(<span class="hljs-params">{ songUrl }</span>) </span>{
  <span class="hljs-keyword">const</span> [isPlaying, setIsPlaying] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [currentTime, setCurrentTime] = useState(<span class="hljs-number">0</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (!isPlaying) <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">const</span> audio = <span class="hljs-keyword">new</span> Audio(songUrl);
    audio.currentTime = currentTime;
    audio.play();

    <span class="hljs-keyword">const</span> interval = <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =&gt;</span> {
      setCurrentTime(audio.currentTime);
    }, <span class="hljs-number">100</span>);

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-built_in">clearInterval</span>(interval);
      audio.pause();
    };
  }, [isPlaying, songUrl, currentTime]);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Now Playing<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Time: {currentTime.toFixed(2)}s<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> setIsPlaying(!isPlaying)}&gt;
        {isPlaying ? 'Pause' : 'Play'}
      <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [tab, setTab] = useState(<span class="hljs-string">'player'</span>);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> setTab('player')}&gt;Player<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> setTab('library')}&gt;Library<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Activity</span> <span class="hljs-attr">mode</span>=<span class="hljs-string">{tab</span> === <span class="hljs-string">'player'</span> ? '<span class="hljs-attr">visible</span>' <span class="hljs-attr">:</span> '<span class="hljs-attr">hidden</span>'}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">MusicPlayer</span> <span class="hljs-attr">songUrl</span>=<span class="hljs-string">"/song.mp3"</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Activity</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Activity</span> <span class="hljs-attr">mode</span>=<span class="hljs-string">{tab</span> === <span class="hljs-string">'library'</span> ? '<span class="hljs-attr">visible</span>' <span class="hljs-attr">:</span> '<span class="hljs-attr">hidden</span>'}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Library</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Activity</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>When you switch from Player to Library, the audio stops playing because the effect cleanup runs. The currentTime state is preserved at, for example, 1:32. When you switch back to Player, the audio starts from 1:32 because the state was preserved.</p>
<h3 id="heading-pre-rendering-load-before-you-need-it">Pre-rendering: Load Before You Need It</h3>
<p>One of the most powerful features of Activity is the ability to pre-render components while they're hidden.</p>
<pre><code class="lang-jsx"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Dashboard</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [activeSection, setActiveSection] = useState(<span class="hljs-string">'overview'</span>);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">SectionTabs</span> <span class="hljs-attr">active</span>=<span class="hljs-string">{activeSection}</span> <span class="hljs-attr">onChange</span>=<span class="hljs-string">{setActiveSection}</span> /&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Activity</span> <span class="hljs-attr">mode</span>=<span class="hljs-string">{activeSection</span> === <span class="hljs-string">'overview'</span> ? '<span class="hljs-attr">visible</span>' <span class="hljs-attr">:</span> '<span class="hljs-attr">hidden</span>'}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Overview</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Activity</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Activity</span> <span class="hljs-attr">mode</span>=<span class="hljs-string">{activeSection</span> === <span class="hljs-string">'analytics'</span> ? '<span class="hljs-attr">visible</span>' <span class="hljs-attr">:</span> '<span class="hljs-attr">hidden</span>'}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Analytics</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Activity</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Activity</span> <span class="hljs-attr">mode</span>=<span class="hljs-string">{activeSection</span> === <span class="hljs-string">'reports'</span> ? '<span class="hljs-attr">visible</span>' <span class="hljs-attr">:</span> '<span class="hljs-attr">hidden</span>'}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Reports</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Activity</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Analytics</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> data = use(fetchAnalyticsData());

  <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">AnalyticsChart</span> <span class="hljs-attr">data</span>=<span class="hljs-string">{data}</span> /&gt;</span></span>;
}
</code></pre>
<p>By the time users click the Analytics tab, the data is already loaded and rendered. The transition feels instant.</p>
<h3 id="heading-important-dom-side-effects">Important: DOM Side Effects</h3>
<p>Most React components work perfectly with Activity, but there's one edge case to watch for: DOM elements with persistent side effects.</p>
<pre><code class="lang-jsx"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">VideoPlayer</span>(<span class="hljs-params">{ src }</span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Activity</span> <span class="hljs-attr">mode</span>=<span class="hljs-string">{isVisible</span> ? '<span class="hljs-attr">visible</span>' <span class="hljs-attr">:</span> '<span class="hljs-attr">hidden</span>'}&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">video</span> <span class="hljs-attr">src</span>=<span class="hljs-string">{src}</span> <span class="hljs-attr">autoPlay</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Activity</span>&gt;</span></span>
  );
}
</code></pre>
<p>Problem: When you hide this component, the video tag gets display: none, but the video keeps playing. You can hear the audio even though you can't see it.</p>
<p><strong>Solution: Add an effect with proper cleanup</strong></p>
<pre><code class="lang-jsx"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">VideoPlayer</span>(<span class="hljs-params">{ src }</span>) </span>{
  <span class="hljs-keyword">const</span> videoRef = useRef(<span class="hljs-literal">null</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> video = videoRef.current;
    <span class="hljs-keyword">if</span> (!video) <span class="hljs-keyword">return</span>;

    video.play();

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
      video.pause();
    };
  }, []);

  <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">video</span> <span class="hljs-attr">ref</span>=<span class="hljs-string">{videoRef}</span> <span class="hljs-attr">src</span>=<span class="hljs-string">{src}</span> /&gt;</span></span>;
}
</code></pre>
<p>Other DOM elements that need this treatment include: audio, iframe, Canvas animations, and WebSocket connections.</p>
<h3 id="heading-memory-considerations">Memory Considerations</h3>
<p>Activity trades memory for speed. A hidden component stays in memory at roughly 2x the cost of the visible version (Fiber tree + DOM). For most applications, this is acceptable. A few hidden tabs use negligible memory on modern devices.</p>
<p>However, if you're building something with dozens of potential activities, the React team is exploring automatic state destruction for least-recently-used hidden activities.</p>
<p><strong>Current recommendation:</strong></p>
<pre><code class="lang-jsx">&lt;Activity mode={tab === <span class="hljs-string">'home'</span> ? <span class="hljs-string">'visible'</span> : <span class="hljs-string">'hidden'</span>}&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">HomeTab</span> /&gt;</span></span>
&lt;/Activity&gt;

{isModalOpen &amp;&amp; <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Modal</span> /&gt;</span></span>}
</code></pre>
<p>Use Activity for components the user frequently switches between. Use conditional rendering for components the user is unlikely to return to soon, such as closed modals.</p>
<h2 id="heading-the-useeffectevent-hook">The useEffectEvent Hook</h2>
<h3 id="heading-the-problem-dependency-array-issues">The Problem: Dependency Array Issues</h3>
<p>Here's a scenario every React developer has faced:</p>
<pre><code class="lang-jsx"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ChatRoom</span>(<span class="hljs-params">{ roomId, theme }</span>) </span>{
  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> connection = createConnection(roomId);
    connection.connect();

    connection.on(<span class="hljs-string">'connected'</span>, <span class="hljs-function">() =&gt;</span> {
      showNotification(<span class="hljs-string">'Connected!'</span>, theme);
    });

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> connection.disconnect();
  }, [roomId, theme]);
}
</code></pre>
<p>The problem: You want the effect to reconnect when roomId changes, but not when theme changes. However, ESLint's exhaustive-deps rule correctly requires you to include theme in the dependency array.</p>
<p>If you include theme, the effect tears down and reconnects the entire WebSocket connection just because the user switched from light mode to dark mode. That's wasteful and causes flickering.</p>
<h3 id="heading-the-old-workaround-useref-pattern">The Old Workaround: useRef Pattern</h3>
<p>Before useEffectEvent, developers used this pattern:</p>
<pre><code class="lang-jsx"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ChatRoom</span>(<span class="hljs-params">{ roomId, theme }</span>) </span>{
  <span class="hljs-keyword">const</span> themeRef = useRef(theme);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    themeRef.current = theme;
  });

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> connection = createConnection(roomId);
    connection.connect();

    connection.on(<span class="hljs-string">'connected'</span>, <span class="hljs-function">() =&gt;</span> {
      showNotification(<span class="hljs-string">'Connected!'</span>, themeRef.current);
    });

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> connection.disconnect();
  }, [roomId]);
}
</code></pre>
<p>This works, but it's verbose, error-prone, and the linter can't help you. Plus, it's not obvious to other developers what's happening.</p>
<h3 id="heading-the-solution-useeffectevent">The Solution: useEffectEvent</h3>
<p>React 19.2 introduces useEffectEvent to handle this pattern cleanly:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> { useEffect, useEffectEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ChatRoom</span>(<span class="hljs-params">{ roomId, theme }</span>) </span>{
  <span class="hljs-keyword">const</span> onConnected = useEffectEvent(<span class="hljs-function">() =&gt;</span> {
    showNotification(<span class="hljs-string">'Connected!'</span>, theme);
  });

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> connection = createConnection(roomId);
    connection.connect();

    connection.on(<span class="hljs-string">'connected'</span>, <span class="hljs-function">() =&gt;</span> {
      onConnected();
    });

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> connection.disconnect();
  }, [roomId]);
}
</code></pre>
<p>How it works:</p>
<ol>
<li><p>useEffectEvent creates a stable function reference that doesn't change between renders</p>
</li>
<li><p>The function always sees the latest values of theme and other variables</p>
</li>
<li><p>The effect only re-runs when roomId changes</p>
</li>
<li><p>ESLint's React Hooks plugin (v6+) understands useEffectEvent and won't complain</p>
</li>
</ol>
<h3 id="heading-real-world-example-analytics-logger">Real-World Example: Analytics Logger</h3>
<p>A common use case is logging events that need current user state but shouldn't trigger effect re-runs:</p>
<pre><code class="lang-jsx"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Page</span>(<span class="hljs-params">{ url }</span>) </span>{
  <span class="hljs-keyword">const</span> { items } = useContext(ShoppingCartContext);
  <span class="hljs-keyword">const</span> numberOfItems = items.length;

  <span class="hljs-keyword">const</span> onNavigate = useEffectEvent(<span class="hljs-function">(<span class="hljs-params">visitedUrl</span>) =&gt;</span> {
    logVisit(visitedUrl, numberOfItems);
  });

  useEffect(<span class="hljs-function">() =&gt;</span> {
    onNavigate(url);
  }, [url]);
}
</code></pre>
<p>The effect only re-runs when URL changes, but it always logs the current cart count.</p>
<h3 id="heading-example-timers-and-intervals">Example: Timers and Intervals</h3>
<p>Another classic use case is intervals that need to read current state:</p>
<pre><code class="lang-jsx"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Counter</span>(<span class="hljs-params">{ incrementBy }</span>) </span>{
  <span class="hljs-keyword">const</span> [count, setCount] = useState(<span class="hljs-number">0</span>);

  <span class="hljs-keyword">const</span> onTick = useEffectEvent(<span class="hljs-function">() =&gt;</span> {
    setCount(<span class="hljs-function"><span class="hljs-params">c</span> =&gt;</span> c + incrementBy);
  });

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> id = <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =&gt;</span> {
      onTick();
    }, <span class="hljs-number">1000</span>);

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">clearInterval</span>(id);
  }, []);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Count: {count}<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Incrementing by: {incrementBy}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>The interval never restarts, but it always uses the current incrementBy value.</p>
<h3 id="heading-creating-reusable-hooks">Creating Reusable Hooks</h3>
<p>useEffectEvent is particularly useful when building custom hooks:</p>
<pre><code class="lang-jsx"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useInterval</span>(<span class="hljs-params">callback, delay</span>) </span>{
  <span class="hljs-keyword">const</span> onTick = useEffectEvent(callback);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (delay === <span class="hljs-literal">null</span>) <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">const</span> id = <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =&gt;</span> {
      onTick();
    }, delay);

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">clearInterval</span>(id);
  }, [delay]);
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [count, setCount] = useState(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> [incrementBy, setIncrementBy] = useState(<span class="hljs-number">1</span>);

  useInterval(<span class="hljs-function">() =&gt;</span> {
    setCount(<span class="hljs-function"><span class="hljs-params">c</span> =&gt;</span> c + incrementBy);
  }, <span class="hljs-number">1000</span>);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Count: {count}<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">select</span> 
        <span class="hljs-attr">value</span>=<span class="hljs-string">{incrementBy}</span> 
        <span class="hljs-attr">onChange</span>=<span class="hljs-string">{e</span> =&gt;</span> setIncrementBy(Number(e.target.value))}
      &gt;
        <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{1}</span>&gt;</span>+1<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{5}</span>&gt;</span>+5<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{10}</span>&gt;</span>+10<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">select</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<h3 id="heading-important-rules-and-limitations">Important Rules and Limitations</h3>
<p><strong>Only call from effects or other Effect Events:</strong></p>
<pre><code class="lang-jsx"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Component</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> onEvent = useEffectEvent(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Event fired'</span>);
  });

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> subscription = subscribe(onEvent);
    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> subscription.unsubscribe();
  }, []);

  <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>Component<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>;
}
</code></pre>
<p>Do not call useEffectEvent during render or pass it as a prop to DOM elements.</p>
<p><strong>Do not use to hide dependencies:</strong></p>
<pre><code class="lang-jsx">useEffect(<span class="hljs-function">() =&gt;</span> {
  saveData(data);
}, [data]);
</code></pre>
<p>If data changes should trigger the effect, keep it in the dependency array. useEffectEvent is for reading values that shouldn't trigger re-runs.</p>
<p><strong>Effect Events don't have stable identity:</strong></p>
<p>Do not pass Effect Events to child components or compare them in useEffect. They are meant to be called only from within effects.</p>
<h3 id="heading-when-to-use-useeffectevent">When to Use useEffectEvent</h3>
<p><strong>Good use cases:</strong></p>
<ul>
<li><p>Reading latest props or state in event handlers inside effects</p>
</li>
<li><p>Logging with current user context</p>
</li>
<li><p>Notifications that need current theme or settings</p>
</li>
<li><p>Timers or intervals that need current values</p>
</li>
<li><p>Cleanup functions that need current state</p>
</li>
</ul>
<p><strong>Bad use cases:</strong></p>
<ul>
<li><p>Avoiding legitimate reactive dependencies</p>
</li>
<li><p>Replacing useCallback for child component props</p>
</li>
<li><p>Hiding bugs in dependency arrays</p>
</li>
<li><p>Event handlers that should be passed to DOM elements</p>
</li>
</ul>
<h2 id="heading-migration-guide">Migration Guide</h2>
<h3 id="heading-adopting-activity-in-existing-apps">Adopting Activity in Existing Apps</h3>
<p><strong>Step 1: Identify candidates</strong></p>
<p>Look for components that:</p>
<ul>
<li><p>Are frequently shown or hidden (tabs, modals, sidebars)</p>
</li>
<li><p>Have expensive setup or teardown (WebSocket connections, subscriptions)</p>
</li>
<li><p>Contain user input that should be preserved (forms, text editors)</p>
</li>
</ul>
<p><strong>Step 2: Test incrementally</strong></p>
<pre><code class="lang-jsx">{activeTab === <span class="hljs-string">'settings'</span> &amp;&amp; <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">SettingsTab</span> /&gt;</span></span>}
</code></pre>
<p>becomes</p>
<pre><code class="lang-jsx">&lt;Activity mode={activeTab === <span class="hljs-string">'settings'</span> ? <span class="hljs-string">'visible'</span> : <span class="hljs-string">'hidden'</span>}&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">SettingsTab</span> /&gt;</span></span>
&lt;/Activity&gt;
</code></pre>
<p><strong>Step 3: Audit effect cleanup</strong></p>
<p>Make sure your components have proper cleanup:</p>
<pre><code class="lang-jsx">useEffect(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> subscription = subscribe();

  <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
    subscription.unsubscribe();
  };
}, []);
</code></pre>
<p>Enable StrictMode to catch missing cleanup functions.</p>
<h3 id="heading-adopting-useeffectevent-in-existing-apps">Adopting useEffectEvent in Existing Apps</h3>
<p><strong>Step 1: Find patterns where it helps</strong></p>
<p>Search your codebase for:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> someRef = useRef(someValue);
useEffect(<span class="hljs-function">() =&gt;</span> { 
  someRef.current = someValue; 
});
</code></pre>
<p>or</p>
<pre><code class="lang-jsx">useEffect(<span class="hljs-function">() =&gt;</span> {

}, []);
</code></pre>
<p><strong>Step 2: Refactor to useEffectEvent</strong></p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> latestCallback = useRef(callback);
useEffect(<span class="hljs-function">() =&gt;</span> { 
  latestCallback.current = callback; 
});
useEffect(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> id = <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =&gt;</span> latestCallback.current(), <span class="hljs-number">1000</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">clearInterval</span>(id);
}, []);
</code></pre>
<p>becomes</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> onTick = useEffectEvent(callback);
useEffect(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> id = <span class="hljs-built_in">setInterval</span>(onTick, <span class="hljs-number">1000</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">clearInterval</span>(id);
}, []);
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>React 19.2's Activity component and useEffectEvent hook solve two fundamental problems that have plagued React developers since the beginning. Activity eliminates the false choice between state preservation and performance when hiding UI. useEffectEvent provides a clean, linter-friendly way to access latest values without unnecessary effect re-runs.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a target="_blank" href="https://react.dev/blog/2025/10/01/react-19-2">React 19.2 Release Notes</a></p>
</li>
<li><p><a target="_blank" href="https://react.dev/reference/react/Activity">Activity API Reference</a></p>
</li>
<li><p><a target="_blank" href="https://react.dev/reference/react/useEffectEvent">useEffectEvent Hook Reference</a></p>
</li>
<li><p><a target="_blank" href="https://react.dev/learn/separating-events-from-effects">Separating Events from Effects</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Build a Privacy-First AI Chatbot with Ollama, TanStack AI, and React: The Complete 2026 Guide]]></title><description><![CDATA[Building AI applications doesn't mean you have to send every conversation to OpenAI's servers or rack up massive API bills. In 2026, running powerful language models locally is not just possible it's practical, private, and often faster than cloud al...]]></description><link>https://yogeshbhawsar.com/build-a-privacy-first-ai-chatbot-with-ollama-tanstack-ai-and-react</link><guid isPermaLink="true">https://yogeshbhawsar.com/build-a-privacy-first-ai-chatbot-with-ollama-tanstack-ai-and-react</guid><category><![CDATA[AI]]></category><category><![CDATA[ollama]]></category><category><![CDATA[React]]></category><category><![CDATA[chatbot]]></category><category><![CDATA[tanstack]]></category><dc:creator><![CDATA[Yogesh Bhawsar]]></dc:creator><pubDate>Mon, 09 Feb 2026 11:47:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770637824031/942f5abf-5d7e-4cdf-832c-751b679bb582.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building AI applications doesn't mean you have to send every conversation to OpenAI's servers or rack up massive API bills. In 2026, running powerful language models locally is not just possible it's practical, private, and often faster than cloud alternatives.</p>
<p>I’ll show you how to build an AI chatbot that runs entirely on your infrastructure using three cutting-edge technologies:</p>
<ul>
<li><p><strong>Ollama</strong> - Run LLMs locally with zero configuration</p>
</li>
<li><p><strong>TanStack AI</strong> - Framework-agnostic, type-safe AI SDK with no vendor lock-in</p>
</li>
<li><p><strong>React</strong> - Build a modern, responsive chat interface</p>
</li>
</ul>
<hr />
<h2 id="heading-why-local-ai-matters-in-2026">Why Local AI Matters in 2026</h2>
<p>The AI landscape has fundamentally shifted. What required datacenter-scale infrastructure in 2023 now runs on a laptop. Here's why developers are choosing local AI:</p>
<h3 id="heading-privacy-amp-compliance">Privacy &amp; Compliance</h3>
<ul>
<li><p><strong>GDPR/CCPA compliance</strong> becomes trivial when data never leaves your infrastructure</p>
</li>
<li><p><strong>Healthcare/finance apps</strong> can use AI without HIPAA/PCI concerns</p>
</li>
<li><p><strong>No telemetry</strong> - your conversations aren't training someone else's model</p>
</li>
<li><p><strong>Corporate secrets stay secret</strong> - code assistance without sending source to third parties</p>
</li>
</ul>
<hr />
<h2 id="heading-prerequisites">Prerequisites</h2>
<h3 id="heading-required-software">Required Software</h3>
<ul>
<li><p><strong>node.js</strong></p>
</li>
<li><p><strong>npm or pnpm or bun</strong></p>
</li>
<li><p><strong>Ollama</strong></p>
</li>
</ul>
<h3 id="heading-checking-your-setup">Checking Your Setup</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Verify Node.js</span>
node --version  <span class="hljs-comment"># Should be v18.0.0 or higher</span>

<span class="hljs-comment"># Verify npm</span>
npm --version
</code></pre>
<hr />
<h2 id="heading-part-1-setting-up-ollama">Part 1: Setting Up Ollama</h2>
<h3 id="heading-step-1-install-ollama">Step 1: Install Ollama</h3>
<pre><code class="lang-bash">brew install ollama
</code></pre>
<h3 id="heading-step-2-verify-installation">Step 2: Verify Installation</h3>
<pre><code class="lang-bash">ollama --version
<span class="hljs-comment"># Should output: ollama version is X.X.X</span>
</code></pre>
<p>Ollama automatically starts a background service on <code>http://localhost:11434</code>.</p>
<h3 id="heading-step-3-pull-a-model">Step 3: Pull a Model</h3>
<p>We'll use <strong>DeepSeek-R1:1.5B</strong> - a small, fast model perfect for development:</p>
<pre><code class="lang-bash">ollama pull deepseek-r1:1.5b
</code></pre>
<p><strong>First pull takes 5-10 minutes</strong> (1.5GB download). Subsequent pulls are instant.</p>
<h3 id="heading-step-4-test-the-model">Step 4: Test the Model</h3>
<pre><code class="lang-bash">ollama run deepseek-r1:1.5b
</code></pre>
<p>You'll get an interactive prompt:</p>
<pre><code class="lang-plaintext">&gt;&gt;&gt; Hello! How are you?
I'm doing well, thank you for asking! How can I help you today?

&gt;&gt;&gt; /bye  # Exit with /bye
</code></pre>
<p><strong>Success!</strong> Ollama is running. Press Ctrl+D or type <code>/bye</code> to exit.</p>
<h3 id="heading-step-5-test-the-api">Step 5: Test the API</h3>
<pre><code class="lang-bash">curl http://localhost:11434/api/generate -d <span class="hljs-string">'{
  "model": "deepseek-r1:1.5b",
  "prompt": "Why is the sky blue?",
  "stream": false
}'</span>
</code></pre>
<p>You should get a JSON response with the model's answer. This confirms the API is working.</p>
<hr />
<h2 id="heading-part-2-building-the-backend-with-tanstack-ai">Part 2: Building the Backend with TanStack AI</h2>
<h3 id="heading-project-setup">Project Setup</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Create project directory</span>
mkdir ollama-chatbot
<span class="hljs-built_in">cd</span> ollama-chatbot

<span class="hljs-comment"># Initialize Node.js project</span>
npm init -y

<span class="hljs-comment"># Install dependencies</span>
npm install @tanstack/ai @tanstack/ai-ollama express cors dotenv
npm install -D typescript @types/node @types/express @types/cors tsx

<span class="hljs-comment"># Initialize TypeScript</span>
npx tsc --init
</code></pre>
<h3 id="heading-configure-typescript">Configure TypeScript</h3>
<p>Edit <code>tsconfig.json</code>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"target"</span>: <span class="hljs-string">"ES2022"</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"ESNext"</span>,
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"bundler"</span>,
    <span class="hljs-attr">"esModuleInterop"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"skipLibCheck"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"outDir"</span>: <span class="hljs-string">"./dist"</span>,
    <span class="hljs-attr">"rootDir"</span>: <span class="hljs-string">"./src"</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"src/**/*"</span>],
  <span class="hljs-attr">"exclude"</span>: [<span class="hljs-string">"node_modules"</span>]
}
</code></pre>
<h3 id="heading-create-environment-config">Create Environment Config</h3>
<p>Create <code>.env</code>:</p>
<pre><code class="lang-bash">PORT=3001
OLLAMA_BASE_URL=http://localhost:11434
MODEL=deepseek-r1:1.5b
</code></pre>
<h3 id="heading-build-the-server">Build the Server</h3>
<p>Create <code>src/server.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> cors <span class="hljs-keyword">from</span> <span class="hljs-string">'cors'</span>;
<span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;
<span class="hljs-keyword">import</span> { chat, toServerSentEventsResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/ai'</span>;
<span class="hljs-keyword">import</span> { ollamaText } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/ai-ollama'</span>;

dotenv.config();

<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">const</span> PORT = process.env.PORT || <span class="hljs-number">3001</span>;
<span class="hljs-keyword">const</span> MODEL = process.env.MODEL || <span class="hljs-string">'deepseek-r1:1.5b'</span>;

app.use(cors());
app.use(express.json());

app.get(<span class="hljs-string">'/health'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.json({ 
    status: <span class="hljs-string">'ok'</span>, 
    model: MODEL,
    ollamaUrl: process.env.OLLAMA_BASE_URL 
  });
});

app.post(<span class="hljs-string">'/api/chat'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { messages } = req.body;

    <span class="hljs-keyword">if</span> (!messages || !<span class="hljs-built_in">Array</span>.isArray(messages)) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ error: <span class="hljs-string">'Messages array required'</span> });
    }

    <span class="hljs-keyword">const</span> stream = chat({
      adapter: ollamaText(MODEL),
      messages,
    });

    <span class="hljs-keyword">return</span> toServerSentEventsResponse(stream, res);

  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Chat error:'</span>, error);
    res.status(<span class="hljs-number">500</span>).json({ 
      error: error <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span> ? error.message : <span class="hljs-string">'Internal server error'</span> 
    });
  }
});

app.post(<span class="hljs-string">'/api/chat/simple'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { messages } = req.body;

    <span class="hljs-keyword">const</span> stream = chat({
      adapter: ollamaText(MODEL),
      messages,
    });

    <span class="hljs-comment">// Collect full response</span>
    <span class="hljs-keyword">let</span> fullResponse = <span class="hljs-string">''</span>;
    <span class="hljs-keyword">for</span> <span class="hljs-keyword">await</span> (<span class="hljs-keyword">const</span> chunk <span class="hljs-keyword">of</span> stream) {
      <span class="hljs-keyword">if</span> (chunk.type === <span class="hljs-string">'text-delta'</span>) {
        fullResponse += chunk.text;
      }
    }

    res.json({ response: fullResponse });

  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Chat error:'</span>, error);
    res.status(<span class="hljs-number">500</span>).json({ 
      error: error <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span> ? error.message : <span class="hljs-string">'Internal server error'</span> 
    });
  }
});

app.listen(PORT, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server running on http://localhost:<span class="hljs-subst">${PORT}</span>`</span>);
});
</code></pre>
<h3 id="heading-add-scripts-to-packagejson">Add Scripts to package.json</h3>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"tsx watch src/server.ts"</span>,
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"tsc"</span>,
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"node dist/server.js"</span>
  }
}
</code></pre>
<h3 id="heading-start-the-server">Start the Server</h3>
<pre><code class="lang-bash">npm run dev
</code></pre>
<h3 id="heading-test-the-backend">Test the Backend</h3>
<p><strong>Test health endpoint:</strong></p>
<pre><code class="lang-bash">curl http://localhost:3001/health
</code></pre>
<p><strong>Test simple chat:</strong></p>
<pre><code class="lang-bash">curl http://localhost:3001/api/chat/simple \
  -H <span class="hljs-string">"Content-Type: application/json"</span> \
  -d <span class="hljs-string">'{
    "messages": [
      {"role": "user", "content": "What is 2+2?"}
    ]
  }'</span>
</code></pre>
<p>Expected response:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"response"</span>: <span class="hljs-string">"2 + 2 equals 4."</span>
}
</code></pre>
<p><strong>Perfect!</strong> Your backend is working. Keep the server running and open a new terminal for the frontend.</p>
<hr />
<h2 id="heading-part-3-creating-the-react-frontend">Part 3: Creating the React Frontend</h2>
<h3 id="heading-setup-react-with-vite">Setup React with Vite</h3>
<p>In a <strong>new terminal</strong> (keep the backend running):</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> ollama-chatbot

npm create vite@latest frontend -- --template react-ts

<span class="hljs-built_in">cd</span> frontend
npm install

npm install @tanstack/react-query axios lucide-react
</code></pre>
<h3 id="heading-configure-development-server">Configure Development Server</h3>
<p>Edit <code>frontend/vite.config.ts</code> to proxy API requests:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { defineConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'vite'</span>;
<span class="hljs-keyword">import</span> react <span class="hljs-keyword">from</span> <span class="hljs-string">'@vitejs/plugin-react'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      <span class="hljs-string">'/api'</span>: {
        target: <span class="hljs-string">'http://localhost:3001'</span>,
        changeOrigin: <span class="hljs-literal">true</span>,
      },
    },
  },
});
</code></pre>
<p>This lets you call <code>/api/chat</code> from the frontend without CORS issues.</p>
<h3 id="heading-build-the-chat-interface">Build the Chat Interface</h3>
<p>Replace <code>frontend/src/App.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useState, useRef, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { Send, Bot, User, Loader2 } <span class="hljs-keyword">from</span> <span class="hljs-string">'lucide-react'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./App.css'</span>;

<span class="hljs-keyword">interface</span> Message {
  role: <span class="hljs-string">'user'</span> | <span class="hljs-string">'assistant'</span>;
  content: <span class="hljs-built_in">string</span>;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [messages, setMessages] = useState&lt;Message[]&gt;([]);
  <span class="hljs-keyword">const</span> [input, setInput] = useState(<span class="hljs-string">''</span>);
  <span class="hljs-keyword">const</span> [isStreaming, setIsStreaming] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> messagesEndRef = useRef&lt;HTMLDivElement&gt;(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> abortControllerRef = useRef&lt;AbortController | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);

  <span class="hljs-keyword">const</span> scrollToBottom = <span class="hljs-function">() =&gt;</span> {
    messagesEndRef.current?.scrollIntoView({ behavior: <span class="hljs-string">'smooth'</span> });
  };

  useEffect(<span class="hljs-function">() =&gt;</span> {
    scrollToBottom();
  }, [messages]);

  <span class="hljs-keyword">const</span> sendMessage = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!input.trim() || isStreaming) <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">const</span> userMessage: Message = { role: <span class="hljs-string">'user'</span>, content: input };
    <span class="hljs-keyword">const</span> newMessages = [...messages, userMessage];

    setMessages(newMessages);
    setInput(<span class="hljs-string">''</span>);
    setIsStreaming(<span class="hljs-literal">true</span>);

    abortControllerRef.current = <span class="hljs-keyword">new</span> AbortController();

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/chat'</span>, {
        method: <span class="hljs-string">'POST'</span>,
        headers: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
        body: <span class="hljs-built_in">JSON</span>.stringify({ messages: newMessages }),
        signal: abortControllerRef.current.signal,
      });

      <span class="hljs-keyword">if</span> (!response.ok) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`HTTP error! status: <span class="hljs-subst">${response.status}</span>`</span>);
      }

      <span class="hljs-keyword">const</span> reader = response.body?.getReader();
      <span class="hljs-keyword">const</span> decoder = <span class="hljs-keyword">new</span> TextDecoder();
      <span class="hljs-keyword">let</span> assistantMessage = <span class="hljs-string">''</span>;

      <span class="hljs-keyword">if</span> (!reader) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'No reader available'</span>);

      <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
        <span class="hljs-keyword">const</span> { done, value } = <span class="hljs-keyword">await</span> reader.read();
        <span class="hljs-keyword">if</span> (done) <span class="hljs-keyword">break</span>;

        <span class="hljs-keyword">const</span> chunk = decoder.decode(value, { stream: <span class="hljs-literal">true</span> });
        <span class="hljs-keyword">const</span> lines = chunk.split(<span class="hljs-string">'\n'</span>);

        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> line <span class="hljs-keyword">of</span> lines) {
          <span class="hljs-keyword">if</span> (line.startsWith(<span class="hljs-string">'data: '</span>)) {
            <span class="hljs-keyword">const</span> data = line.slice(<span class="hljs-number">6</span>);
            <span class="hljs-keyword">if</span> (data === <span class="hljs-string">'[DONE]'</span>) <span class="hljs-keyword">continue</span>;

            <span class="hljs-keyword">try</span> {
              <span class="hljs-keyword">const</span> parsed = <span class="hljs-built_in">JSON</span>.parse(data);
              <span class="hljs-keyword">if</span> (parsed.type === <span class="hljs-string">'text-delta'</span>) {
                assistantMessage += parsed.text;
                <span class="hljs-comment">// Update UI with streaming text</span>
                setMessages([
                  ...newMessages,
                  { role: <span class="hljs-string">'assistant'</span>, content: assistantMessage },
                ]);
              }
            } <span class="hljs-keyword">catch</span> (e) {
              <span class="hljs-comment">// Skip invalid JSON</span>
            }
          }
        }
      }
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-keyword">if</span> (error <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span> &amp;&amp; error.name === <span class="hljs-string">'AbortError'</span>) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Request cancelled'</span>);
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Chat error:'</span>, error);
        setMessages([
          ...newMessages,
          { 
            role: <span class="hljs-string">'assistant'</span>, 
            content: <span class="hljs-string">'Sorry, an error occurred. Please try again.'</span> 
          },
        ]);
      }
    } <span class="hljs-keyword">finally</span> {
      setIsStreaming(<span class="hljs-literal">false</span>);
      abortControllerRef.current = <span class="hljs-literal">null</span>;
    }
  };

  <span class="hljs-keyword">const</span> handleKeyPress = <span class="hljs-function">(<span class="hljs-params">e: React.KeyboardEvent</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (e.key === <span class="hljs-string">'Enter'</span> &amp;&amp; !e.shiftKey) {
      e.preventDefault();
      sendMessage();
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"app"</span>&gt;
      &lt;div className=<span class="hljs-string">"chat-container"</span>&gt;
        &lt;div className=<span class="hljs-string">"chat-header"</span>&gt;
          &lt;Bot className=<span class="hljs-string">"icon"</span> /&gt;
          &lt;h1&gt;Local AI Chatbot&lt;/h1&gt;
          &lt;span className=<span class="hljs-string">"status"</span>&gt;
            {isStreaming ? <span class="hljs-string">'Thinking...'</span> : <span class="hljs-string">'Ready'</span>}
          &lt;/span&gt;
        &lt;/div&gt;

        &lt;div className=<span class="hljs-string">"messages"</span>&gt;
          {messages.length === <span class="hljs-number">0</span> &amp;&amp; (
            &lt;div className=<span class="hljs-string">"empty-state"</span>&gt;
              &lt;Bot size={<span class="hljs-number">64</span>} className=<span class="hljs-string">"empty-icon"</span> /&gt;
              &lt;h2&gt;Start a conversation&lt;/h2&gt;
              &lt;p&gt;This chatbot runs entirely on your local machine using Ollama&lt;/p&gt;
            &lt;/div&gt;
          )}

          {messages.map(<span class="hljs-function">(<span class="hljs-params">msg, idx</span>) =&gt;</span> (
            &lt;div key={idx} className={<span class="hljs-string">`message <span class="hljs-subst">${msg.role}</span>`</span>}&gt;
              &lt;div className=<span class="hljs-string">"message-icon"</span>&gt;
                {msg.role === <span class="hljs-string">'user'</span> ? &lt;User size={<span class="hljs-number">20</span>} /&gt; : &lt;Bot size={<span class="hljs-number">20</span>} /&gt;}
              &lt;/div&gt;
              &lt;div className=<span class="hljs-string">"message-content"</span>&gt;
                {msg.content}
              &lt;/div&gt;
            &lt;/div&gt;
          ))}

          {isStreaming &amp;&amp; messages[messages.length - <span class="hljs-number">1</span>]?.role !== <span class="hljs-string">'assistant'</span> &amp;&amp; (
            &lt;div className=<span class="hljs-string">"message assistant"</span>&gt;
              &lt;div className=<span class="hljs-string">"message-icon"</span>&gt;
                &lt;Loader2 size={<span class="hljs-number">20</span>} className=<span class="hljs-string">"spinner"</span> /&gt;
              &lt;/div&gt;
              &lt;div className=<span class="hljs-string">"message-content typing"</span>&gt;
                Thinking...
              &lt;/div&gt;
            &lt;/div&gt;
          )}

          &lt;div ref={messagesEndRef} /&gt;
        &lt;/div&gt;

        &lt;div className=<span class="hljs-string">"input-container"</span>&gt;
          &lt;textarea
            value={input}
            onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setInput(e.target.value)}
            onKeyPress={handleKeyPress}
            placeholder=<span class="hljs-string">"Type your message... (Enter to send)"</span>
            disabled={isStreaming}
            rows={<span class="hljs-number">1</span>}
          /&gt;
          &lt;button 
            onClick={sendMessage} 
            disabled={!input.trim() || isStreaming}
            className=<span class="hljs-string">"send-button"</span>
          &gt;
            {isStreaming ? (
              &lt;Loader2 size={<span class="hljs-number">20</span>} className=<span class="hljs-string">"spinner"</span> /&gt;
            ) : (
              &lt;Send size={<span class="hljs-number">20</span>} /&gt;
            )}
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<h3 id="heading-start-the-frontend">Start the Frontend</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># In the frontend directory</span>
npm run dev
</code></pre>
<p>Open <a target="_blank" href="http://localhost:5173">http://localhost:5173</a> in your browser.</p>
<p><strong>Congratulations!</strong> You now have a working local AI chatbot. Try asking it questions:</p>
<ul>
<li><p>"What is TypeScript?"</p>
</li>
<li><p>"Write a haiku about local AI"</p>
</li>
<li><p>"Explain quantum computing in simple terms"</p>
</li>
</ul>
<hr />
<h2 id="heading-part-4-adding-streaming-responses">Part 4: Adding Streaming Responses</h2>
<p>The code above already implements streaming! Here's how it works:</p>
<h3 id="heading-backend-tanstack-ai-streaming">Backend: TanStack AI Streaming</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// In server.ts</span>
<span class="hljs-keyword">const</span> stream = chat({
  adapter: ollamaText(MODEL),
  messages,
});

<span class="hljs-keyword">return</span> toServerSentEventsResponse(stream, res);
</code></pre>
<p>TanStack AI's <code>toServerSentEventsResponse</code> automatically:</p>
<ol>
<li><p>Converts the async iterator to Server-Sent Events format</p>
</li>
<li><p>Sends <code>data:</code> prefixed chunks</p>
</li>
<li><p>Handles backpressure and errors</p>
</li>
<li><p>Sends <code>[DONE]</code> when complete</p>
</li>
</ol>
<h3 id="heading-frontend-sse-parsing">Frontend: SSE Parsing</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// In App.tsx</span>
<span class="hljs-keyword">const</span> reader = response.body?.getReader();
<span class="hljs-keyword">const</span> decoder = <span class="hljs-keyword">new</span> TextDecoder();

<span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
  <span class="hljs-keyword">const</span> { done, value } = <span class="hljs-keyword">await</span> reader.read();
  <span class="hljs-keyword">if</span> (done) <span class="hljs-keyword">break</span>;

  <span class="hljs-keyword">const</span> chunk = decoder.decode(value, { stream: <span class="hljs-literal">true</span> });
  <span class="hljs-keyword">const</span> lines = chunk.split(<span class="hljs-string">'\n'</span>);

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> line <span class="hljs-keyword">of</span> lines) {
    <span class="hljs-keyword">if</span> (line.startsWith(<span class="hljs-string">'data: '</span>)) {
      <span class="hljs-keyword">const</span> data = line.slice(<span class="hljs-number">6</span>);
      <span class="hljs-keyword">const</span> parsed = <span class="hljs-built_in">JSON</span>.parse(data);

      <span class="hljs-keyword">if</span> (parsed.type === <span class="hljs-string">'text-delta'</span>) {
        assistantMessage += parsed.text;
        setMessages([...newMessages, { 
          role: <span class="hljs-string">'assistant'</span>, 
          content: assistantMessage 
        }]);
      }
    }
  }
}
</code></pre>
<p>This creates the "typewriter effect" users expect from modern chatbots.</p>
<hr />
<h3 id="heading-key-takeaways">Key Takeaways</h3>
<ol>
<li><p><strong>Ollama makes local AI trivial</strong> - Install, pull model, run. That's it.</p>
</li>
<li><p><strong>TanStack AI provides vendor freedom</strong> - Switch providers by changing one line</p>
</li>
<li><p><strong>React gives you full UI control</strong> - Build exactly what your users need</p>
</li>
<li><p><strong>Performance is competitive</strong> - Often faster than cloud APIs</p>
</li>
<li><p><strong>Privacy is guaranteed</strong> - Your data never leaves your machine</p>
</li>
</ol>
<hr />
<h3 id="heading-fun-fact">Fun Fact</h3>
<p>I’m using this exact setup to help my wife translate medical documents and reports, and it’s working with about <strong>90–95% accuracy</strong> on the local setup. The model being used is <strong>translategemma:4b</strong></p>
<hr />
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><strong>Ollama:</strong> <a target="_blank" href="https://ollama.com">ollama.com</a></p>
</li>
<li><p><strong>TanStack AI:</strong> <a target="_blank" href="https://tanstack.com/ai">tanstack.com/ai</a></p>
</li>
<li><p><strong>Model Library:</strong> <a target="_blank" href="https://ollama.com/library">ollama.com/library</a></p>
</li>
</ul>
<h3 id="heading-recommended-reading">Recommended Reading</h3>
<ul>
<li><p><a target="_blank" href="https://ollama.com/blog/launch">Ollama Launch: One-Command AI Coding Assistants</a></p>
</li>
<li><p><a target="_blank" href="https://www.ibm.com/think/news/ai-tech-trends-predictions-2026">Small Language Models: The Efficient AI Future</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Why you should use Zustand over Redux.]]></title><description><![CDATA[What is Zustand
Zustand is one of the state management libraries for React. It is a simple and powerful way to manage the state in React.
Why Zustand is better than Redux?
- Redux is Complicated for beginners.
- We have to write more code for even sm...]]></description><link>https://yogeshbhawsar.com/why-you-should-use-zustand-over-redux</link><guid isPermaLink="true">https://yogeshbhawsar.com/why-you-should-use-zustand-over-redux</guid><category><![CDATA[zustand]]></category><category><![CDATA[React]]></category><category><![CDATA[Redux]]></category><category><![CDATA[State Management ]]></category><dc:creator><![CDATA[Yogesh Bhawsar]]></dc:creator><pubDate>Fri, 16 Dec 2022 08:20:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770628019677/e0d1f9de-62a4-422d-9858-c956d94ce7c9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-what-is-zustand">What is Zustand</h2>
<p><a target="_blank" href="https://github.com/pmndrs/zustand/">Zustand</a> is one of the state management libraries for React. It is a simple and powerful way to manage the state in React.</p>
<h2 id="heading-why-zustand-is-better-than-redux">Why Zustand is better than Redux?</h2>
<h4 id="heading-redux-is-complicated-for-beginners">- Redux is Complicated for beginners.</h4>
<h4 id="heading-we-have-to-write-more-code-for-even-small-ui-changes-in-redux">- We have to write more code for even small UI changes in Redux.</h4>
<h4 id="heading-too-much-boilerplate-code-in-redux">- Too much boilerplate code in Redux.</h4>
<h4 id="heading-its-easy-to-handle-multiple-stores-from-multiple-folders">- It's easy to handle multiple stores from multiple folders.</h4>
<h4 id="heading-easier-to-set-up-and-maintain-than-redux">- Easier to set up and maintain than Redux.</h4>
<hr />
<h5 id="heading-let-me-show-you-a-sample-code-for-redux-and-zustand-for-a-simple-counter-app">Let me show you a sample code for Redux and Zustand for a simple counter app.</h5>
<h2 id="heading-redux">Redux</h2>
<p>First, we'll set up a store and provider.</p>
<pre><code class="lang-tsx">import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

export type RootState = ReturnType&lt;typeof store.getState&gt;
export type AppDispatch = typeof store.dispatch
</code></pre>
<p>And in our index.tsx file we'll use the provider.</p>
<pre><code class="lang-tsx">import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { store } from './app/store'
import { Provider } from 'react-redux'

ReactDOM.render(
  &lt;Provider store={store}&gt;
    &lt;App /&gt;
  &lt;/Provider&gt;,
  document.getElementById('root')
)
</code></pre>
<p>Now We will create a slice to manage the state of the counter.</p>
<pre><code class="lang-tsx">counterSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

export interface CounterState {
  value: number
}

const initialState: CounterState = {
  value: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) =&gt; {
      state.value += 1
    },
    decrement: (state) =&gt; {
      state.value -= 1
    },
  },
})

export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer
</code></pre>
<h2 id="heading-zustand">Zustand</h2>
<p>In Zustand we just need to create a store and set the state values.</p>
<pre><code class="lang-tsx">import create from 'zustand'

interface ValueState {
  bears: number
  increment: (by: number) =&gt; void
  decrement: (by: number) =&gt; void
}

const useStore = create&lt;ValueState&gt;((set) =&gt; ({
  value: 0,
  increment: () =&gt; set((state) =&gt; ({ value: state.value + 1 })),
  decrement: () =&gt; set((state) =&gt; ({ value: state.value - 1 })),
}))
</code></pre>
<p>The main advantage of Zustand is that we can use multiple stores in one app. And we don't even need to wrap the whole app with the provider. We can just use the store in our components.</p>
<p>I have just finished my exams and started to work on my blog again. I will try to keep up with the progress. I hope you will like it. Let me know! I'm always open to <a target="_blank" href="https://twitter.com/yo9e5h">feedback</a>.</p>
]]></content:encoded></item><item><title><![CDATA[CSS Selectors and React]]></title><description><![CDATA[Believe it or not CSS selectors are a not-to-go for styling in React. I usually prefers TailwindCSS or styled-components for most of my projects, but sometimes you don't need extra package in your project.
The Issue with inline styles
There's no way ...]]></description><link>https://yogeshbhawsar.com/css-selectors-and-react</link><guid isPermaLink="true">https://yogeshbhawsar.com/css-selectors-and-react</guid><category><![CDATA[CSS]]></category><category><![CDATA[React]]></category><category><![CDATA[css_selector]]></category><dc:creator><![CDATA[Yogesh Bhawsar]]></dc:creator><pubDate>Mon, 17 May 2021 06:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/NYcUkFJuxg0/upload/v1658166254652/WDOWAGlXK.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Believe it or not CSS selectors are a not-to-go for styling in React. I usually prefers TailwindCSS or styled-components for most of my projects, but sometimes you don't need extra package in your project.</p>
<h2 id="heading-the-issue-with-inline-styles">The Issue with inline styles</h2>
<p>There's no way you can use inline styles in React as you use them traditionaly. You have to use a CSS class. I found a article by <a target="_blank" href="https://www.freecodecamp.org/news/react-styled-components-inline-styles-3-other-css-styling-approaches-with-examples/">FreeCodeCamp</a> about styles in react.</p>
<p>Well it gathered my attention about using CSS selectors with inline css. So I decided to write a small article about it. I hope you will find it useful.</p>
<h2 id="heading-are-there-advantages">Are there advantages?</h2>
<ul>
<li><p>No need for extra package.</p>
</li>
<li><p>And they are quick.</p>
</li>
<li><p>No need to use class.</p>
</li>
</ul>
<p>Today I'm going to show you how to mutate css selectors with useState and pointer events.:</p>
<pre><code class="lang-plaintext">function Text() {
return (
  &lt;div
    style={{
      color: 'red,
    }}&gt;
    Click Me
  &lt;/div&gt;
  )
}
</code></pre>
<p>Let's move the styles to another object.</p>
<pre><code class="lang-plaintext">const TextStyle = {
  color: 'red,
};

export function Text() {
 return (
  &lt;div
    style={TextStyle}&gt;
    Sample Text
  &lt;/div&gt;
  )
}
</code></pre>
<p>Now we are going to use useState hook to change the color of the Text on hover.</p>
<pre><code class="lang-plaintext">import { useState } from 'react'

const TextStyle = ({ hover }) =&gt; ({
  color: hover ? 'yellow' : 'red',
})

export default function Text() {
  const [hover, setHover] = useState(false)
  return (
    &lt;div
      style={TextStyle({ hover })}
      onPointerOver={() =&gt; setHover(true)}
      onPointerOut={() =&gt; setHover(false)}
    &gt;
      Click Me
    &lt;/div&gt;
  )
}
</code></pre>
<p>The useState and pointer events helped us to transform the color of Text. I rarely used inline styles in React before, but I think it's a good example.</p>
<p>Also this is my first post. I hope you will like it. Let me know! I'm always open to <a target="_blank" href="https://twitter.com/yo9e5h">feedback</a>.</p>
]]></content:encoded></item></channel></rss>