<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged react</title>
<link>https://coderlegion.com/tag/react</link>
<description>Powered by Question2Answer</description>
<item>
<title>What I've Been Improving Behind the Scenes</title>
<link>https://coderlegion.com/33205/what-ive-been-improving-behind-the-scenes</link>
<description>&lt;p&gt;Over the past few weeks, I've been spending more time refining Galaxy Movies rather than adding entirely new features.&lt;/p&gt;
&lt;p&gt;For anyone who hasn't come across it yet, Galaxy Movies is my movie and TV discovery project. It helps people find titles and check where they're streaming.&lt;/p&gt;
&lt;p&gt;A few recent changes have made a noticeable difference to the experience.&lt;/p&gt;
&lt;h4 id=&quot;giving-the-watchlist-a-better-starting-point&quot;&gt;Giving the watchlist a better starting point&lt;/h4&gt;
&lt;p&gt;One thing I wanted to improve was the empty watchlist experience.&lt;/p&gt;
&lt;p&gt;Previously, a new user could open the app and immediately be greeted with an empty page. Now the empty state includes a trending section with titles that can be added directly to the watchlist.&lt;/p&gt;
&lt;p&gt;The basic idea is to give users something to explore before they've saved anything.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;{watchlist.length === 0 ? (
  &amp;lt;WatchlistEmptyState&amp;gt;
    &amp;lt;TrendingRail
      title=&quot;Start Here&quot;
      onAdd={addToWatchlist}
    /&amp;gt;
  &amp;lt;/WatchlistEmptyState&amp;gt;
) : (
  &amp;lt;WatchlistGrid titles={watchlist} /&amp;gt;
)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's a small change, but it gives people a much better starting point.&lt;/p&gt;
&lt;h4 id=&quot;making-navigation-feel-smoother&quot;&gt;Making navigation feel smoother&lt;/h4&gt;
&lt;p&gt;I added a progress bar for page transitions so there's better feedback when navigating around the app.&lt;/p&gt;
&lt;p&gt;I also fixed an issue with streaming provider filters where search results could briefly appear before the provider lookup finished.&lt;/p&gt;
&lt;p&gt;The important part is keeping the loading state visible until the provider lookup is complete.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;if (isLoadingProviders) {
  return &amp;lt;SearchLoadingState /&amp;gt;;
}

return (
  &amp;lt;MovieGrid movies={filteredMovies} /&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prevents users from seeing results that haven't been filtered yet.&lt;/p&gt;
&lt;h4 id=&quot;spending-more-time-on-the-details&quot;&gt;Spending more time on the details&lt;/h4&gt;
&lt;p&gt;I've also been consolidating repeated UI elements into shared components. It's not the most exciting work to show off, but it makes future changes easier and keeps the interface more consistent.&lt;/p&gt;
&lt;p&gt;Instead of maintaining separate versions of the same card rail across the app, shared components can handle the common layout.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;CardRail
  title=&quot;Trending Now&quot;
  items={trendingTitles}
  renderItem={(title) =&amp;gt; (
    &amp;lt;MovieCard
      key={title.id}
      movie={title}
    /&amp;gt;
  )}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the SEO side, I revisited the sitemap to focus on pages that are more useful to search engines.&lt;/p&gt;
&lt;p&gt;I also added a yearly streaming changes accumulator to preserve arrivals and departures by provider. This will make it possible to build yearly streaming recaps using data collected throughout the year.&lt;/p&gt;
&lt;h4 id=&quot;what-s-next-&quot;&gt;What's next?&lt;/h4&gt;
&lt;p&gt;I'm still working on improving discovery and making Galaxy Movies more useful without adding unnecessary complexity.&lt;/p&gt;
&lt;p&gt;Building a product independently means constantly deciding where time is best spent. Sometimes the most useful improvements aren't new features. They're the smaller fixes that make everything work better.&lt;/p&gt;
&lt;p&gt;You can check out the latest version - &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.galaxymovies.app&quot;&gt;https://www.galaxymovies.app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Source code is available - &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/NickFasulo/galaxy-movies&quot;&gt;https://github.com/NickFasulo/galaxy-movies&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you're building a side project too, I'm curious how much of your development time goes toward new features versus improving what already exists.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33205/what-ive-been-improving-behind-the-scenes</guid>
<pubDate>Sat, 31 Oct 2026 07:26:42 +0000</pubDate>
</item>
<item>
<title>Stop Syncing State With useEffect: 4 Bugs That Disappear When You Delete the Effect</title>
<link>https://coderlegion.com/32955/stop-syncing-state-with-useeffect-4-bugs-that-disappear-when-you-delete-the-effect</link>
<description>&lt;p&gt;Here is a pattern I see in almost every React codebase I read:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const [fullName, setFullName] = useState('');

useEffect(() =&amp;gt; {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works. It also renders twice every time a name changes, shows a stale value for one frame, and is a bug waiting to happen when someone adds a third dependency and forgets to list it.&lt;/p&gt;
&lt;p&gt;The fix is to delete the effect. Here are four cases where that is the right move.&lt;/p&gt;
&lt;h3 id=&quot;why-these-effects-cause-bugs&quot;&gt;Why These Effects Cause Bugs&lt;/h3&gt;
&lt;p&gt;An effect runs &lt;strong&gt;after&lt;/strong&gt; React has painted the screen. So any state you set inside it means:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;React renders with the old value and paints it.&lt;/li&gt;
&lt;li&gt;The effect runs and calls &lt;code&gt;setState&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;React renders again with the new value.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The user can see step 1. That is the flicker. And every effect you add is another place where the dependency list can silently drift out of date.&lt;/p&gt;
&lt;h3 id=&quot;case-1-derived-values&quot;&gt;Case 1: Derived Values&lt;/h3&gt;
&lt;p&gt;If you can calculate something from props or state you already have, do not store it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
const [fullName, setFullName] = useState('');
useEffect(() =&amp;gt; {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

// After
const fullName = firstName + ' ' + lastName;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No state, no effect, no extra render, and it can never be out of sync, because it is recalculated every time.&lt;/p&gt;
&lt;p&gt;The same goes for filtering and sorting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before: two sources of truth
const [visible, setVisible] = useState(items);
useEffect(() =&amp;gt; {
  setVisible(items.filter((i) =&amp;gt; i.active));
}, [items]);

// After
const visible = items.filter((i) =&amp;gt; i.active);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the calculation is genuinely expensive, wrap it in &lt;code&gt;useMemo&lt;/code&gt;. Measure first. For a few hundred items, plain code is fine.&lt;/p&gt;
&lt;h3 id=&quot;case-2-resetting-state-when-a-prop-changes&quot;&gt;Case 2: Resetting State When a Prop Changes&lt;/h3&gt;
&lt;p&gt;This is the one people struggle with most:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function Profile({ userId }: { userId: string }) {
  const [comment, setComment] = useState('');

  useEffect(() =&amp;gt; {
    setComment(''); // reset when the user changes
  }, [userId]);

  return &amp;lt;textarea value={comment} onChange={(e) =&amp;gt; setComment(e.target.value)} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When &lt;code&gt;userId&lt;/code&gt; changes, the old comment shows for one frame, then clears. If you have several pieces of state, you need one reset line for each, and you will forget one.&lt;/p&gt;
&lt;p&gt;The fix is the &lt;code&gt;key&lt;/code&gt; prop:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function ProfilePage({ userId }: { userId: string }) {
  return &amp;lt;Profile key={userId} userId={userId} /&amp;gt;;
}

function Profile({ userId }: { userId: string }) {
  const [comment, setComment] = useState('');
  return &amp;lt;textarea value={comment} onChange={(e) =&amp;gt; setComment(e.target.value)} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the &lt;code&gt;key&lt;/code&gt; changes, React treats it as a &lt;strong&gt;different component&lt;/strong&gt;: it throws away the old one, including all its state, and mounts a fresh one. Every piece of state resets, with no effect and no flicker. Remember this one, because it solves a whole category of &quot;why is the old data still there&quot; bugs.&lt;/p&gt;
&lt;h3 id=&quot;case-3-logic-that-belongs-in-an-event-handler&quot;&gt;Case 3: Logic That Belongs in an Event Handler&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const [submitted, setSubmitted] = useState(false);

useEffect(() =&amp;gt; {
  if (submitted) {
    sendOrder(cart);
    setSubmitted(false);
  }
}, [submitted, cart]);

return &amp;lt;button onClick={() =&amp;gt; setSubmitted(true)}&amp;gt;Buy&amp;lt;/button&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This uses an effect as a delayed event handler. It is hard to follow, and it can fire again if &lt;code&gt;cart&lt;/code&gt; changes while &lt;code&gt;submitted&lt;/code&gt; is still true.&lt;/p&gt;
&lt;p&gt;Put the logic where the event happens:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function handleBuy() {
  sendOrder(cart);
}

return &amp;lt;button onClick={handleBuy}&amp;gt;Buy&amp;lt;/button&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rule of thumb: &lt;strong&gt;if something happens because the user did something, it belongs in an event handler. If it happens because the component appeared on screen, it can be an effect.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;case-4-notifying-a-parent&quot;&gt;Case 4: Notifying a Parent&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&amp;gt; {
  onChange(value);
}, [value, onChange]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This calls the parent after the render, so the parent and child briefly disagree, and it can trigger extra renders in both. Call the parent in the same handler that changes the value:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function handleChange(next: string) {
  setValue(next);
  onChange(next);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both updates are batched into one render.&lt;/p&gt;
&lt;h3 id=&quot;so-when-is-an-effect-right-&quot;&gt;So When Is an Effect Right?&lt;/h3&gt;
&lt;p&gt;Effects are for &lt;strong&gt;synchronizing with something outside React&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Subscribing to a browser event, a WebSocket or a timer&lt;/li&gt;
&lt;li&gt;Starting and cleaning up a third-party widget&lt;/li&gt;
&lt;li&gt;Sending an analytics event because a page was shown&lt;/li&gt;
&lt;li&gt;Fetching data on mount (with a cleanup, so a slow response cannot overwrite a newer one)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point deserves care, because an effect that fetches can show old results if responses arrive out of order. I wrote about that race condition in a separate article about search inputs. If your library offers one, a data-fetching library or a framework's server data loading removes the need for the effect altogether.&lt;/p&gt;
&lt;h3 id=&quot;quick-checklist-before-you-write-an-effect&quot;&gt;Quick Checklist Before You Write an Effect&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Question &lt;/th&gt;&lt;th&gt;If yes&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Can I calculate it from existing props or state? &lt;/td&gt;&lt;td&gt;Calculate it during render&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Am I resetting state when a prop changes? &lt;/td&gt;&lt;td&gt;Use &lt;code&gt;key&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Does it happen because the user did something? &lt;/td&gt;&lt;td&gt;Put it in the event handler&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Am I telling the parent something changed? &lt;/td&gt;&lt;td&gt;Call the parent in the handler&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Am I connecting to something outside React? &lt;/td&gt;&lt;td&gt;An effect is right&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;the-rule&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;An effect that only calls &lt;code&gt;setState&lt;/code&gt; based on other state or props is almost always a bug waiting to happen.&lt;/strong&gt; Compute it during render, reset with &lt;code&gt;key&lt;/code&gt;, and handle events in handlers.&lt;/p&gt;
&lt;p&gt;Quick audit: search your project for &lt;code&gt;useEffect&lt;/code&gt; and read the body of each one. Count how many do nothing but set state. Each one is a candidate for deletion, and the component usually gets shorter and faster when you remove it.&lt;/p&gt;
&lt;p&gt;If you build with Next.js and React and want a clean starting point, you can browse my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;templates on pixelanas.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What is the strangest &lt;code&gt;useEffect&lt;/code&gt; you have ever found in a codebase? Share it in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32955/stop-syncing-state-with-useeffect-4-bugs-that-disappear-when-you-delete-the-effect</guid>
<pubDate>Thu, 29 Oct 2026 10:55:28 +0000</pubDate>
</item>
<item>
<title>Mastering the Native Drag and Drop API in React</title>
<link>https://coderlegion.com/32953/mastering-the-native-drag-and-drop-api-in-react</link>
<description>&lt;h3 id=&quot;what-is-the-drag-and-drop-api-&quot;&gt;What is the Drag and Drop API?&lt;/h3&gt;
&lt;p&gt;The Drag and Drop API is a built-in browser capability that allows web applications to make elements on a webpage interactive through dragging and dropping. Instead of forcing users to click buttons or select items from drop-down menus, this browser feature enables users to click on an item, hold down the mouse button or touchscreen, move the item across the screen, and release it over a designated area. The browser automatically tracks the movement coordinates, mouse state, and transfer data throughout the entire process.&lt;/p&gt;
&lt;p&gt;At its core, this API exposes a set of event listeners—hooks provided by the browser to run code when specific interactions happen—that fire as an item is picked up, dragged over drop targets, and finally released.&lt;/p&gt;
&lt;h3 id=&quot;a-real-life-analogy-the-corkboard-and-sticky-notes&quot;&gt;A Real-Life Analogy: The Corkboard and Sticky Notes&lt;/h3&gt;
&lt;p&gt;Imagine you have a physical corkboard on your office wall divided into three columns: &quot;To Do,&quot; &quot;In Progress,&quot; and &quot;Done.&quot; Each task is written on a physical sticky note.&lt;/p&gt;
&lt;p&gt;When you decide to start working on a task, you place your finger on a sticky note in the &quot;To Do&quot; column, lift it off the board, hover it across the room over to the &quot;In Progress&quot; column, and press it onto the corkboard. During this entire movement, you are carrying both the physical note and the context of what task is written on it. The board itself does not need to send a letter to an assistant asking to move the note; your hands handle the position, while the board simply provides the surface to receive it.&lt;/p&gt;
&lt;p&gt;This is precisely how the Drag and Drop API works on a screen. The draggable element acts as the sticky note, the data payload attached to the drag event acts as the text on the note, and the drop target acts as the receiving column on the corkboard.&lt;/p&gt;
&lt;h3 id=&quot;why-it-matters-in-tech-today&quot;&gt;Why It Matters in Tech Today&lt;/h3&gt;
&lt;p&gt;In modern web engineering, user experience (UX) directly impacts engagement and product adoption. Complex interfaces like Kanban boards (such as Trello or Jira), file management dashboards (like Google Drive), and custom page builders (like Wix or Squarespace) rely heavily on drag-and-drop interactions to make software feel natural and tactile.&lt;/p&gt;
&lt;p&gt;Engineers use the native Drag and Drop API to create fluid interfaces without dragging down performance. Without this browser-native API, software developers would have to write complex, prone-to-bugs math scripts that manually track mouse pixel coordinates on every screen frame. Utilizing the browser's built-in events prevents memory leaks, reduces CPU usage, and saves hundreds of hours of manual positioning logic.&lt;/p&gt;
&lt;p&gt;Furthermore, when building file uploading components, this API allows users to drag files straight from their computer's desktop into a browser window, bridging the gap between desktop operating systems and web applications.&lt;/p&gt;
&lt;h3 id=&quot;building-a-drag-and-drop-task-list-in-react&quot;&gt;Building a Drag and Drop Task List in React&lt;/h3&gt;
&lt;p&gt;Below is a simple React component that uses the browser's native Drag and Drop API to move tasks between two lists.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React, { useState } from 'react';

export default function TaskBoard() {
  const [tasks, setTasks] = useState([
    { id: '1', title: 'Design Database Schema', status: 'todo' },
    { id: '2', title: 'Build Express Route', status: 'todo' },
  ]);

  // Triggered when the user starts dragging an item
  const handleDragStart = (event, id) =&amp;gt; {
    event.dataTransfer.setData('text/plain', id);
  };

  // Necessary to allow dropping into a container
  const handleDragOver = (event) =&amp;gt; {
    event.preventDefault();
  };

  // Triggered when the item is dropped
  const handleDrop = (event, targetStatus) =&amp;gt; {
    event.preventDefault();
    const taskId = event.dataTransfer.getData('text/plain');
    
    setTasks((prevTasks) =&amp;gt;
      prevTasks.map((task) =&amp;gt;
        task.id === taskId ? { ...task, status: targetStatus } : task
      )
    );
  };

  return (
    &amp;lt;div style={{ display: 'flex', gap: '20px', padding: '20px' }}&amp;gt;
      {['todo', 'done'].map((status) =&amp;gt; (
        &amp;lt;div
          key={status}
          onDragOver={handleDragOver}
          onDrop={(e) =&amp;gt; handleDrop(e, status)}
          style={{
            width: '200px',
            minHeight: '200px',
            border: '2px dashed #ccc',
            padding: '10px',
          }}
        &amp;gt;
          &amp;lt;h3&amp;gt;{status.toUpperCase()}&amp;lt;/h3&amp;gt;
          {tasks
            .filter((task) =&amp;gt; task.status === status)
            .map((task) =&amp;gt; (
              &amp;lt;div
                key={task.id}
                draggable
                onDragStart={(e) =&amp;gt; handleDragStart(e, task.id)}
                style={{
                  padding: '10px',
                  marginBottom: '8px',
                  backgroundColor: '#4A90E2',
                  color: '#fff',
                  cursor: 'grab',
                }}
              &amp;gt;
                {task.title}
              &amp;lt;/div&amp;gt;
            ))}
        &amp;lt;/div&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;key-takeaway&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;The Drag and Drop API transforms rigid web interactions into intuitive, tactile experiences by letting the browser perform the heavy lifting of mouse tracking and state transfers. Mastering this native web feature enables engineers to craft modern, highly interactive web applications with minimal overhead and zero external dependencies.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/demystifying-webs-built-in-drag-and.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32953/mastering-the-native-drag-and-drop-api-in-react</guid>
<pubDate>Thu, 29 Oct 2026 10:24:46 +0000</pubDate>
</item>
<item>
<title>Redux Refactoring Playbook: From Action-Types Soup to RTK Query — Without Rewriting the App</title>
<link>https://coderlegion.com/32949/redux-refactoring-playbook-from-action-types-soup-to-rtk-query-without-rewriting-the-app</link>
<description>&lt;p&gt;Every React codebase of a certain age has one: the legacy Redux store. Hundreds of thunk actions, manually managed loading flags, per-feature cache invalidation, and a polling timer per data type pulling the full payload whether anything changed or not.&lt;/p&gt;
&lt;p&gt;Ours lived under a real-time monitoring dashboard tracking 1,000+ GSM installations at Orange Romania — and by the time we migrated to RTK Query, the story ended with dashboard responsiveness up ~40%, telemetry traffic down 20%, and &quot;frozen dashboard&quot; complaints down 30%.&lt;/p&gt;
&lt;p&gt;The previous article told that performance story. This one is the refactoring &lt;em&gt;mechanics&lt;/em&gt;: in what order things moved, which decisions unlocked the next one, and the one rule that made a no-rewrite migration possible.&lt;/p&gt;
&lt;h3 id=&quot;rule-1-strangle-don-t-rewrite&quot;&gt;Rule 1 — Strangle, don't rewrite&lt;/h3&gt;
&lt;p&gt;The store wasn't broken in one place; it was broken &lt;em&gt;everywhere&lt;/em&gt;: a switch-case reducer file nobody fully read, five middleware, &lt;code&gt;isLoading&lt;/code&gt; / &lt;code&gt;isRefreshing&lt;/code&gt; / &lt;code&gt;hasError&lt;/code&gt; flag families per resource, and component-level &lt;code&gt;useEffect&lt;/code&gt; chains firing fetches in a specific order &quot;because it worked.&quot;&lt;/p&gt;
&lt;p&gt;A big-bang rewrite was a non-starter: the dashboard was used every day by operations teams. So the rule was:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One page migrates per PR. The rest of the app keeps running on the legacy store untouched.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Redux makes this possible because &lt;code&gt;combineReducers&lt;/code&gt; is natural strangler scaffolding:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const rootReducer = combineReducers({
  // new pages live here
  ordersApi: ordersApi.reducer,          // RTK Query
  invoices: invoicesSlice.reducer,       // RTK slice
  // legacy pages keep their ducks until their turn
  legacyTelemetry: legacyTelemetryReducer, // switch-case reducer, retired last
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each migration PR carried its own before/after render counts and INP p75 from a fixed scripted interaction. If the numbers moved the wrong way, the PR didn't merge — refactoring without a measurement loop is just rewriting with extra steps.&lt;/p&gt;
&lt;p&gt;The code was the easy half. The pitch mattered as much: I sold this as risk control, not cleanup. Every PR shippable alone meant no feature-freeze window that anyone had to buy; the cost I did have to name up front was the parallel-store tax: two stores side by side meant duplicated fixtures and slower reviews while a slice waited for its turn. Leadership signed off because every migration PR ended with a measurement, not with the word &quot;done&quot;. One more org-side lesson: the change-flag contract needed a backend buy-in, and I opened that conversation politely, late. Open it on day one; that delay cost us a month.&lt;/p&gt;
&lt;h3 id=&quot;rule-2-selectors-are-the-api-freeze-them-first-&quot;&gt;Rule 2 — Selectors are the API. Freeze them first.&lt;/h3&gt;
&lt;p&gt;The trick that removed 90% of the migration risk: &lt;strong&gt;components never touched state shapes directly.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The legacy store had selector functions (&lt;code&gt;selectOrders&lt;/code&gt;, &lt;code&gt;selectNodeById&lt;/code&gt;, &lt;code&gt;selectIsNodeActive&lt;/code&gt;). Before migrating anything, we froze them as the contract and then made each selector a re-export shim that could source data from either world:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// selectors/orders.js
import { legacyOrders } from './legacySelectors';
import { useGetOrdersQuery } from '../api/ordersApi';

export function useOrders(selector) {
  const { data, isFetching } = useGetOrdersQuery();
  // legacy pages keep working against the same contract
  return useMemo(() =&amp;gt; selector(data ?? legacyOrders()), [data, selector]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Simplified — the real shims carried fallbacks during the window when a page's data half-lived in the old store. The point is the shape: &lt;strong&gt;the component-facing API never moved, only its plumbing did.&lt;/strong&gt;)&lt;/p&gt;
&lt;p&gt;Result: a &quot;migration PR&quot; changed the data layer under a page, not forty components over it. That made page-by-page migration realistic rather than a nice idea.&lt;/p&gt;
&lt;h3 id=&quot;rule-3-kill-loading-flags-with-rtk-query-not-with-better-thunks&quot;&gt;Rule 3 — Kill loading flags with RTK Query, not with better thunks&lt;/h3&gt;
&lt;p&gt;There was a middle step available: keep the thunks and clean them up. I'd have kept every race. Request A finishing after request B silently overwrote fresher data, and that was our most common production bug, because each flag family carried its own private version of it.&lt;/p&gt;
&lt;p&gt;So server state moved into RTK Query, and components declare what they need, not how to fetch it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// before: every call site, every page
dispatch(fetchOrders());
// + component-local: isLoading, ordersError, lastRefreshed...

// after: the component declares the requirement
const { data: orders, isLoading } = useGetOrdersQuery(undefined, {
  pollingInterval: 0, // see Rule 4 — we don't poll blindly
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Seventeen loading-flag families disappeared in this step. After it, a component tracking its own fetch lifecycle existed in exactly one place: a page that legitimately needed manual control.&lt;/p&gt;
&lt;h3 id=&quot;rule-4-polling-moves-from-timers-to-tags&quot;&gt;Rule 4 — Polling moves from timers to tags&lt;/h3&gt;
&lt;p&gt;Old polling: one &lt;code&gt;setInterval&lt;/code&gt; per data type, full payload every tick. On a dashboard watching 1,000+ nodes, that's thousands of identical requests an hour and constant re-render churn over data that hadn't changed.&lt;/p&gt;
&lt;p&gt;New polling is no polling — it's a &lt;strong&gt;change-flag endpoint&lt;/strong&gt;: a ten-byte version string the dashbo&lt;br&gt;
ard polls on a timer, and the real data refetches when that string actually changes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Nodes'],
  endpoints: (build) =&amp;gt; ({
    nodesChanged: build.query&amp;lt;{ version: string }, void&amp;gt;({
      query: () =&amp;gt; 'nodes/changed',       // returns a version string only
    }),
    getNodes: build.query&amp;lt;NodeState[], void&amp;gt;({
      query: () =&amp;gt; 'nodes',
      providesTags: ['Nodes'],
    }),
    // refetch tagged data only when the flag version changes:
    // a listener middleware polls `nodesChanged` (cheap, ~10 lines) and,
    // on version change, dispatches api.util.invalidateTags(['Nodes'])
  }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const flagListener = createListenerMiddleware();

flagListener.startListening({
  predicate: (action, currentState, previousState) =&amp;gt; {
    // the version lives in RTK Query's own cache, so the store
    // snapshot gives us both &quot;current&quot; and &quot;what it used to be&quot;
    const cur = nodesChanged.select()(currentState)?.data;
    const prev = nodesChanged.select()(previousState)?.data;
    return cur != null &amp;amp;&amp;amp; cur !== prev;
  },
  effect: (action, api) =&amp;gt; {
    api.dispatch(api.util.invalidateTags(['Nodes']));
  },
});

// the flag endpoint itself: cheap on purpose
//   query: () =&amp;gt; 'nodes/changed',   // returns ~10 bytes: a version string
//   pollingInterval: 5000,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What happens on an update, end to end: the version goes into the cache and&lt;br&gt;
&lt;code&gt;predicate&lt;/code&gt; sees it differ from the previous snapshot, &lt;code&gt;invalidateTags&lt;/code&gt; marks&lt;br&gt;
every tagged entry stale &lt;strong&gt;without removing it&lt;/strong&gt; (the screen keeps the old&lt;br&gt;
data), mounted subscribers immediately refetch — deduplicated to one request&lt;br&gt;
per endpoint+args no matter how many components listen — and one re-render&lt;br&gt;
lands the new data. A component sees &lt;code&gt;isLoading: false, isFetching: true&lt;/code&gt;&lt;br&gt;
during that window, which is why nothing flashes. If the flag endpoint dies,&lt;br&gt;
data simply goes stale until the next successful tick; nothing breaks. If&lt;br&gt;
nodes changed five times between two ticks, one refetch covers it: the&lt;br&gt;
version is a snapshot of &quot;what is true now&quot;, not an event queue.&lt;/p&gt;
&lt;h3 id=&quot;what-stayed-in-redux-core&quot;&gt;What stayed in Redux core&lt;/h3&gt;
&lt;p&gt;A mistake we avoided (mostly): RTK Query is for &lt;strong&gt;server state only&lt;/strong&gt;. UI-owned state — selected node, filter panel state, column layout, wizard steps — stayed in plain &lt;code&gt;createSlice&lt;/code&gt;. Migrating that into queries would have replaced a working pattern with a worse one.&lt;/p&gt;
&lt;p&gt;The end state: slices for decisions, RTK Query for data, zero custom cache code, zero hand-rolled loading flags.&lt;/p&gt;
&lt;h3 id=&quot;the-honest-ledger&quot;&gt;The honest ledger&lt;/h3&gt;
&lt;p&gt;From the published case, measured as &lt;em&gt;perceived dashboard responsiveness&lt;/em&gt; (how fast fresh data lands after a change, without UI-thread churn):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dashboard responsiveness: &lt;strong&gt;+~40%&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&quot;Frozen dashboard&quot; complaints: &lt;strong&gt;−30%&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;REST telemetry traffic: &lt;strong&gt;−20%&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What I can't give you is a LOC table with a precise count — we deleted more code than we wrote every migration PR, and by the end the store directory had visibly halved; I kept no exact tally, and a made-up precision here would be dishonest.&lt;/p&gt;
&lt;h3 id=&quot;what-i-d-do-differently&quot;&gt;What I'd do differently&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Start with RTK Query for any new page.&lt;/strong&gt; Half the migration tail was legacy thunks for features that could have been endpoints from day one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Freeze selectors before touching anything.&lt;/strong&gt; The component-facing API is the migration's unit of safety.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure per PR, not per epic.&lt;/strong&gt; The scripted interaction + INP check cost minutes per PR and caught two regressions a &quot;it looks fine&quot; test never would.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write the change-flag backend contract early.&lt;/strong&gt; The polling redesign was the single biggest win and it needed a backend buy-in — politeness about it cost us a month.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;your-refactoring-checklist&quot;&gt;Your refactoring checklist&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Inventory actions/thunks/loading flags — count them, this is your &quot;before&quot; baseline.&lt;/li&gt;
&lt;li&gt;Freeze selectors as the component-facing contract.&lt;/li&gt;
&lt;li&gt;Add RTK Query via &lt;code&gt;combineReducers&lt;/code&gt; alongside the legacy store.&lt;/li&gt;
&lt;li&gt;Migrate one page per PR; measure INP p75 + render count on a fixed interaction.&lt;/li&gt;
&lt;li&gt;Server state → endpoints with tags; UI state stays in slices.&lt;/li&gt;
&lt;li&gt;Replace timers with a change-flag contract before touching fetch logic.&lt;/li&gt;
&lt;li&gt;Delete: action-type strings, flag families, custom cache code. The deletion count is the score.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;The performance numbers come from the same production system as my earlier piece, &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/veaceslav/a-40-faster-fault-detection-pipeline-without-websockets-the-polling-trick-we-shipped-e28&quot;&gt;A 40% Faster Fault-Detection Pipeline Without WebSockets&lt;/a&gt; — that one tells the polling-protocol story; this is the data-layer refactoring around it.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32949/redux-refactoring-playbook-from-action-types-soup-to-rtk-query-without-rewriting-the-app</guid>
<pubDate>Thu, 29 Oct 2026 08:51:02 +0000</pubDate>
</item>
<item>
<title>LifeOps Project (Part 6): 360° Analytics Dashboard with Multi-Dimensional Filtering, Recharts and Co</title>
<link>https://coderlegion.com/32785/lifeops-project-part-360-analytics-dashboard-with-multi-dimensional-filtering-recharts</link>
<description>&lt;p&gt;In &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.datalaria.com/en/posts/app-lifeops_part5_deploy_mobile_pwa/&quot;&gt;Part 5 of our engineering series&lt;/a&gt;, we achieved a major milestone: deploying the entire &lt;strong&gt;LifeOps architecture 24/7 in production at zero cost ($0/month)&lt;/strong&gt; by combining Netlify, Render, and Supabase, optimizing mobile ergonomics for smartphones, and installing the app as a native PWA.&lt;/p&gt;
&lt;p&gt;However, once you start logging dozens of daily activities (workouts, bike rides, books read, movies watched, and Kanban tasks), a critical analytical challenge emerges: &lt;strong&gt;data without analysis lacks operational value&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Having 40 or 50 records in a PostgreSQL database is of little use if opening the dashboard doesn't immediately answer operational questions such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;“How many kilometers did I run this month compared to last month?”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“What percentage of my training went into sprint intervals versus long endurance runs?”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“How has my daily consistency evolved, and what is my current active day streak?”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“How can I filter exclusively by Running to inspect average pace and calories burned without mixing in gym workouts or movies?”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this sixth installment, we tackle the full evolution of the &lt;strong&gt;360° Analytics Dashboard&lt;/strong&gt;, addressing both the deterministic client-side aggregation design and the most notorious UX challenge of zero-cost serverless infrastructure: &lt;strong&gt;cloud container sleep periods (cold-starts)&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Try the analytics live&lt;/strong&gt;: You can test the new 360° Dashboard with interactive data in Sandbox mode at &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/#/?mock=true&amp;amp;lang=en&quot;&gt;https://datalaria.com/apps/lifeops/#/?mock=true&amp;amp;lang=en&lt;/a&gt; or log in with your Supabase account at &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/#/?lang=en&quot;&gt;https://datalaria.com/apps/lifeops/#/?lang=en&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;1-architecture-decision-external-bi-grafana-metabase-vs-native-react-engine-adr-013-&quot;&gt;1. Architecture Decision: External BI (Grafana / Metabase) vs. Native React Engine (ADR-013) ⚖️&lt;/h4&gt;
&lt;p&gt;When planning advanced charts and timeframe filters, the first engineering instinct is often to reach for established Business Intelligence tools like &lt;strong&gt;Grafana&lt;/strong&gt;, &lt;strong&gt;Metabase&lt;/strong&gt;, or &lt;strong&gt;Apache Superset&lt;/strong&gt;. However, evaluating our requirements against our project constraints led to architectural decision &lt;strong&gt;ADR-013&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Evaluation Factor &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;External BI Tool (Grafana / Metabase) &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Native React 19 + Recharts 3.10 Engine (Our Choice)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Multi-Tenant Security (RLS)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;❌ Requires static direct database connection. Passing individual Supabase Auth JWTs requires expensive Enterprise licenses or custom proxy tokens. &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✅ &lt;strong&gt;Perfect Isolation&lt;/strong&gt;: consumes secure user-scoped REST endpoints protected by Row-Level Security (&lt;code&gt;auth.uid() = user_id&lt;/code&gt;).&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Infrastructure Cost&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;❌ Requires a separate container running 24/7 with dedicated RAM, or paid cloud plans. &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✅ &lt;strong&gt;$0.00/month&lt;/strong&gt;: compiles directly into the Netlify static bundle.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Sandbox Demo Mode (&lt;code&gt;?mock=true&lt;/code&gt;)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;❌ Impossible to feed an external iframe using in-memory mock state. &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✅ &lt;strong&gt;100% Compatibility&lt;/strong&gt;: runs instantly with deterministic synthetic data.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Filter Latency&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;⚠️ Every click triggers a remote SQL round-trip to the cloud. &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✅ &lt;strong&gt;0 ms&lt;/strong&gt;: the timeframe dataset filters in client memory using pure &lt;code&gt;useMemo&lt;/code&gt; hooks.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Mobile UX &amp;amp; Aesthetics&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;⚠️ Visual disconnect from &lt;em&gt;Glassmorphism Dark Mode&lt;/em&gt; and nested iframe scroll friction on mobile. &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✅ &lt;strong&gt;Unified Design&lt;/strong&gt;: seamless responsive layout tailored to our design system.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5 id=&quot;data-flow-architecture&quot;&gt;Data Flow Architecture&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;API Layer&lt;/strong&gt;: FastAPI REST API / Supabase PostgreSQL (&lt;code&gt;GET /api/v1/activities/details?limit=200&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local Cache&lt;/strong&gt;: Stale-While-Revalidate (SWR) cache in &lt;code&gt;localStorage&lt;/code&gt; (&lt;code&gt;lifeops_swr_v1:&amp;lt;user_id&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client Engine&lt;/strong&gt;: Deterministic Client Analytics Engine (&lt;code&gt;lib/analyticsEngine.js&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Control Bar&lt;/strong&gt;: Timeframes (7D, 30D, Month, 90D, Year), period comparisons (&lt;code&gt;vs. prev.&lt;/code&gt;), and category/subcategory drill-downs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visual Suite&lt;/strong&gt;: 4 dynamic KPI cards with &lt;code&gt;Δ%&lt;/code&gt;, multi-metric &lt;code&gt;ComposedChart&lt;/code&gt; time-series, &lt;code&gt;PieChart&lt;/code&gt; donut breakdowns, and a 16-week consistency heatmap.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-multi-dimensional-cascading-filters-amp-period-over-period-deltas-&quot;&gt;2. Multi-Dimensional Cascading Filters &amp;amp; Period-over-Period Deltas ⏱️📊&lt;/h4&gt;
&lt;p&gt;The cornerstone of the new experience is the &lt;strong&gt;Analytics Control Bar&lt;/strong&gt;, enabling three levels of analytical exploration without page reloads:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.datalaria.com/en/posts/app-lifeops_part6_analytics_dashboard_360/lifeops_analytics_filters_drilldown_en.png&quot; alt=&quot;Analytics Control Bar in LifeOps&quot;&gt;&lt;/p&gt;
&lt;h5 id=&quot;a-timeframe-resolution&quot;&gt;A. Timeframe Resolution&lt;/h5&gt;
&lt;p&gt;The engine supports five time horizons powered by the deterministic &lt;code&gt;resolveTimeWindows()&lt;/code&gt; function:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;7 Days&lt;/code&gt; (&lt;code&gt;week&lt;/code&gt;)&lt;/strong&gt;: Rolling last 7 days vs. immediately preceding 7 days.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;30 Days&lt;/code&gt; (&lt;code&gt;month&lt;/code&gt;)&lt;/strong&gt;: Rolling last 30 days vs. immediately preceding 30 days.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;By Month&lt;/code&gt; (&lt;code&gt;calendar_month&lt;/code&gt;)&lt;/strong&gt;: Dynamic month selector (&lt;code&gt;January&lt;/code&gt; to &lt;code&gt;December&lt;/code&gt;) and year, compared against the preceding calendar month.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;90 Days&lt;/code&gt; (&lt;code&gt;quarter&lt;/code&gt;)&lt;/strong&gt;: Rolling last 90 days aggregated into weekly buckets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;By Year&lt;/code&gt; (&lt;code&gt;year&lt;/code&gt;)&lt;/strong&gt;: Full annual overview with 12 monthly buckets vs. prior year.&lt;/li&gt;
&lt;/ol&gt;
&lt;h5 id=&quot;b-invariant-inv-8-safe-percentage-delta-calculation-&quot;&gt;B. Invariant INV-8: Safe Percentage Delta Calculation (Δ%)&lt;/h5&gt;
&lt;p&gt;To compare current performance (&lt;code&gt;V_curr&lt;/code&gt;) against the previous period (&lt;code&gt;V_prev&lt;/code&gt;) without division-by-zero errors when starting a new discipline:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;V_prev = 0&lt;/code&gt; and &lt;code&gt;V_curr = 0&lt;/code&gt; → &lt;code&gt;Δ% = 0%&lt;/code&gt; (Stable).&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;V_prev = 0&lt;/code&gt; and &lt;code&gt;V_curr &amp;gt; 0&lt;/code&gt; → &lt;code&gt;Δ% = +100%&lt;/code&gt; (Baseline creation).&lt;/li&gt;
&lt;li&gt;In any other case → &lt;code&gt;Δ% = round(((V_curr - V_prev) / V_prev) * 100)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export function computeDelta(currentVal, prevVal, unit = '', language = 'en') {
  const curr = Number(currentVal) || 0;
  const prev = Number(prevVal) || 0;
  const vsLabel = language === 'es' ? 'vs. ant.' : 'vs. prev.';

  if (prev === 0 &amp;amp;&amp;amp; curr === 0) {
    return { positive: true, deltaPct: 0, text: 'Stable (0% vs. prev.)' };
  }
  if (prev === 0 &amp;amp;&amp;amp; curr &amp;gt; 0) {
    return { positive: true, deltaPct: 100, text: `+100% (+${curr}${unit} ${vsLabel})` };
  }

  const diff = curr - prev;
  const pct = Math.round((diff / prev) * 100);
  const sign = diff &amp;gt;= 0 ? '+' : '';
  return {
    positive: diff &amp;gt;= 0,
    deltaPct: pct,
    diff,
    text: `${sign}${pct}% (${sign}${diff.toFixed(1)}${unit} ${vsLabel})`,
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-the-visual-suite-with-recharts-contextual-kpis-time-series-amp-donut-charts-&quot;&gt;3. The Visual Suite with Recharts: Contextual KPIs, Time-Series &amp;amp; Donut Charts 📈🍩&lt;/h4&gt;
&lt;p&gt;Selecting a category or drilling down into a subcategory (for example, filtering by &lt;strong&gt;&lt;code&gt;Sport&lt;/code&gt; ➔ &lt;code&gt;Running&lt;/code&gt;&lt;/strong&gt;) dynamically updates the entire dashboard:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.datalaria.com/en/posts/app-lifeops_part6_analytics_dashboard_360/lifeops_dashboard_360_analytics_en.png&quot; alt=&quot;360 Analytics Dashboard View&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;4 Contextual KPI Cards&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;Sport&lt;/strong&gt;: adapt metrics to the selected discipline:&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Sessions&lt;/em&gt;: workout count with &lt;code&gt;Δ%&lt;/code&gt; badge and personal bests (PB).&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Distance&lt;/em&gt;: cumulative mileage and average km per session.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Time &amp;amp; Pace&lt;/em&gt;: active duration and average pace (&lt;code&gt;min/km&lt;/code&gt; in running or &lt;code&gt;km/h&lt;/code&gt; in cycling).&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Calories &amp;amp; Effort&lt;/em&gt;: calories burned, average heart rate (&lt;code&gt;bpm&lt;/code&gt;), and elevation gain (&lt;code&gt;+m&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-Metric Time-Series Chart (&lt;code&gt;ComposedChart&lt;/code&gt;)&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Renders current period bars color-coded by category and overlays a &lt;strong&gt;dashed trendline of the previous period&lt;/strong&gt; when the &lt;code&gt;⇄ Compare vs. prev.&lt;/code&gt; toggle is active.&lt;/li&gt;
&lt;li&gt;Built-in metric switcher: toggle on the fly between &lt;strong&gt;Sessions&lt;/strong&gt;, &lt;strong&gt;Distance (km)&lt;/strong&gt;, &lt;strong&gt;Calories (kcal)&lt;/strong&gt;, &lt;strong&gt;Duration (h)&lt;/strong&gt;, or &lt;strong&gt;Elevation Gain (m)&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subcategory Distribution (&lt;code&gt;PieChart&lt;/code&gt; Donut)&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Displays percentage and absolute shares for each activity type. Clicking any slice or legend item applies an automatic &lt;em&gt;drill-down&lt;/em&gt; into that subcategory.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;4-16-week-consistency-daily-heatmap-strava-github-style-&quot;&gt;4. 16-Week Consistency Daily Heatmap (Strava / GitHub Style) 🔥🗓️&lt;/h4&gt;
&lt;p&gt;Habit formation and athletic progression rely on consistency. To provide clear visual accountability, we built a 112-day grid (16 weeks × 7 days, Monday to Sunday):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.datalaria.com/en/posts/app-lifeops_part6_analytics_dashboard_360/lifeops_consistency_heatmap_streaks_en.png&quot; alt=&quot;Daily Consistency Heatmap&quot;&gt;&lt;/p&gt;
&lt;p&gt;The algorithm evaluates daily activity volume across 5 intensity tiers (&lt;code&gt;level-0&lt;/code&gt; to &lt;code&gt;level-4&lt;/code&gt;) and computes in real time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Active days&lt;/strong&gt; during the evaluated window.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Current active streak&lt;/strong&gt;: counting consecutive active days backwards from today or yesterday.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All-time best streak&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;5-overcoming-the-free-tier-cloud-ux-challenge-cold-start-mitigation-&quot;&gt;5. Overcoming the Free-Tier Cloud UX Challenge: Cold-Start Mitigation ⚡🛡️&lt;/h4&gt;
&lt;p&gt;Deploying full-stack web applications on free cloud tiers (such as Render.com's Free Web Service) introduces an unavoidable reality: &lt;strong&gt;the container sleeps after 15 minutes of inactivity&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When a user visits after several hours, the initial request takes &lt;strong&gt;20 to 40 seconds&lt;/strong&gt; while the virtual machine boots. In early versions, this lag produced a terrible UX: users saw blank charts and KPI cards showing &lt;code&gt;0&lt;/code&gt;, leading them to believe &lt;strong&gt;their data was lost or deleted&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;To eliminate this friction while staying strictly within our &lt;strong&gt;$0.00/month budget&lt;/strong&gt;, we engineered a 3-layer mitigation system:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.datalaria.com/en/posts/app-lifeops_part6_analytics_dashboard_360/lifeops_cloud_sync_banner_coldstart_en.png&quot; alt=&quot;Cloud Synchronization Banner &amp;amp; Cold Start Feedback&quot;&gt;&lt;/p&gt;
&lt;h5 id=&quot;layer-1-silent-pre-warming-wakeupserver-&quot;&gt;Layer 1: Silent Pre-Warming (&lt;code&gt;wakeUpServer&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;The moment the browser loads the SPA inside &lt;code&gt;AuthContext.jsx&lt;/code&gt;—before the user even finishes entering credentials or navigating to the Dashboard—a non-blocking background &lt;code&gt;fetch&lt;/code&gt; pings &lt;code&gt;/health&lt;/code&gt;. This gives the container a 5-to-10 second head start to spin up.&lt;/p&gt;
&lt;h5 id=&quot;layer-2-user-scoped-stale-while-revalidate-swr-local-cache&quot;&gt;Layer 2: User-Scoped Stale-While-Revalidate (SWR) Local Cache&lt;/h5&gt;
&lt;p&gt;Every read request (&lt;code&gt;GET /api/v1/activities/details&lt;/code&gt;, &lt;code&gt;/tasks&lt;/code&gt;, &lt;code&gt;/stats/dashboard&lt;/code&gt;) is cached in &lt;code&gt;localStorage&lt;/code&gt; under a strict user-prefixed key (&lt;code&gt;lifeops_swr_v1:&amp;lt;user_id&amp;gt;:&amp;lt;endpoint&amp;gt;&lt;/code&gt;).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On subsequent visits, cards and charts hydrate &lt;strong&gt;in under 10 milliseconds (effectively 0 ms)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The user sees their data immediately while background network calls fetch fresh updates.&lt;/li&gt;
&lt;li&gt;Any write operation (&lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;) automatically invalidates the local cache to guarantee consistency.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;layer-3-foreground-synchronization-banner-and-skeletons&quot;&gt;Layer 3: Foreground Synchronization Banner and Skeletons&lt;/h5&gt;
&lt;p&gt;If a user accesses the app from a clean browser and must wait through a cold start:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Never show misleading zeros&lt;/strong&gt;: KPI cards display waiting indicators (&lt;code&gt;···&lt;/code&gt;) instead of &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transparent Status Banner&lt;/strong&gt;: displays an animated spinner, live elapsed seconds counter (&lt;code&gt;0s, 1s, 2s...&lt;/code&gt;), visual progress bar, and active phase badges:&lt;ul&gt;
&lt;li&gt;&lt;code&gt;✓ Authenticated Session (Supabase Auth)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;⟳ Waking API Engine on Render Cloud&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;⏳ Hydrating Activities &amp;amp; 360° Analytics&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If the wait exceeds 4 seconds, the banner explicitly explains that the free cloud container is spinning up and that &lt;strong&gt;all data is 100% safe&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic Timeframe Fallback&lt;/strong&gt;: if the query returns activities but none fall within the last 30 days (e.g. after a vacation), the dashboard automatically switches to &lt;code&gt;90 Days&lt;/code&gt; or &lt;code&gt;By Year&lt;/code&gt; so charts appear populated immediately.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;6-conclusions-amp-links-&quot;&gt;6. Conclusions &amp;amp; Links 🚀&lt;/h4&gt;
&lt;p&gt;With this update, LifeOps evolves from a plain activity logger into a &lt;strong&gt;full personal operations intelligence system&lt;/strong&gt;. Combining deterministic client-side analytics with a resilient cold-start mitigation architecture proves that it is entirely possible to build fast, secure, and visually stunning software on a &lt;strong&gt;$0.00/month infrastructure bill&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🎮 &lt;strong&gt;Deployed Web App&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/&quot;&gt;https://datalaria.com/apps/lifeops/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🕹️ &lt;strong&gt;Interactive Sandbox Mode (No Registration)&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/#/?mock=true&amp;amp;lang=en&quot;&gt;https://datalaria.com/apps/lifeops/#/?mock=true&amp;amp;lang=en&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;📋 &lt;strong&gt;Official LifeOps Technical Overview&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/apps/lifeops/&quot;&gt;https://datalaria.com/en/apps/lifeops/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;If you enjoyed this technical breakdown, explore more open engineering articles and quantitative apps at &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.datalaria.com&quot;&gt;www.datalaria.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32785/lifeops-project-part-360-analytics-dashboard-with-multi-dimensional-filtering-recharts</guid>
<pubDate>Wed, 28 Oct 2026 08:18:20 +0000</pubDate>
</item>
<item>
<title>You Upgraded to Next.js 15 and Your Site Got Slower. The Caching Defaults Flipped</title>
<link>https://coderlegion.com/32719/you-upgraded-to-next-js-15-and-your-site-got-slower-the-caching-defaults-flipped</link>
<description>&lt;p&gt;You upgrade from Next.js 14 to 15. The app compiles, the tests pass, nothing visibly breaks. A week later your database bill is higher, your pages feel slower, and your Time to First Byte graph has a step in it that lines up exactly with the deploy.&lt;/p&gt;
&lt;p&gt;Nothing is broken. Something that used to happen for free stopped happening.&lt;/p&gt;
&lt;h3 id=&quot;what-changed&quot;&gt;What Changed&lt;/h3&gt;
&lt;p&gt;In Next.js 14, data fetching was cached &lt;strong&gt;by default&lt;/strong&gt;. A &lt;code&gt;fetch&lt;/code&gt; in a Server Component ran once, the result was stored, and later requests reused it. You had to opt &lt;em&gt;out&lt;/em&gt; of caching.&lt;/p&gt;
&lt;p&gt;In Next.js 15, the default flipped. Per the upgrade notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fetch&lt;/code&gt; requests are no longer cached by default.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;GET&lt;/code&gt; route handlers are no longer cached by default.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The client-side router cache no longer holds page segments between navigations by default.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So code you wrote once and never touched now behaves differently:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/products/page.tsx
export default async function ProductsPage() {
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();
  return &amp;lt;ProductList products={products} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On 14, this fetched the data once and served the page from cache. On 15, &lt;strong&gt;every visitor triggers the API call&lt;/strong&gt;, and because the page now depends on data fetched at request time, the route is rendered dynamically.&lt;/p&gt;
&lt;h3 id=&quot;why-it-is-easy-to-miss&quot;&gt;Why It Is Easy to Miss&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nothing errors.&lt;/strong&gt; The code is valid. It just does more work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dev mode hides it.&lt;/strong&gt; In &lt;code&gt;next dev&lt;/code&gt;, nearly everything is rendered per request anyway.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The slowdown is gradual.&lt;/strong&gt; With low traffic, an extra API call per request is invisible. It shows up when the traffic does.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Third-party limits bite later.&lt;/strong&gt; If the API you call has rate limits or per-request billing, you find out from the invoice.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;how-to-see-it-in-30-seconds&quot;&gt;How to See It in 30 Seconds&lt;/h3&gt;
&lt;p&gt;Run a production build and read the route table at the end:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;next build
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Route (app)
┌ ○ /                 (Static)
├ ƒ /products         (Dynamic)
└ ƒ /blog/[slug]      (Dynamic)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;○&lt;/code&gt; means the page is pre-rendered once at build time. &lt;code&gt;ƒ&lt;/code&gt; means it runs on the server for every request. A page that used to be static and is now &lt;code&gt;ƒ&lt;/code&gt; is exactly what this upgrade can cause. Compare the table before and after the upgrade.&lt;/p&gt;
&lt;h3 id=&quot;fix-1-cache-one-request-on-purpose&quot;&gt;Fix 1: Cache One Request on Purpose&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const res = await fetch('https://api.example.com/products', {
  cache: 'force-cache',
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This restores the old behavior for that single request. Use it for data that does not change per user and rarely changes at all.&lt;/p&gt;
&lt;h3 id=&quot;fix-2-cache-with-an-expiry&quot;&gt;Fix 2: Cache With an Expiry&lt;/h3&gt;
&lt;p&gt;Most data is not permanent, just slow-changing. Revalidate it on a timer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const res = await fetch('https://api.example.com/products', {
  next: { revalidate: 3600 }, // seconds
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Visitors get the cached copy, and after an hour the next request refreshes it in the background. This is the right default for product lists, blog posts and prices.&lt;/p&gt;
&lt;h3 id=&quot;fix-3-set-it-once-for-the-whole-page&quot;&gt;Fix 3: Set It Once for the Whole Page&lt;/h3&gt;
&lt;p&gt;If several fetches on a page should share the same freshness, declare it at the route level:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/products/page.tsx
export const revalidate = 3600;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything that page renders is treated as cacheable for an hour, without repeating options on each call.&lt;/p&gt;
&lt;h3 id=&quot;fix-4-route-handlers-need-their-own-opt-in&quot;&gt;Fix 4: Route Handlers Need Their Own Opt-In&lt;/h3&gt;
&lt;p&gt;A &lt;code&gt;GET&lt;/code&gt; route handler that returns the same data for everyone used to be cached automatically. Now you must say so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/products/route.ts
export const dynamic = 'force-static';

export async function GET() {
  const products = await getProducts();
  return Response.json(products);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without that line, the handler runs on every request.&lt;/p&gt;
&lt;h3 id=&quot;the-part-people-forget-your-database-was-never-covered&quot;&gt;The Part People Forget: Your Database Was Never Covered&lt;/h3&gt;
&lt;p&gt;Here is the subtle thing. The &lt;code&gt;fetch&lt;/code&gt; cache only ever applied to &lt;code&gt;fetch&lt;/code&gt;. If you read from MongoDB or Postgres directly with an ORM, this change did not touch those calls. Depending on whether the page used request data such as cookies, it was either rendered once at build time with the data frozen in, or rendered on every request. Neither is controlled by the fetch cache, so check your build table for those pages too.&lt;/p&gt;
&lt;p&gt;To cache a database call at runtime, wrap it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { unstable_cache } from 'next/cache';

const getProducts = unstable_cache(
  async () =&amp;gt; Product.find().lean(),
  ['products'],
  { revalidate: 3600, tags: ['products'] }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;tags&lt;/code&gt; option lets you clear that cache on demand with &lt;code&gt;revalidateTag('products')&lt;/code&gt; after an edit. I wrote about why &lt;code&gt;revalidateTag&lt;/code&gt; appears to do nothing when the data is not tagged in the first place. Newer Next.js versions keep moving toward explicit opt-in caching, so check the docs for the version you are on before copying any of this.&lt;/p&gt;
&lt;h3 id=&quot;should-you-cache-everything-again-&quot;&gt;Should You Cache Everything Again?&lt;/h3&gt;
&lt;p&gt;No. The new default exists because stale data surprised people. Show a user their own cart, balance or notifications from a shared cache and you have a bug, or a data leak.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Data &lt;/th&gt;&lt;th&gt;Choice&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Public, rarely changes (marketing pages, docs) &lt;/td&gt;&lt;td&gt;Cache it, long expiry&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Public, changes often (product list, prices) &lt;/td&gt;&lt;td&gt;Cache with a short &lt;code&gt;revalidate&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Per-user (dashboard, cart, account) &lt;/td&gt;&lt;td&gt;Do not cache&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Admin views &lt;/td&gt;&lt;td&gt;Do not cache&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;the-rule-1&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;When a framework changes a default, your old code changes behavior without a single line changing.&lt;/strong&gt; After any major upgrade, run &lt;code&gt;next build&lt;/code&gt; before and after and compare which routes are static (&lt;code&gt;○&lt;/code&gt;) and which are dynamic (&lt;code&gt;ƒ&lt;/code&gt;). Then opt back in on purpose, for the data that deserves it.&lt;/p&gt;
&lt;p&gt;Slow server responses hurt the metrics Google measures. I break those down in plain language in my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com/blog/core-web-vitals-explained&quot;&gt;guide to Core Web Vitals&lt;/a&gt;, and if you want a Next.js starting point, you can browse my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;templates on pixelanas.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Did a framework upgrade ever quietly change your app's behavior? Tell me what it was in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32719/you-upgraded-to-next-js-15-and-your-site-got-slower-the-caching-defaults-flipped</guid>
<pubDate>Tue, 27 Oct 2026 16:13:23 +0000</pubDate>
</item>
<item>
<title>How to Build a Real Time Activity Log for Your Web App</title>
<link>https://coderlegion.com/32629/how-to-build-a-real-time-activity-log-for-your-web-app</link>
<description>&lt;p&gt;Modern web applications generate a continuous stream of user activities. Users log in, update their profiles, create records, upload files, change permissions, and perform many other actions throughout the day. In a small application, tracking these activities manually may seem manageable. However, as an application grows, understanding who did what, when an action occurred, and what changed becomes increasingly important.&lt;/p&gt;
&lt;p&gt;This is where a &lt;strong&gt;real-time activity log&lt;/strong&gt; becomes valuable.&lt;/p&gt;
&lt;p&gt;A real-time activity log records important events within an application and makes them available to users, administrators, or support teams as they happen. Instead of searching through scattered server logs or asking users to explain what happened, developers can provide a centralized timeline of meaningful actions.&lt;/p&gt;
&lt;p&gt;For example, a project management application might display the following activities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sarah created a new project.&lt;/li&gt;
&lt;li&gt;Michael updated the project deadline.&lt;/li&gt;
&lt;li&gt;David uploaded a document.&lt;/li&gt;
&lt;li&gt;Sarah invited a new team member.&lt;/li&gt;
&lt;li&gt;An administrator changed a user's permissions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These events help teams understand application activity, investigate problems, and collaborate more effectively.&lt;/p&gt;
&lt;p&gt;In this article, we will explore how to design and build a real-time activity log using React, Python, FastAPI, PostgreSQL, and WebSockets. We will cover database design, event recording, real-time updates, security, performance optimization, and deployment considerations.&lt;/p&gt;
&lt;h3 id=&quot;1-understanding-real-time-activity-logs&quot;&gt;1. Understanding Real-Time Activity Logs&lt;/h3&gt;
&lt;p&gt;Before writing code, it is important to understand what an activity log does and how it differs from ordinary application logging.&lt;/p&gt;
&lt;p&gt;Traditional application logs are primarily designed for developers and infrastructure teams. They record technical information such as exceptions, failed database connections, HTTP errors, and performance problems.&lt;/p&gt;
&lt;p&gt;An activity log focuses on meaningful actions performed by users or systems.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Technical log: &lt;code&gt;POST /api/projects returned status 201&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Activity log: &lt;code&gt;Sarah created the project &quot;Website Redesign&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both records may describe the same operation, but they serve different purposes.&lt;/p&gt;
&lt;h4 id=&quot;common-use-cases&quot;&gt;Common use cases&lt;/h4&gt;
&lt;p&gt;Real-time activity logs are useful in many applications.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project management platforms:&lt;/strong&gt; Track task creation, assignments, comments, and deadline changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;E-commerce applications:&lt;/strong&gt; Record order updates, refunds, inventory changes, and administrative actions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CRM systems:&lt;/strong&gt; Track customer record updates, sales activities, and changes to lead ownership.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Financial dashboards:&lt;/strong&gt; Maintain auditable records of important account operations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Administrative panels:&lt;/strong&gt; Show user logins, permission changes, configuration updates, and security events.&lt;/p&gt;
&lt;p&gt;A well-designed activity log should answer three fundamental questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Who performed the action?&lt;/li&gt;
&lt;li&gt;What action occurred?&lt;/li&gt;
&lt;li&gt;When did the action happen?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Depending on the application, it may also record the affected resource, the previous and updated values, the source of the action, and the result.&lt;/p&gt;
&lt;h3 id=&quot;2-choosing-the-technology-stack&quot;&gt;2. Choosing the Technology Stack&lt;/h3&gt;
&lt;p&gt;Our example uses a stack that supports structured data, asynchronous APIs, and live browser updates.&lt;/p&gt;
&lt;h4 id=&quot;frontend-react&quot;&gt;Frontend: React&lt;/h4&gt;
&lt;p&gt;React renders the activity timeline and updates the interface when new events arrive. It can connect to a WebSocket endpoint and display incoming messages without requiring a complete page refresh.&lt;/p&gt;
&lt;h4 id=&quot;backend-python-and-fastapi&quot;&gt;Backend: Python and FastAPI&lt;/h4&gt;
&lt;p&gt;FastAPI handles authentication, validates incoming requests, records activities, and exposes endpoints for retrieving historical events. Its WebSocket support allows the backend to communicate with connected browsers.&lt;/p&gt;
&lt;h4 id=&quot;database-postgresql&quot;&gt;Database: PostgreSQL&lt;/h4&gt;
&lt;p&gt;PostgreSQL stores activity records permanently. It supports relational queries, indexes, transactions, and pagination, making it suitable for an activity timeline.&lt;/p&gt;
&lt;h4 id=&quot;real-time-communication-websockets&quot;&gt;Real-time communication: WebSockets&lt;/h4&gt;
&lt;p&gt;WebSockets establish a persistent connection between the browser and server. After a new activity is committed to the database, the backend can publish an event to authorized connected clients.&lt;/p&gt;
&lt;h4 id=&quot;optional-infrastructure-redis&quot;&gt;Optional infrastructure: Redis&lt;/h4&gt;
&lt;p&gt;For applications running multiple backend instances, Redis Pub/Sub or a durable message broker can distribute events between servers. Redis is not required for a single-instance prototype.&lt;/p&gt;
&lt;p&gt;The architecture looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User performs an action
          |
          v
React frontend
          |
          v
FastAPI endpoint
          |
          v
Business operation + database transaction
          |
          v
PostgreSQL activity record
          |
          v
Event publisher
          |
          v
WebSocket connection
          |
          v
React activity timeline
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The essential principle is that the database remains the source of truth. WebSockets deliver updates quickly, but clients should be able to retrieve historical events from the database if they disconnect.&lt;/p&gt;
&lt;h3 id=&quot;3-designing-the-activity-log-database&quot;&gt;3. Designing the Activity Log Database&lt;/h3&gt;
&lt;p&gt;A reliable activity log starts with a clear database schema. Each record should contain enough information to explain an event without storing unnecessary sensitive data.&lt;/p&gt;
&lt;p&gt;We can create an &lt;code&gt;activity_logs&lt;/code&gt; table with the following fields:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Field &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;id&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Unique event identifier&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;actor_id&lt;/code&gt; &lt;/td&gt;&lt;td&gt;User or system responsible for the action&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;action&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Type of activity performed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;entity_type&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Type of affected resource&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;entity_id&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Identifier of the affected resource&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;description&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Human-readable event summary&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;metadata&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Additional structured event details&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;created_at&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Timestamp when the event was recorded&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &lt;code&gt;metadata&lt;/code&gt; field can use PostgreSQL's JSONB type. This makes it possible to store flexible information without creating a new database column for every event type.&lt;/p&gt;
&lt;p&gt;For example, a project update could contain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;field&quot;: &quot;deadline&quot;,
  &quot;old_value&quot;: &quot;2026-11-10&quot;,
  &quot;new_value&quot;: &quot;2026-11-15&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Avoid storing passwords, authentication tokens, payment card details, or unnecessary personal information in event metadata.&lt;/p&gt;
&lt;h4 id=&quot;creating-the-database-table&quot;&gt;Creating the database table&lt;/h4&gt;
&lt;p&gt;A simple PostgreSQL migration might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE activity_logs (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    actor_id BIGINT NULL,
    action VARCHAR(100) NOT NULL,
    entity_type VARCHAR(100),
    entity_id VARCHAR(100),
    description TEXT NOT NULL,
    metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_activity_logs_created_at
    ON activity_logs (created_at DESC, id DESC);

CREATE INDEX idx_activity_logs_actor
    ON activity_logs (actor_id, created_at DESC, id DESC);

CREATE INDEX idx_activity_logs_entity
    ON activity_logs (
        entity_type,
        entity_id,
        created_at DESC,
        id DESC
    );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These indexes support common queries, including retrieving recent events, filtering by actor, and viewing the history of a specific resource.&lt;/p&gt;
&lt;p&gt;In a multi-tenant application, include a &lt;code&gt;tenant_id&lt;/code&gt; or equivalent ownership identifier and design indexes around the actual access patterns.&lt;/p&gt;
&lt;p&gt;The example uses a nullable &lt;code&gt;actor_id&lt;/code&gt; because some events may be generated by scheduled jobs or automated processes. In a production database, define appropriate foreign keys and retention rules according to your user-deletion and audit requirements.&lt;/p&gt;
&lt;h3 id=&quot;4-defining-consistent-activity-events&quot;&gt;4. Defining Consistent Activity Events&lt;/h3&gt;
&lt;p&gt;A major challenge is maintaining consistency across different parts of an application.&lt;/p&gt;
&lt;p&gt;Without a shared event structure, one feature might record &lt;code&gt;&quot;user updated&quot;&lt;/code&gt; while another records &lt;code&gt;&quot;profile_changed&quot;&lt;/code&gt;. The frontend then becomes responsible for understanding many inconsistent formats.&lt;/p&gt;
&lt;p&gt;A better approach is to define a standard event contract.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;id&quot;: &quot;evt_1024&quot;,
  &quot;actor&quot;: {
    &quot;id&quot;: 42,
    &quot;name&quot;: &quot;Sarah&quot;
  },
  &quot;action&quot;: &quot;project.updated&quot;,
  &quot;entity_type&quot;: &quot;project&quot;,
  &quot;entity_id&quot;: &quot;project_15&quot;,
  &quot;description&quot;: &quot;Sarah updated the project deadline&quot;,
  &quot;metadata&quot;: {
    &quot;field&quot;: &quot;deadline&quot;
  },
  &quot;created_at&quot;: &quot;2026-10-27T06:30:00Z&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The action name should describe the event in a predictable way. Names such as &lt;code&gt;project.created&lt;/code&gt;, &lt;code&gt;project.updated&lt;/code&gt;, &lt;code&gt;task.assigned&lt;/code&gt;, and &lt;code&gt;user.permission_changed&lt;/code&gt; are easier to maintain than arbitrary descriptions used as event identifiers.&lt;/p&gt;
&lt;p&gt;Separate the machine-readable action from the human-readable description.&lt;/p&gt;
&lt;p&gt;The action supports filtering, analytics, and integrations. The description is intended for display and can be localized later.&lt;/p&gt;
&lt;p&gt;Also, distinguish between the person who performed an action and the person or object affected by it. For example, an administrator may change another user's permissions.&lt;/p&gt;
&lt;h3 id=&quot;5-building-the-fastapi-activity-service&quot;&gt;5. Building the FastAPI Activity Service&lt;/h3&gt;
&lt;p&gt;The backend should provide reusable functions for creating and retrieving activities. Individual business endpoints should call these functions rather than duplicating database logic.&lt;/p&gt;
&lt;p&gt;The following example illustrates the structure of a service. It assumes that the application has already configured its database session and authentication system.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from datetime import datetime, timezone
from typing import Any

from pydantic import BaseModel, Field


class ActivityCreate(BaseModel):
    actor_id: int | None = None
    action: str = Field(min_length=1, max_length=100)
    entity_type: str | None = None
    entity_id: str | None = None
    description: str = Field(min_length=1, max_length=500)
    metadata: dict[str, Any] = Field(default_factory=dict)


class ActivityResponse(BaseModel):
    id: int
    actor_id: int | None
    action: str
    entity_type: str | None
    entity_id: str | None
    description: str
    metadata: dict[str, Any]
    created_at: datetime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pydantic validates the shape of the data before it reaches the service. However, validation alone is not enough. The backend must also determine whether the authenticated actor is allowed to perform the action and whether the supplied resource belongs to the correct tenant.&lt;/p&gt;
&lt;h4 id=&quot;recording-an-activity&quot;&gt;Recording an activity&lt;/h4&gt;
&lt;p&gt;The activity service should insert the event into the database using the same transaction as the business operation whenever practical.&lt;/p&gt;
&lt;p&gt;Conceptually, the service performs the following steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Validate the event data.&lt;/li&gt;
&lt;li&gt;Confirm the actor's identity and permissions.&lt;/li&gt;
&lt;li&gt;Execute the business operation.&lt;/li&gt;
&lt;li&gt;Insert the activity record.&lt;/li&gt;
&lt;li&gt;Commit the transaction.&lt;/li&gt;
&lt;li&gt;Publish the event for real-time delivery.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This ordering matters. Broadcasting an event before the database transaction commits can cause the interface to display a change that is later rolled back.&lt;/p&gt;
&lt;p&gt;For a small application, a database insert followed by publication may be sufficient. For production systems where losing an event is unacceptable, the transactional outbox pattern provides stronger reliability.&lt;/p&gt;
&lt;h3 id=&quot;6-connecting-business-operations-to-the-activity-log&quot;&gt;6. Connecting Business Operations to the Activity Log&lt;/h3&gt;
&lt;p&gt;The most useful logs are generated automatically by meaningful application operations.&lt;/p&gt;
&lt;p&gt;Suppose a user creates a project. The backend should create the project and its activity record together.&lt;/p&gt;
&lt;p&gt;A simplified FastAPI endpoint could follow this pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastapi import APIRouter, Depends, HTTPException

router = APIRouter()


@router.post(&quot;/projects&quot;)
async def create_project(
    payload: dict,
    current_user=Depends(get_current_user),
    db=Depends(get_db),
):
    project = await save_project(db, payload, current_user.id)

    activity = await save_activity(
        db=db,
        actor_id=current_user.id,
        action=&quot;project.created&quot;,
        entity_type=&quot;project&quot;,
        entity_id=str(project.id),
        description=f&quot;{current_user.name} created a project&quot;,
        metadata={&quot;project_name&quot;: project.name},
    )

    await db.commit()

    await publish_activity(activity)

    return project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;get_current_user&lt;/code&gt;, &lt;code&gt;get_db&lt;/code&gt;, &lt;code&gt;save_project&lt;/code&gt;, &lt;code&gt;save_activity&lt;/code&gt;, and &lt;code&gt;publish_activity&lt;/code&gt; represent application-specific functions that must be implemented.&lt;/p&gt;
&lt;p&gt;For true transactional safety, both database writes must use the same transaction, and failures must roll back both operations. The publishing function should also handle delivery failures without incorrectly reporting that a committed business operation failed.&lt;/p&gt;
&lt;p&gt;A stronger design records an outbox message in the same transaction. A separate worker publishes committed messages and retries failed deliveries.&lt;/p&gt;
&lt;h4 id=&quot;which-actions-should-be-recorded-&quot;&gt;Which actions should be recorded?&lt;/h4&gt;
&lt;p&gt;Do not record every technical operation indiscriminately. Focus on events that help users understand changes or help administrators investigate important activity.&lt;/p&gt;
&lt;p&gt;Useful examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating, editing, or deleting a record.&lt;/li&gt;
&lt;li&gt;Assigning a task to a team member.&lt;/li&gt;
&lt;li&gt;Updating an order status.&lt;/li&gt;
&lt;li&gt;Changing account permissions.&lt;/li&gt;
&lt;li&gt;Uploading or removing a document.&lt;/li&gt;
&lt;li&gt;Inviting or removing a user.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Read-only operations, routine polling requests, and other high-frequency events usually belong in application telemetry rather than the business activity timeline.&lt;/p&gt;
&lt;h3 id=&quot;7-building-the-websocket-endpoint&quot;&gt;7. Building the WebSocket Endpoint&lt;/h3&gt;
&lt;p&gt;REST APIs are useful for retrieving historical activities, but repeatedly polling an endpoint is inefficient when users need immediate updates.&lt;/p&gt;
&lt;p&gt;WebSockets allow the server to push newly published events to connected clients.&lt;/p&gt;
&lt;p&gt;A minimal FastAPI implementation might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastapi import WebSocket, WebSocketDisconnect


class ConnectionManager:
    def __init__(self):
        self.connections: set[WebSocket] = set()

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.connections.add(websocket)

    def disconnect(self, websocket: WebSocket):
        self.connections.discard(websocket)

    async def broadcast(self, event: dict):
        disconnected = []

        for connection in list(self.connections):
            try:
                await connection.send_json(event)
            except Exception:
                disconnected.append(connection)

        for connection in disconnected:
            self.disconnect(connection)


manager = ConnectionManager()


@app.websocket(&quot;/ws/activity&quot;)
async def activity_socket(websocket: WebSocket):
    # Authenticate and authorize the connection before accepting it.
    user = await authenticate_websocket(websocket)

    if user is None:
        await websocket.close(code=1008)
        return

    await manager.connect(websocket)

    try:
        while True:
            # Production implementations should also handle heartbeats,
            # disconnects, and bounded message processing.
            await websocket.receive_text()
    except WebSocketDisconnect:
        manager.disconnect(websocket)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code illustrates the connection lifecycle rather than a complete production implementation. The &lt;code&gt;authenticate_websocket&lt;/code&gt; function must be implemented using your application's authentication strategy.&lt;/p&gt;
&lt;p&gt;The manager broadcasts only to sockets registered with it. In a real multi-user application, a global broadcast to every connection would be unsafe. Store connections by authorized tenant, project, organization, or user, and send each event only to clients entitled to see it.&lt;/p&gt;
&lt;p&gt;Never assume that a user is authorized simply because they successfully established a WebSocket connection.&lt;/p&gt;
&lt;h3 id=&quot;8-implementing-the-react-activity-timeline&quot;&gt;8. Implementing the React Activity Timeline&lt;/h3&gt;
&lt;p&gt;The frontend needs two capabilities: loading existing activities and receiving new ones.&lt;/p&gt;
&lt;p&gt;The historical API might expose an endpoint such as &lt;code&gt;GET /api/activity?limit=20&lt;/code&gt;. The browser can load the most recent records when the page opens, while the WebSocket delivers updates afterward.&lt;/p&gt;
&lt;p&gt;A basic React component could look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { useEffect, useState } from &quot;react&quot;;

export default function ActivityTimeline() {
  const [activities, setActivities] = useState([]);

  useEffect(() =&amp;gt; {
    let socket;

    async function loadActivities() {
      const response = await fetch(&quot;/api/activity?limit=20&quot;, {
        credentials: &quot;include&quot;,
      });

      if (!response.ok) {
        throw new Error(&quot;Unable to load activity history&quot;);
      }

      const data = await response.json();
      setActivities(data.items);
    }

    loadActivities().catch(console.error);

    socket = new WebSocket(&quot;wss://example.com/ws/activity&quot;);

    socket.onmessage = (message) =&amp;gt; {
      const activity = JSON.parse(message.data);

      setActivities((current) =&amp;gt; {
        if (current.some((item) =&amp;gt; item.id === activity.id)) {
          return current;
        }

        return [activity, ...current].slice(0, 50);
      });
    };

    return () =&amp;gt; {
      socket?.close();
    };
  }, []);

  return (
    &amp;lt;section&amp;gt;
      &amp;lt;h2&amp;gt;Recent Activity&amp;lt;/h2&amp;gt;

      &amp;lt;ul&amp;gt;
        {activities.map((activity) =&amp;gt; (
          &amp;lt;li key={activity.id}&amp;gt;
            &amp;lt;strong&amp;gt;{activity.description}&amp;lt;/strong&amp;gt;
            &amp;lt;time dateTime={activity.created_at}&amp;gt;
              {new Date(activity.created_at).toLocaleString()}
            &amp;lt;/time&amp;gt;
          &amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/section&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the example domain with your own deployment address. The API response is assumed to contain an &lt;code&gt;items&lt;/code&gt; array, and the WebSocket endpoint must return events matching the activity schema.&lt;/p&gt;
&lt;p&gt;This example intentionally stays small. A production component should handle loading states, API errors, WebSocket reconnection, authentication expiration, and changes to network connectivity.&lt;/p&gt;
&lt;p&gt;For session-cookie authentication, use secure cookie settings and validate the WebSocket handshake's origin. If your system uses bearer tokens, avoid placing long-lived credentials in URLs because URLs can appear in logs and monitoring systems.&lt;/p&gt;
&lt;h4 id=&quot;improving-the-user-experience&quot;&gt;Improving the user experience&lt;/h4&gt;
&lt;p&gt;An activity timeline should be easy to scan. Consider displaying:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The actor's name and profile image, where appropriate.&lt;/li&gt;
&lt;li&gt;A short action description.&lt;/li&gt;
&lt;li&gt;A relative timestamp such as &quot;2 minutes ago&quot;.&lt;/li&gt;
&lt;li&gt;A link to the affected resource.&lt;/li&gt;
&lt;li&gt;Filters for event type, user, or date range.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use semantic HTML and accessible labels. Color can communicate event categories, but the interface should not depend on color alone.&lt;/p&gt;
&lt;p&gt;Also, avoid inserting user-generated descriptions as raw HTML. Render them as text or sanitize them appropriately to prevent cross-site scripting.&lt;/p&gt;
&lt;h3 id=&quot;9-retrieving-historical-activities-with-pagination&quot;&gt;9. Retrieving Historical Activities with Pagination&lt;/h3&gt;
&lt;p&gt;An activity table can grow rapidly. Returning every event in one API response will eventually become slow and expensive.&lt;/p&gt;
&lt;p&gt;Use pagination to retrieve only the records the interface needs.&lt;/p&gt;
&lt;p&gt;For smaller datasets, limit-and-offset pagination is straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, actor_id, action, entity_type,
       entity_id, description, created_at
FROM activity_logs
WHERE tenant_id = :tenant_id
ORDER BY created_at DESC, id DESC
LIMIT 20 OFFSET 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This query assumes that a &lt;code&gt;tenant_id&lt;/code&gt; column has been added to the table.&lt;/p&gt;
&lt;p&gt;For large datasets, cursor-based pagination is usually preferable. The client supplies the timestamp and ID of the last event it received. The next page uses a condition such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, actor_id, action, entity_type,
       entity_id, description, created_at
FROM activity_logs
WHERE tenant_id = :tenant_id
  AND (created_at, id) &amp;lt; (:cursor_time, :cursor_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The timestamp and ID together provide a stable ordering when multiple activities share the same timestamp.&lt;/p&gt;
&lt;p&gt;Validate pagination parameters on the backend and apply a maximum page size. The database should return only fields the client is allowed to see.&lt;/p&gt;
&lt;h3 id=&quot;10-handling-real-time-events-reliably&quot;&gt;10. Handling Real-Time Events Reliably&lt;/h3&gt;
&lt;p&gt;A WebSocket connection does not guarantee that every event will reach every browser.&lt;/p&gt;
&lt;p&gt;Users may lose their network connection, close a laptop, refresh a page, or move between networks. A server may also restart while clients are connected.&lt;/p&gt;
&lt;p&gt;For that reason, treat real-time delivery as an optimization rather than the permanent storage mechanism.&lt;/p&gt;
&lt;p&gt;A reliable workflow should:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Save the activity record.&lt;/li&gt;
&lt;li&gt;Commit the database transaction.&lt;/li&gt;
&lt;li&gt;Publish an event containing the record identifier and relevant payload.&lt;/li&gt;
&lt;li&gt;Retry publication if delivery fails.&lt;/li&gt;
&lt;li&gt;Allow clients to reconnect.&lt;/li&gt;
&lt;li&gt;Fetch missed activities after reconnection.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For higher reliability, use the &lt;strong&gt;transactional outbox pattern&lt;/strong&gt;. In the same database transaction as the business change, insert an outbox record describing the event. A background worker reads unpublished records, publishes them to a message broker, and marks them as processed.&lt;/p&gt;
&lt;p&gt;If the worker crashes after publishing but before marking a message as processed, it may publish the same event again. Therefore, consumers should be idempotent and use event IDs to avoid duplicate processing.&lt;/p&gt;
&lt;p&gt;Redis Pub/Sub can distribute messages between application instances, but it does not provide durable message storage for disconnected consumers. If events must survive outages, consider a durable queue or streaming system appropriate to your workload.&lt;/p&gt;
&lt;h3 id=&quot;11-security-and-access-control&quot;&gt;11. Security and Access Control&lt;/h3&gt;
&lt;p&gt;Activity logs can contain sensitive information about business operations. Protecting them is as important as protecting the primary application database.&lt;/p&gt;
&lt;h4 id=&quot;authenticate-every-request&quot;&gt;Authenticate every request&lt;/h4&gt;
&lt;p&gt;Require authentication for historical activity endpoints and WebSocket connections. Expired sessions and revoked permissions must be handled correctly.&lt;/p&gt;
&lt;h4 id=&quot;enforce-authorization&quot;&gt;Enforce authorization&lt;/h4&gt;
&lt;p&gt;Filter activity records by tenant, organization, project, or user permissions on the server. Never rely on the React interface to hide records that the user is not allowed to access.&lt;/p&gt;
&lt;h4 id=&quot;prevent-tampering&quot;&gt;Prevent tampering&lt;/h4&gt;
&lt;p&gt;For important audit trails, restrict modification and deletion privileges. Consider append-only database permissions, immutable backups, or external audit storage where appropriate.&lt;/p&gt;
&lt;h4 id=&quot;minimize-sensitive-data&quot;&gt;Minimize sensitive data&lt;/h4&gt;
&lt;p&gt;Do not store passwords, session cookies, API keys, full payment details, or unnecessary personal information in event descriptions or metadata.&lt;/p&gt;
&lt;h4 id=&quot;validate-websocket-origins&quot;&gt;Validate WebSocket origins&lt;/h4&gt;
&lt;p&gt;For cookie-authenticated connections, validate the request origin against an explicit allowlist. Use encrypted connections in production and rate-limit connection attempts.&lt;/p&gt;
&lt;h4 id=&quot;separate-business-events-from-security-logs&quot;&gt;Separate business events from security logs&lt;/h4&gt;
&lt;p&gt;A business activity timeline and a security audit trail may overlap, but they are not identical. Security logs may require stricter access controls, longer retention, reliable ingestion, and records of privileged administrative actions.&lt;/p&gt;
&lt;p&gt;Do not assume that a normal user-facing activity feed is sufficient for regulatory or security auditing.&lt;/p&gt;
&lt;h3 id=&quot;12-performance-optimization-for-large-applications&quot;&gt;12. Performance Optimization for Large Applications&lt;/h3&gt;
&lt;p&gt;As the number of users grows, the volume of activity events can increase significantly. A design that works for a small prototype may struggle under a large workload.&lt;/p&gt;
&lt;h4 id=&quot;use-appropriate-database-indexes&quot;&gt;Use appropriate database indexes&lt;/h4&gt;
&lt;p&gt;Index fields used for filtering and ordering. Avoid adding indexes without a clear query pattern because every additional index increases storage requirements and write costs.&lt;/p&gt;
&lt;h4 id=&quot;limit-websocket-payloads&quot;&gt;Limit WebSocket payloads&lt;/h4&gt;
&lt;p&gt;Send only the fields the recipient needs. Large metadata objects increase network traffic and browser processing time.&lt;/p&gt;
&lt;h4 id=&quot;avoid-blocking-database-operations&quot;&gt;Avoid blocking database operations&lt;/h4&gt;
&lt;p&gt;Use asynchronous database access where appropriate, connection pooling, and carefully managed transaction boundaries.&lt;/p&gt;
&lt;h4 id=&quot;batch-or-queue-publication&quot;&gt;Batch or queue publication&lt;/h4&gt;
&lt;p&gt;If many events are generated in a short period, a queue can absorb bursts and allow workers to publish at a controlled rate.&lt;/p&gt;
&lt;h4 id=&quot;apply-retention-policies&quot;&gt;Apply retention policies&lt;/h4&gt;
&lt;p&gt;Not every event needs to remain in the primary database forever. Depending on business requirements, older records may be archived, partitioned, or deleted after an approved retention period.&lt;/p&gt;
&lt;p&gt;Never introduce a retention policy that conflicts with legal, contractual, or audit obligations.&lt;/p&gt;
&lt;h4 id=&quot;monitor-important-metrics&quot;&gt;Monitor important metrics&lt;/h4&gt;
&lt;p&gt;Track event creation latency, publication failures, queue depth, WebSocket connection counts, query duration, and database storage growth.&lt;/p&gt;
&lt;p&gt;These metrics help identify bottlenecks before users experience slow timelines or missing updates.&lt;/p&gt;
&lt;h3 id=&quot;13-testing-the-activity-log&quot;&gt;13. Testing the Activity Log&lt;/h3&gt;
&lt;p&gt;Testing should cover both the stored history and real-time behavior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unit tests:&lt;/strong&gt; Verify event validation, action naming, metadata handling, and formatting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Integration tests:&lt;/strong&gt; Confirm that a successful business operation creates the expected activity record and that a failed transaction does not leave a misleading record.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WebSocket tests:&lt;/strong&gt; Check that authorized clients receive relevant events and unauthorized clients cannot subscribe to restricted activity streams.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pagination tests:&lt;/strong&gt; Verify stable ordering when timestamps match and ensure that cursor-based requests do not unexpectedly repeat records.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reconnect tests:&lt;/strong&gt; Simulate a dropped connection, reconnect, and fetch missed events.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security tests:&lt;/strong&gt; Check tenant isolation, authorization changes, origin validation, and attempts to inject unsafe content.&lt;/p&gt;
&lt;p&gt;One especially important test is whether a client can request another tenant's activity by changing a URL parameter or subscribing to a different WebSocket channel. The server must reject unauthorized access regardless of what the frontend displays.&lt;/p&gt;
&lt;h3 id=&quot;14-common-mistakes-to-avoid&quot;&gt;14. Common Mistakes to Avoid&lt;/h3&gt;
&lt;p&gt;Several implementation mistakes can make an activity log unreliable or unsafe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Publishing before committing:&lt;/strong&gt; The browser may display an event for a transaction that later fails.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Broadcasting to every connected user:&lt;/strong&gt; Private activities may be disclosed to unauthorized clients.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using WebSockets as the only data store:&lt;/strong&gt; Disconnected clients can miss events permanently unless they can retrieve history.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Logging every request:&lt;/strong&gt; Excessive noise makes meaningful activity difficult to find and increases storage costs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Storing entire database records:&lt;/strong&gt; This can expose sensitive fields and produce large event payloads.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ignoring duplicate delivery:&lt;/strong&gt; Retries can cause the same activity to appear multiple times.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using unstable pagination:&lt;/strong&gt; Ordering only by timestamp can produce inconsistent pages when several events share the same timestamp.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skipping authorization checks:&lt;/strong&gt; A valid login does not automatically grant access to every project or organization.&lt;/p&gt;
&lt;p&gt;Avoiding these mistakes early makes the system easier to maintain as the application grows.&lt;/p&gt;
&lt;h3 id=&quot;15-deployment-considerations&quot;&gt;15. Deployment Considerations&lt;/h3&gt;
&lt;p&gt;Before deploying the activity service, configure the infrastructure for production workloads.&lt;/p&gt;
&lt;p&gt;Use environment variables or a managed secrets system for database credentials. Enable TLS for HTTPS and WSS traffic. Configure connection limits, request timeouts, database pooling, and reverse-proxy WebSocket support.&lt;/p&gt;
&lt;p&gt;If multiple FastAPI instances are running, remember that an in-memory connection manager only knows about connections attached to its own process. Use shared messaging infrastructure so events can reach clients connected to different instances.&lt;/p&gt;
&lt;p&gt;Configure health checks for the API and background workers. Monitor failed publications and make sure the outbox or queue can be recovered after an outage.&lt;/p&gt;
&lt;p&gt;Finally, test deployment behavior under realistic conditions. Restart application instances while users are connected, simulate slow clients, and confirm that historical events remain available after reconnecting.&lt;/p&gt;
&lt;h3 id=&quot;16-future-enhancements&quot;&gt;16. Future Enhancements&lt;/h3&gt;
&lt;p&gt;Once the basic activity log is working, several improvements can make it more useful.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Advanced filtering:&lt;/strong&gt; Allow users to filter by actor, event category, project, or date range.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Search:&lt;/strong&gt; Add full-text search for descriptions and selected metadata fields.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Notifications:&lt;/strong&gt; Turn important activity events into targeted notifications or email alerts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Export functionality:&lt;/strong&gt; Allow authorized users to export filtered activity records for reporting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Analytics:&lt;/strong&gt; Aggregate event data to understand product usage and team collaboration patterns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audit history:&lt;/strong&gt; Provide detailed before-and-after values for selected changes, with appropriate redaction and access controls.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live indicators:&lt;/strong&gt; Show a small notification badge when new events arrive and let users decide when to load them into the current timeline.&lt;/p&gt;
&lt;p&gt;Each feature should be built around a clear requirement. Avoid adding complexity that does not improve the experience or meet an operational need.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Building a real-time activity log is more than connecting a WebSocket to a React component. A dependable implementation combines a well-designed database schema, consistent event definitions, secure authorization, reliable persistence, and a frontend that can handle new events and network interruptions.&lt;/p&gt;
&lt;p&gt;React provides the interactive timeline, FastAPI manages application operations and real-time connections, and PostgreSQL stores the permanent activity history. WebSockets deliver immediate updates, while pagination, database indexes, and background workers help the architecture scale.&lt;/p&gt;
&lt;p&gt;For a small application, begin with a structured activity table, a reusable event service, a historical REST endpoint, and a secured WebSocket connection. As requirements grow, introduce cursor-based pagination, a transactional outbox, shared messaging, retention policies, and stronger audit controls.&lt;/p&gt;
&lt;p&gt;The ultimate goal is to make application activity transparent, useful, and trustworthy. When implemented correctly, a real-time activity log helps users understand changes, gives administrators better visibility, and makes complex web applications easier to maintain.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32629/how-to-build-a-real-time-activity-log-for-your-web-app</guid>
<pubDate>Tue, 27 Oct 2026 06:15:00 +0000</pubDate>
</item>
<item>
<title>Building a CSV Import and Export System for a Web Application</title>
<link>https://coderlegion.com/32371/building-a-csv-import-and-export-system-for-a-web-application</link>
<description>&lt;p&gt;Modern web applications often need to handle large amounts of structured data. Whether you are building a customer relationship management (CRM) platform, an inventory management system, an employee dashboard, or an e-commerce application, users frequently need to move data between different systems. One of the simplest and most widely supported formats for this purpose is CSV, which stands for Comma-Separated Values.&lt;/p&gt;
&lt;p&gt;CSV files allow users to organize information in a tabular structure that can be opened in spreadsheet applications such as Microsoft Excel, Google Sheets, and LibreOffice Calc. They are lightweight, easy to generate, and supported by almost every programming language.&lt;/p&gt;
&lt;p&gt;A well-designed CSV import and export system enables users to upload data into an application and download existing records for reporting, analysis, backup, or migration. However, building this functionality involves more than simply reading a file or converting database records into comma-separated text. Developers must consider data validation, error handling, security, performance, encoding, database transactions, and the user experience.&lt;/p&gt;
&lt;p&gt;In this article, we will explore how to build a reliable CSV import and export system for a modern web application. We will discuss the architecture, implementation strategy, validation techniques, security measures, and optimization practices needed to create a solution that works effectively in production.&lt;/p&gt;
&lt;h3 id=&quot;1-understanding-csv-import-and-export&quot;&gt;1. Understanding CSV Import and Export&lt;/h3&gt;
&lt;p&gt;Before implementing the system, it is important to understand the responsibilities of CSV import and export functionality.&lt;/p&gt;
&lt;h4 id=&quot;csv-import&quot;&gt;CSV Import&lt;/h4&gt;
&lt;p&gt;CSV import allows users to upload a file containing structured information and insert or update records in an application.&lt;/p&gt;
&lt;p&gt;For example, an administrator might upload a CSV file containing hundreds of customer records.&lt;/p&gt;
&lt;p&gt;A typical file could look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csv&quot;&gt;name,email,company
John Smith,*Emails are not allowed*,Example Inc
Sarah Johnson,*Emails are not allowed*,Tech Solutions
Michael Brown,*Emails are not allowed*,Digital Systems
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application reads the file, checks whether the values are valid, and stores the accepted records in a database.&lt;/p&gt;
&lt;h4 id=&quot;csv-export&quot;&gt;CSV Export&lt;/h4&gt;
&lt;p&gt;CSV export performs the opposite operation. It retrieves information from a database and converts it into a downloadable file.&lt;/p&gt;
&lt;p&gt;For example, a CRM application might allow a user to export all customers matching a particular search query.&lt;/p&gt;
&lt;p&gt;The generated file could contain the same columns shown in the import example.&lt;/p&gt;
&lt;h4 id=&quot;why-these-features-matter&quot;&gt;Why These Features Matter&lt;/h4&gt;
&lt;p&gt;CSV functionality provides several benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data migration:&lt;/strong&gt; Move records between applications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bulk operations:&lt;/strong&gt; Create or update many records at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reporting:&lt;/strong&gt; Analyze application data using spreadsheet tools.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backup support:&lt;/strong&gt; Download structured copies of selected information.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration:&lt;/strong&gt; Exchange data with external business systems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Productivity:&lt;/strong&gt; Reduce repetitive manual data entry.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These benefits make CSV processing a valuable feature for applications of almost any size.&lt;/p&gt;
&lt;h3 id=&quot;2-choosing-the-right-technology-stack&quot;&gt;2. Choosing the Right Technology Stack&lt;/h3&gt;
&lt;p&gt;The implementation depends on the application's existing technology stack. A common architecture uses React for the frontend and Python with FastAPI for the backend, supported by PostgreSQL or another relational database.&lt;/p&gt;
&lt;p&gt;Each component has a specific responsibility.&lt;/p&gt;
&lt;h4 id=&quot;frontend&quot;&gt;Frontend&lt;/h4&gt;
&lt;p&gt;The frontend provides the interface for selecting files, submitting imports, monitoring progress, displaying validation errors, and downloading exported data.&lt;/p&gt;
&lt;p&gt;React is suitable for this purpose because it supports reusable components and interactive interfaces.&lt;/p&gt;
&lt;h4 id=&quot;backend&quot;&gt;Backend&lt;/h4&gt;
&lt;p&gt;The backend receives uploaded files, validates their contents, processes records, queries the database, and generates export files.&lt;/p&gt;
&lt;p&gt;FastAPI provides request validation, asynchronous request handling, file upload support, and automatically generated API documentation.&lt;/p&gt;
&lt;h4 id=&quot;database&quot;&gt;Database&lt;/h4&gt;
&lt;p&gt;The database stores imported records and supplies data for export operations.&lt;/p&gt;
&lt;p&gt;PostgreSQL is a strong option when the application requires relational integrity, unique constraints, transactions, and efficient filtering.&lt;/p&gt;
&lt;h4 id=&quot;csv-processing-libraries&quot;&gt;CSV Processing Libraries&lt;/h4&gt;
&lt;p&gt;Python's built-in &lt;code&gt;csv&lt;/code&gt; module is useful for straightforward CSV processing. It handles quoted fields, delimiters, and escaped quotation marks without requiring an additional dependency.&lt;/p&gt;
&lt;p&gt;For complex data transformations, libraries such as pandas may be useful. However, pandas can consume substantial memory when processing very large files, so it should not be the default choice for every import operation.&lt;/p&gt;
&lt;p&gt;The best technology stack is the one that fits the application's scale, existing architecture, and operational requirements.&lt;/p&gt;
&lt;h3 id=&quot;3-designing-the-system-architecture&quot;&gt;3. Designing the System Architecture&lt;/h3&gt;
&lt;p&gt;A reliable CSV system separates the user interface, file processing, validation, persistence, and export generation.&lt;/p&gt;
&lt;p&gt;A typical import workflow follows these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user selects a CSV file.&lt;/li&gt;
&lt;li&gt;The frontend submits the file to the backend.&lt;/li&gt;
&lt;li&gt;The backend checks the file type and size.&lt;/li&gt;
&lt;li&gt;The CSV parser reads the header and rows.&lt;/li&gt;
&lt;li&gt;The validation layer checks each record.&lt;/li&gt;
&lt;li&gt;Validated records are inserted or updated in the database.&lt;/li&gt;
&lt;li&gt;The application returns a summary of the results.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The export workflow is slightly different:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user selects an export option or applies filters.&lt;/li&gt;
&lt;li&gt;The frontend sends an authenticated request.&lt;/li&gt;
&lt;li&gt;The backend checks permissions and validates the filters.&lt;/li&gt;
&lt;li&gt;The database returns the authorized records.&lt;/li&gt;
&lt;li&gt;The backend serializes the records into CSV format.&lt;/li&gt;
&lt;li&gt;The response provides a downloadable file.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Keeping these responsibilities separate makes the system easier to test, maintain, and extend.&lt;/p&gt;
&lt;p&gt;For larger applications, the import process can be moved into a background worker. The API then creates an import job and returns a job identifier. The frontend polls a status endpoint or receives updates through a notification mechanism.&lt;/p&gt;
&lt;p&gt;This architecture prevents long-running operations from blocking ordinary application requests.&lt;/p&gt;
&lt;h3 id=&quot;4-creating-the-database-model&quot;&gt;4. Creating the Database Model&lt;/h3&gt;
&lt;p&gt;Before implementing the import endpoint, define the structure of the records being imported.&lt;/p&gt;
&lt;p&gt;Consider a customer management application using SQLAlchemy and PostgreSQL.&lt;/p&gt;
&lt;p&gt;A simplified SQLAlchemy model could look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from sqlalchemy import String, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

class Base(DeclarativeBase):
    pass


class Customer(Base):
    __tablename__ = &quot;customers&quot;

    id: Mapped[int] = mapped_column(
        Integer,
        primary_key=True
    )

    name: Mapped[str] = mapped_column(
        String(150),
        nullable=False
    )

    email: Mapped[str] = mapped_column(
        String(255),
        unique=True,
        nullable=False
    )

    company: Mapped[str | None] = mapped_column(
        String(200),
        nullable=True
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This model defines a customer with a required name, a unique email address, and an optional company name.&lt;/p&gt;
&lt;p&gt;The unique constraint on the email field is particularly important. Application-level validation can detect duplicate emails before insertion, but the database constraint provides a final layer of protection against concurrent requests and unexpected data conditions.&lt;/p&gt;
&lt;p&gt;In production, use database migrations to create and modify tables instead of relying on automatic table creation during application startup.&lt;/p&gt;
&lt;h3 id=&quot;5-building-the-csv-import-endpoint&quot;&gt;5. Building the CSV Import Endpoint&lt;/h3&gt;
&lt;p&gt;FastAPI supports file uploads through &lt;code&gt;UploadFile&lt;/code&gt;. It provides access to the uploaded file without requiring the entire request body to be copied into a Python string.&lt;/p&gt;
&lt;p&gt;The following example demonstrates a basic import endpoint. It assumes that &lt;code&gt;get_db&lt;/code&gt; provides a SQLAlchemy session and &lt;code&gt;Customer&lt;/code&gt; is the database model defined earlier.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import csv
import io

from fastapi import (
    FastAPI,
    UploadFile,
    File,
    Depends,
    HTTPException
)
from sqlalchemy.orm import Session

app = FastAPI()

MAX_FILE_SIZE = 5 * 1024 * 1024

REQUIRED_COLUMNS = {
    &quot;name&quot;,
    &quot;email&quot;,
    &quot;company&quot;
}


@app.post(&quot;/customers/import&quot;)
async def import_customers(
    file: UploadFile = File(...),
    db: Session = Depends(get_db)
):
    if not file.filename or not file.filename.lower().endswith(&quot;.csv&quot;):
        raise HTTPException(
            status_code=400,
            detail=&quot;Please upload a CSV file.&quot;
        )

    content = await file.read(MAX_FILE_SIZE + 1)

    if len(content) &amp;gt; MAX_FILE_SIZE:
        raise HTTPException(
            status_code=413,
            detail=&quot;The uploaded file exceeds the size limit.&quot;
        )

    try:
        text = content.decode(&quot;utf-8-sig&quot;)
    except UnicodeDecodeError:
        raise HTTPException(
            status_code=400,
            detail=&quot;The file must use UTF-8 encoding.&quot;
        )

    reader = csv.DictReader(io.StringIO(text))

    if not reader.fieldnames:
        raise HTTPException(
            status_code=400,
            detail=&quot;The CSV file has no header row.&quot;
        )

    headers = {
        header.strip()
        for header in reader.fieldnames
        if header
    }

    if not REQUIRED_COLUMNS.issubset(headers):
        raise HTTPException(
            status_code=400,
            detail=&quot;The CSV file is missing required columns.&quot;
        )

    created = 0
    errors = []

    try:
        for row_number, row in enumerate(reader, start=2):
            name = (row.get(&quot;name&quot;) or &quot;&quot;).strip()
            email = (row.get(&quot;email&quot;) or &quot;&quot;).strip().lower()
            company = (row.get(&quot;company&quot;) or &quot;&quot;).strip()

            if not name or not email:
                errors.append({
                    &quot;row&quot;: row_number,
                    &quot;error&quot;: &quot;Name and email are required.&quot;
                })
                continue

            if &quot;@&quot; not in email:
                errors.append({
                    &quot;row&quot;: row_number,
                    &quot;error&quot;: &quot;Email address is invalid.&quot;
                })
                continue

            customer = Customer(
                name=name,
                email=email,
                company=company or None
            )

            db.add(customer)
            created += 1

        db.commit()

    except Exception:
        db.rollback()
        raise HTTPException(
            status_code=500,
            detail=&quot;The import could not be completed.&quot;
        )

    return {
        &quot;created&quot;: created,
        &quot;errors&quot;: errors
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example illustrates the basic workflow, but it is not a complete production implementation. A production version should also validate field lengths, reject duplicate headers, detect duplicate emails within the file, and distinguish database uniqueness violations from unexpected server errors.&lt;/p&gt;
&lt;p&gt;The header validation shown here checks whether the required names are present, but the parser still uses the original header names. A hardened implementation should normalize the headers before constructing row dictionaries or reject unexpected whitespace and duplicate column names.&lt;/p&gt;
&lt;p&gt;The example also reads the entire file into memory, so its five-megabyte limit is important. For larger files, process the upload in bounded chunks or stream the file into controlled temporary storage.&lt;/p&gt;
&lt;h3 id=&quot;6-validating-imported-data&quot;&gt;6. Validating Imported Data&lt;/h3&gt;
&lt;p&gt;Validation is one of the most important parts of a CSV import system.&lt;/p&gt;
&lt;p&gt;A syntactically valid CSV file may still contain incorrect, incomplete, or malicious values. Importing such data without validation can damage application consistency and create security problems.&lt;/p&gt;
&lt;h4 id=&quot;required-field-validation&quot;&gt;Required Field Validation&lt;/h4&gt;
&lt;p&gt;Required fields must contain meaningful values.&lt;/p&gt;
&lt;p&gt;For example, a customer record may require a name and email address. Rows missing either value should be rejected or reported as invalid.&lt;/p&gt;
&lt;h4 id=&quot;data-type-validation&quot;&gt;Data Type Validation&lt;/h4&gt;
&lt;p&gt;Fields should match their expected types.&lt;/p&gt;
&lt;p&gt;An age field should contain an integer within an acceptable range. A date field should follow an agreed date format. Numeric values should not contain unexpected characters.&lt;/p&gt;
&lt;h4 id=&quot;length-validation&quot;&gt;Length Validation&lt;/h4&gt;
&lt;p&gt;Database columns often have maximum lengths. Validate these limits before inserting records to avoid database errors.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if len(name) &amp;gt; 150:
    errors.append({
        &quot;row&quot;: row_number,
        &quot;error&quot;: &quot;Name exceeds 150 characters.&quot;
    })
    continue
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;email-validation&quot;&gt;Email Validation&lt;/h4&gt;
&lt;p&gt;Checking for an &lt;code&gt;@&lt;/code&gt; character is not sufficient for comprehensive email validation. Use a suitable validation library or a carefully designed validation process.&lt;/p&gt;
&lt;p&gt;Even a syntactically valid address does not guarantee that the mailbox exists.&lt;/p&gt;
&lt;h4 id=&quot;duplicate-detection&quot;&gt;Duplicate Detection&lt;/h4&gt;
&lt;p&gt;Duplicates can appear within the uploaded file or in the existing database.&lt;/p&gt;
&lt;p&gt;For example, a file might contain two records with the same email address. The application must define whether to reject the duplicate, skip it, update the existing record, or merge selected fields.&lt;/p&gt;
&lt;p&gt;These rules should be communicated clearly to users before the import begins.&lt;/p&gt;
&lt;h4 id=&quot;validation-reports&quot;&gt;Validation Reports&lt;/h4&gt;
&lt;p&gt;Instead of returning only a generic error, generate a report containing the row number, field name, and reason for rejection.&lt;/p&gt;
&lt;p&gt;A helpful report might say:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Row 4: Email address is missing.&lt;/li&gt;
&lt;li&gt;Row 9: Name exceeds the maximum length.&lt;/li&gt;
&lt;li&gt;Row 15: Email address already exists.&lt;/li&gt;
&lt;li&gt;Row 21: Invalid date format.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For large imports, offer a downloadable error report so users can correct the original file without manually searching through hundreds of records.&lt;/p&gt;
&lt;h3 id=&quot;7-deciding-between-partial-and-all-or-nothing-imports&quot;&gt;7. Deciding Between Partial and All-or-Nothing Imports&lt;/h3&gt;
&lt;p&gt;There are two common import strategies.&lt;/p&gt;
&lt;h4 id=&quot;partial-import&quot;&gt;Partial Import&lt;/h4&gt;
&lt;p&gt;The application saves valid records while rejecting invalid rows.&lt;/p&gt;
&lt;p&gt;This approach is useful when users upload large datasets and want to correct only the problematic records.&lt;/p&gt;
&lt;p&gt;The response should clearly report how many rows were accepted, rejected, skipped, or updated.&lt;/p&gt;
&lt;h4 id=&quot;atomic-import&quot;&gt;Atomic Import&lt;/h4&gt;
&lt;p&gt;The application validates the complete file and commits all changes only if every required operation succeeds.&lt;/p&gt;
&lt;p&gt;If any record fails, the transaction is rolled back.&lt;/p&gt;
&lt;p&gt;This approach is appropriate when partial data could violate business rules or produce inconsistent relationships.&lt;/p&gt;
&lt;p&gt;The choice depends on the application. A contact-management system may benefit from partial imports, while financial or inventory workflows may require stricter transactional behavior.&lt;/p&gt;
&lt;p&gt;For larger datasets, avoid holding a database transaction open during lengthy file uploads or extended validation. Validate first, then perform the database operation in a controlled transaction or staged workflow.&lt;/p&gt;
&lt;h3 id=&quot;8-implementing-duplicate-handling-and-updates&quot;&gt;8. Implementing Duplicate Handling and Updates&lt;/h3&gt;
&lt;p&gt;Importing a file twice should not unexpectedly create duplicate records.&lt;/p&gt;
&lt;p&gt;A practical system supports explicit duplicate-handling policies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skip duplicates:&lt;/strong&gt; Existing records remain unchanged.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update existing records:&lt;/strong&gt; Matching records are updated using a stable identifier, such as an external ID or unique email address.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reject duplicates:&lt;/strong&gt; The application reports conflicts and asks the user to resolve them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create new records:&lt;/strong&gt; Every valid row creates a new record, provided the data model permits duplicates.&lt;/p&gt;
&lt;p&gt;For business applications, a stable external identifier is often safer than relying only on email addresses because email addresses can change.&lt;/p&gt;
&lt;p&gt;When using PostgreSQL, an upsert operation can insert new records and update matching records in one database statement. SQLAlchemy supports PostgreSQL-specific insert operations that can be configured with &lt;code&gt;on_conflict_do_update&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Whatever strategy is chosen, document it in the interface and test concurrent imports to ensure database constraints remain effective.&lt;/p&gt;
&lt;h3 id=&quot;9-building-the-csv-export-endpoint&quot;&gt;9. Building the CSV Export Endpoint&lt;/h3&gt;
&lt;p&gt;Export functionality retrieves records from the database and serializes them into CSV format.&lt;/p&gt;
&lt;p&gt;Python's &lt;code&gt;csv&lt;/code&gt; module handles quoting and escaping more reliably than manually joining values with commas.&lt;/p&gt;
&lt;p&gt;A simple FastAPI endpoint can generate a CSV file using &lt;code&gt;StreamingResponse&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import csv
import io

from fastapi import Depends
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session


@app.get(&quot;/customers/export&quot;)
def export_customers(
    db: Session = Depends(get_db)
):
    customers = (
        db.query(Customer)
        .order_by(Customer.id)
        .all()
    )

    output = io.StringIO(newline=&quot;&quot;)

    writer = csv.writer(output)

    writer.writerow([
        &quot;id&quot;,
        &quot;name&quot;,
        &quot;email&quot;,
        &quot;company&quot;
    ])

    for customer in customers:
        writer.writerow([
            customer.id,
            customer.name,
            customer.email,
            customer.company or &quot;&quot;
        ])

    output.seek(0)

    return StreamingResponse(
        iter([output.getvalue()]),
        media_type=&quot;text/csv; charset=utf-8&quot;,
        headers={
            &quot;Content-Disposition&quot;:
                'attachment; filename=&quot;customers.csv&quot;'
        }
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This endpoint creates a downloadable file with a header row followed by customer records.&lt;/p&gt;
&lt;p&gt;However, despite using &lt;code&gt;StreamingResponse&lt;/code&gt;, this implementation builds the entire CSV in memory before returning it. It is suitable for small datasets, not for genuinely large exports.&lt;/p&gt;
&lt;p&gt;For larger datasets, use a generator that writes records incrementally and retrieves database rows in batches. Alternatively, generate the file in a background job and provide a secure download link when it is ready.&lt;/p&gt;
&lt;p&gt;Always ensure that the export query returns only records the requesting user is authorized to access.&lt;/p&gt;
&lt;h3 id=&quot;10-exporting-large-datasets-efficiently&quot;&gt;10. Exporting Large Datasets Efficiently&lt;/h3&gt;
&lt;p&gt;Export performance becomes important when a database contains hundreds of thousands or millions of records.&lt;/p&gt;
&lt;p&gt;Loading every record into a Python list can consume significant memory. Generating the complete CSV in a string creates another large in-memory object.&lt;/p&gt;
&lt;p&gt;A more efficient approach uses database pagination or streaming iteration.&lt;/p&gt;
&lt;p&gt;For example, a SQLAlchemy query can iterate through records in batches:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;query = (
    db.query(Customer)
    .order_by(Customer.id)
    .yield_per(1000)
)

for customer in query:
    process_customer(customer)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact behavior depends on the database driver and session configuration. For very large PostgreSQL exports, server-side cursors or carefully managed keyset pagination may be more appropriate.&lt;/p&gt;
&lt;p&gt;Keyset pagination uses a stable ordering column, such as the customer ID, and retrieves the next batch after the last processed ID. This can perform better than large offset-based queries.&lt;/p&gt;
&lt;p&gt;For extremely large exports, consider a background job that writes a file to private object storage. The application can then provide a time-limited download URL after checking permissions.&lt;/p&gt;
&lt;p&gt;Also consider database indexes, query filters, request timeouts, storage limits, and the number of concurrent export jobs.&lt;/p&gt;
&lt;h3 id=&quot;11-creating-a-react-csv-import-interface&quot;&gt;11. Creating a React CSV Import Interface&lt;/h3&gt;
&lt;p&gt;A useful frontend should make uploading and monitoring a file straightforward.&lt;/p&gt;
&lt;p&gt;Users should be able to select a file, review the selected filename, submit the upload, and see the result.&lt;/p&gt;
&lt;p&gt;A basic React component can use the &lt;code&gt;FormData&lt;/code&gt; API to submit the file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { useState } from &quot;react&quot;;

export default function CsvImport() {
  const [file, setFile] = useState(null);
  const [message, setMessage] = useState(&quot;&quot;);
  const [loading, setLoading] = useState(false);

  async function handleImport(event) {
    event.preventDefault();

    if (!file) {
      setMessage(&quot;Please select a CSV file.&quot;);
      return;
    }

    const formData = new FormData();
    formData.append(&quot;file&quot;, file);

    setLoading(true);
    setMessage(&quot;&quot;);

    try {
      const response = await fetch(
        &quot;/customers/import&quot;,
        {
          method: &quot;POST&quot;,
          body: formData,
          credentials: &quot;include&quot;
        }
      );

      const result = await response.json();

      if (!response.ok) {
        throw new Error(
          result.detail || &quot;Import failed.&quot;
        );
      }

      setMessage(
        `Created ${result.created} records. ` +
        `Rejected ${result.errors.length} rows.`
      );
    } catch (error) {
      setMessage(
        error.message || &quot;An unexpected error occurred.&quot;
      );
    } finally {
      setLoading(false);
    }
  }

  return (
    &amp;lt;form onSubmit={handleImport}&amp;gt;
      &amp;lt;label htmlFor=&quot;csv-file&quot;&amp;gt;
        Choose a CSV file
      &amp;lt;/label&amp;gt;

      &amp;lt;input
        id=&quot;csv-file&quot;
        type=&quot;file&quot;
        accept=&quot;.csv,text/csv&quot;
        onChange={(event) =&amp;gt;
          setFile(event.target.files?.[0] ?? null)
        }
      /&amp;gt;

      &amp;lt;button type=&quot;submit&quot; disabled={loading}&amp;gt;
        {loading ? &quot;Importing...&quot; : &quot;Import CSV&quot;}
      &amp;lt;/button&amp;gt;

      {message &amp;amp;&amp;amp; &amp;lt;p role=&quot;status&quot;&amp;gt;{message}&amp;lt;/p&amp;gt;}
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This component provides a basic import experience, but production interfaces should also display detailed validation errors and handle authentication, session expiration, and network failures.&lt;/p&gt;
&lt;p&gt;The browser's &lt;code&gt;accept&lt;/code&gt; attribute helps users select CSV files, but it is not a security boundary. The backend must independently validate every upload.&lt;/p&gt;
&lt;p&gt;The frontend should also disable duplicate submissions while an import is running and explain whether the operation can be safely retried.&lt;/p&gt;
&lt;h3 id=&quot;12-adding-a-csv-export-button-in-react&quot;&gt;12. Adding a CSV Export Button in React&lt;/h3&gt;
&lt;p&gt;Exporting a file can be initiated through a link or a button, depending on the application's authentication and download requirements.&lt;/p&gt;
&lt;p&gt;For cookie-based authentication, a simple link can be sufficient:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;export default function CsvExport() {
  return (
    &amp;lt;a href=&quot;/customers/export&quot;&amp;gt;
      Download Customers CSV
    &amp;lt;/a&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the API requires a bearer token or the frontend needs to inspect the response, use &lt;code&gt;fetch&lt;/code&gt;, read the response as a &lt;code&gt;Blob&lt;/code&gt;, and create a temporary download link.&lt;/p&gt;
&lt;p&gt;The frontend should revoke temporary object URLs after the download has been initiated.&lt;/p&gt;
&lt;p&gt;For large files, prefer a direct download or a secure, time-limited file URL rather than buffering the entire response in browser memory.&lt;/p&gt;
&lt;p&gt;Export controls should also respect the application's permission system. Hiding the button is not enough; the server must enforce authorization independently.&lt;/p&gt;
&lt;h3 id=&quot;13-improving-the-user-experience-with-import-previews&quot;&gt;13. Improving the User Experience with Import Previews&lt;/h3&gt;
&lt;p&gt;A CSV import preview can prevent common mistakes before data is written to the database.&lt;/p&gt;
&lt;p&gt;Instead of immediately importing the uploaded file, the application can provide a preview stage.&lt;/p&gt;
&lt;p&gt;The user uploads a file, the backend parses it, and the frontend displays a small sample of the rows. The interface can identify missing columns, invalid values, duplicate records, and potential conflicts.&lt;/p&gt;
&lt;p&gt;The user can then confirm the import or cancel it.&lt;/p&gt;
&lt;p&gt;A preview workflow is especially useful when the file comes from another application with unfamiliar column names or inconsistent formatting.&lt;/p&gt;
&lt;p&gt;For example, the application may receive a column named &lt;code&gt;Customer Email&lt;/code&gt; when the expected field is &lt;code&gt;email&lt;/code&gt;. A mapping interface allows users to associate incoming columns with database fields.&lt;/p&gt;
&lt;p&gt;The system should validate the mapping before accepting the import and clearly indicate which fields are required.&lt;/p&gt;
&lt;p&gt;For large files, previews should be limited to a reasonable number of rows rather than sending the complete dataset to the browser.&lt;/p&gt;
&lt;h3 id=&quot;14-handling-encoding-and-csv-formatting&quot;&gt;14. Handling Encoding and CSV Formatting&lt;/h3&gt;
&lt;p&gt;CSV files are not always formatted identically.&lt;/p&gt;
&lt;p&gt;One file may use commas, another may use semicolons, and another may contain a byte-order mark at the beginning of the file. Some files use Windows-specific line endings or contain non-English characters.&lt;/p&gt;
&lt;p&gt;These differences can cause parsing problems.&lt;/p&gt;
&lt;h4 id=&quot;utf-8-encoding&quot;&gt;UTF-8 Encoding&lt;/h4&gt;
&lt;p&gt;UTF-8 is a practical default for modern applications. In Python, decoding with &lt;code&gt;utf-8-sig&lt;/code&gt; can remove a UTF-8 byte-order mark when one is present.&lt;/p&gt;
&lt;h4 id=&quot;quoted-fields&quot;&gt;Quoted Fields&lt;/h4&gt;
&lt;p&gt;CSV fields may contain commas, quotation marks, or newline characters. The CSV parser should handle these correctly.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csv&quot;&gt;name,company
&quot;John Smith&quot;,&quot;Smith, Johnson &amp;amp; Co.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Manually splitting each line on commas would incorrectly divide the company field.&lt;/p&gt;
&lt;h4 id=&quot;line-endings&quot;&gt;Line Endings&lt;/h4&gt;
&lt;p&gt;Files can use different newline conventions. Python's CSV documentation recommends opening file objects with &lt;code&gt;newline=&quot;&quot;&lt;/code&gt; when using the &lt;code&gt;csv&lt;/code&gt; module.&lt;/p&gt;
&lt;h4 id=&quot;delimiter-detection&quot;&gt;Delimiter Detection&lt;/h4&gt;
&lt;p&gt;If users regularly upload files from different spreadsheet applications, the application may offer an explicit delimiter option or use carefully constrained detection.&lt;/p&gt;
&lt;p&gt;Automatic detection is not always reliable, so the interface should let users correct the delimiter when necessary.&lt;/p&gt;
&lt;h3 id=&quot;15-securing-the-import-and-export-system&quot;&gt;15. Securing the Import and Export System&lt;/h3&gt;
&lt;p&gt;Security should be part of the design from the beginning.&lt;/p&gt;
&lt;h4 id=&quot;validate-file-size&quot;&gt;Validate File Size&lt;/h4&gt;
&lt;p&gt;Set file-size limits at the application, reverse-proxy, and infrastructure levels. Do not rely solely on a filename or a browser-side restriction.&lt;/p&gt;
&lt;h4 id=&quot;validate-file-content&quot;&gt;Validate File Content&lt;/h4&gt;
&lt;p&gt;A &lt;code&gt;.csv&lt;/code&gt; extension does not prove that the uploaded content is a valid CSV file. Parse the contents and validate the expected structure.&lt;/p&gt;
&lt;h4 id=&quot;enforce-authentication-and-authorization&quot;&gt;Enforce Authentication and Authorization&lt;/h4&gt;
&lt;p&gt;Only authenticated users with the appropriate permissions should be able to import or export records.&lt;/p&gt;
&lt;p&gt;For multi-tenant applications, every database query must also enforce tenant boundaries. Otherwise, users could download or modify another organization's information.&lt;/p&gt;
&lt;h4 id=&quot;protect-against-csv-formula-injection&quot;&gt;Protect Against CSV Formula Injection&lt;/h4&gt;
&lt;p&gt;CSV exports can be dangerous when cells begin with characters that spreadsheet applications interpret as formulas, including &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, or &lt;code&gt;@&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If an exported value contains untrusted user input, opening the file in a spreadsheet application may trigger formula evaluation.&lt;/p&gt;
&lt;p&gt;Mitigations include neutralizing dangerous leading characters according to the target spreadsheet applications and the application's requirements. Quoting a field according to CSV syntax alone does not reliably prevent formula injection.&lt;/p&gt;
&lt;p&gt;Test the chosen mitigation with the spreadsheet applications your users actually use, and preserve the original data in the database rather than silently altering stored values.&lt;/p&gt;
&lt;h4 id=&quot;avoid-sensitive-information-leakage&quot;&gt;Avoid Sensitive Information Leakage&lt;/h4&gt;
&lt;p&gt;Do not export passwords, authentication tokens, internal notes, or confidential fields unless the user has explicit authorization and a valid business reason.&lt;/p&gt;
&lt;p&gt;Keep temporary files in protected storage and remove them according to a defined retention policy.&lt;/p&gt;
&lt;h4 id=&quot;apply-rate-limits&quot;&gt;Apply Rate Limits&lt;/h4&gt;
&lt;p&gt;Large imports and exports can consume substantial CPU, memory, database capacity, and storage. Limit concurrent jobs, enforce quotas, and prevent abusive repeated requests.&lt;/p&gt;
&lt;h4 id=&quot;protect-upload-endpoints-against-csrf&quot;&gt;Protect Upload Endpoints Against CSRF&lt;/h4&gt;
&lt;p&gt;If the application uses cookie-based authentication, implement appropriate CSRF protection and secure cookie settings. CORS configuration alone is not a substitute for CSRF protection.&lt;/p&gt;
&lt;h3 id=&quot;16-handling-errors-and-import-status&quot;&gt;16. Handling Errors and Import Status&lt;/h3&gt;
&lt;p&gt;A good CSV system provides meaningful feedback when something goes wrong.&lt;/p&gt;
&lt;p&gt;Errors can occur during file upload, parsing, validation, database operations, or export generation.&lt;/p&gt;
&lt;p&gt;Instead of returning a generic message for every failure, classify errors into understandable categories.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Invalid file format.&lt;/li&gt;
&lt;li&gt;Missing required columns.&lt;/li&gt;
&lt;li&gt;Unsupported encoding.&lt;/li&gt;
&lt;li&gt;Invalid field values.&lt;/li&gt;
&lt;li&gt;Duplicate records.&lt;/li&gt;
&lt;li&gt;Database constraint violations.&lt;/li&gt;
&lt;li&gt;Permission denied.&lt;/li&gt;
&lt;li&gt;File exceeds the permitted size.&lt;/li&gt;
&lt;li&gt;Import job failed unexpectedly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For background processing, create an import-job table containing the job identifier, initiating user, status, timestamps, and summary counts.&lt;/p&gt;
&lt;p&gt;A job might move through the following states:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pending → processing → completed&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;It may also enter &lt;code&gt;failed&lt;/code&gt; or &lt;code&gt;completed_with_errors&lt;/code&gt;, depending on the application's policy.&lt;/p&gt;
&lt;p&gt;Expose a status endpoint so the frontend can retrieve progress and results. Avoid returning sensitive information in error messages or logs.&lt;/p&gt;
&lt;p&gt;For long-running jobs, retain enough diagnostic information to investigate failures without keeping uploaded files indefinitely.&lt;/p&gt;
&lt;h3 id=&quot;17-making-the-system-scalable-with-background-jobs&quot;&gt;17. Making the System Scalable with Background Jobs&lt;/h3&gt;
&lt;p&gt;Processing a large CSV synchronously can exceed request timeouts and degrade the user experience.&lt;/p&gt;
&lt;p&gt;A background worker architecture separates request handling from file processing.&lt;/p&gt;
&lt;p&gt;The API receives the upload, validates basic requirements, stores the file securely, creates a job record, and returns a job identifier. A worker then processes the file independently.&lt;/p&gt;
&lt;p&gt;A queue such as Celery with Redis or RabbitMQ can coordinate background work, depending on the application's infrastructure.&lt;/p&gt;
&lt;p&gt;The frontend can periodically query the job status or receive updates through WebSockets or server-sent events.&lt;/p&gt;
&lt;p&gt;This architecture offers several benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Long-running imports do not occupy ordinary request workers.&lt;/li&gt;
&lt;li&gt;Jobs can be retried under controlled conditions.&lt;/li&gt;
&lt;li&gt;Processing capacity can be scaled independently.&lt;/li&gt;
&lt;li&gt;Progress can be reported to users.&lt;/li&gt;
&lt;li&gt;Failed jobs can be investigated and resumed safely.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Retries require special attention. If a worker crashes after inserting records but before marking the job complete, processing the file again may create duplicates.&lt;/p&gt;
&lt;p&gt;Use idempotency keys, unique constraints, checkpoints, or deterministic upsert rules to ensure that retrying a job does not produce unintended results.&lt;/p&gt;
&lt;h3 id=&quot;18-testing-the-csv-system&quot;&gt;18. Testing the CSV System&lt;/h3&gt;
&lt;p&gt;Testing should cover both ordinary operations and unusual inputs.&lt;/p&gt;
&lt;h4 id=&quot;unit-tests&quot;&gt;Unit Tests&lt;/h4&gt;
&lt;p&gt;Test parsing, normalization, validation, duplicate detection, and CSV serialization independently.&lt;/p&gt;
&lt;p&gt;Include names containing commas, quotation marks, Unicode characters, and line breaks.&lt;/p&gt;
&lt;h4 id=&quot;integration-tests&quot;&gt;Integration Tests&lt;/h4&gt;
&lt;p&gt;Verify that valid records are saved correctly and invalid records follow the intended transaction policy.&lt;/p&gt;
&lt;p&gt;Test duplicate database constraints, rollback behavior, authorization, and tenant isolation.&lt;/p&gt;
&lt;h4 id=&quot;api-tests&quot;&gt;API Tests&lt;/h4&gt;
&lt;p&gt;Test upload and export endpoints using valid files, empty files, malformed CSV content, oversized uploads, missing headers, and unsupported encodings.&lt;/p&gt;
&lt;h4 id=&quot;frontend-tests&quot;&gt;Frontend Tests&lt;/h4&gt;
&lt;p&gt;Verify file selection, loading states, error messages, successful downloads, and prevention of accidental repeated submissions.&lt;/p&gt;
&lt;h4 id=&quot;performance-tests&quot;&gt;Performance Tests&lt;/h4&gt;
&lt;p&gt;Measure processing time, memory usage, database load, and concurrent job behavior with realistic datasets.&lt;/p&gt;
&lt;p&gt;A system that works for 20 rows may behave very differently with 200,000 rows. Performance tests should reflect expected production workloads.&lt;/p&gt;
&lt;h3 id=&quot;19-monitoring-and-maintaining-the-system&quot;&gt;19. Monitoring and Maintaining the System&lt;/h3&gt;
&lt;p&gt;After deployment, monitor the system to identify problems before they affect many users.&lt;/p&gt;
&lt;p&gt;Useful metrics include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Number of imports and exports.&lt;/li&gt;
&lt;li&gt;Total rows processed.&lt;/li&gt;
&lt;li&gt;Validation failure rate.&lt;/li&gt;
&lt;li&gt;Average and maximum processing duration.&lt;/li&gt;
&lt;li&gt;Background queue length.&lt;/li&gt;
&lt;li&gt;Failed job count.&lt;/li&gt;
&lt;li&gt;Database query duration.&lt;/li&gt;
&lt;li&gt;Temporary storage consumption.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Logs should include a correlation ID or job ID so developers can trace a particular import across application components.&lt;/p&gt;
&lt;p&gt;Avoid recording full CSV contents, passwords, access tokens, or unnecessary personal information in logs.&lt;/p&gt;
&lt;p&gt;Review file-retention rules, database indexes, and worker capacity as usage grows. Remove expired temporary files and export artifacts through a controlled cleanup process.&lt;/p&gt;
&lt;p&gt;A clear operational policy is just as important as the initial implementation.&lt;/p&gt;
&lt;h3 id=&quot;20-common-mistakes-to-avoid&quot;&gt;20. Common Mistakes to Avoid&lt;/h3&gt;
&lt;p&gt;Several mistakes frequently appear in CSV implementations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reading every large file into memory:&lt;/strong&gt; This can exhaust application resources. Use bounded processing or background jobs for larger datasets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Manually splitting rows by commas:&lt;/strong&gt; This fails when values contain quoted commas, newlines, or escaped quotation marks. Use a proper CSV parser.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trusting the file extension:&lt;/strong&gt; Validate content and structure on the server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ignoring duplicate policies:&lt;/strong&gt; Decide how repeated records are handled before deploying the feature.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using unbounded database transactions:&lt;/strong&gt; Large transactions can increase lock duration and resource consumption. Choose batch boundaries carefully.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Returning vague errors:&lt;/strong&gt; Include actionable row-level feedback without exposing sensitive server details.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ignoring spreadsheet formula injection:&lt;/strong&gt; Treat exported values as potentially dangerous when opened in spreadsheet software.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Forgetting authorization:&lt;/strong&gt; Enforce access controls on both import and export endpoints, not only in the frontend.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Retrying jobs without idempotency:&lt;/strong&gt; Ensure that interrupted jobs can be retried without producing unintended duplicates.&lt;/p&gt;
&lt;p&gt;Avoiding these mistakes will make the system more secure, predictable, and maintainable.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Building a CSV import and export system is a practical way to make a web application more flexible and useful. It enables users to move data efficiently, perform bulk operations, generate reports, and integrate information with other business tools.&lt;/p&gt;
&lt;p&gt;However, a reliable implementation requires more than reading rows and writing files. Developers must design clear API boundaries, validate incoming data, establish duplicate-handling rules, protect database integrity, generate efficient exports, and provide meaningful error reports.&lt;/p&gt;
&lt;p&gt;A React frontend combined with a FastAPI backend and PostgreSQL database provides a strong foundation for this functionality. Python's built-in CSV module handles standard parsing and serialization, while background workers and controlled database operations support larger workloads.&lt;/p&gt;
&lt;p&gt;Security also deserves special attention. File-size restrictions, authorization checks, tenant isolation, safe temporary storage, and CSV formula-injection defenses help prevent common vulnerabilities.&lt;/p&gt;
&lt;p&gt;Start with a small implementation that supports well-defined columns and straightforward validation. Then introduce previews, field mapping, background processing, detailed reports, and scalable exports as the application's needs grow.&lt;/p&gt;
&lt;p&gt;By treating CSV processing as a complete data workflow rather than a simple file operation, developers can build a feature that remains reliable as both the application and its users grow.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32371/building-a-csv-import-and-export-system-for-a-web-application</guid>
<pubDate>Sun, 25 Oct 2026 08:09:06 +0000</pubDate>
</item>
<item>
<title>Mastering React Performance: Optimizations That Actually Matter 🚀</title>
<link>https://coderlegion.com/32367/mastering-react-performance-optimizations-that-actually-matter</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;React is a powerful JavaScript library, but as applications grow, performance bottlenecks can arise. Unnecessary re-renders, inefficient state management, and large bundle sizes can lead to slow applications.&lt;/p&gt;
&lt;p&gt;This guide covers practical performance optimizations that actually matter. Whether you’re working on a small project or a large-scale application, these best practices will help you write faster, scalable, and efficient React applications.&lt;/p&gt;
&lt;p&gt;By the end of this blog, you’ll learn:&lt;br&gt;
✅ Why React apps slow down&lt;br&gt;
✅ Best practices for optimizing re-renders&lt;br&gt;
✅ How to efficiently manage state&lt;br&gt;
✅ How to optimize large lists&lt;br&gt;
✅ lazy loading for faster load times&lt;/p&gt;
&lt;p&gt;Let’s dive in! 🚀&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;1-understanding-react-s-rendering-behavior&quot;&gt;1️⃣ Understanding React’s Rendering Behavior&lt;/h4&gt;
&lt;p&gt;Before optimizing, it’s crucial to understand how React renders components.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;📌 React re-renders a component when:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;State changes in that component.&lt;/li&gt;
&lt;li&gt;Props change from the parent component.&lt;/li&gt;
&lt;li&gt;A parent re-renders, causing child components to re-render.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;🔍 How to detect unnecessary re-renders?&lt;/strong&gt;&lt;br&gt;
Use &lt;a rel=&quot;nofollow&quot; href=&quot;https://legacy.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html&quot;&gt;React DevTools Profiler&lt;/a&gt; to analyze component renders.&lt;/p&gt;
&lt;p&gt;✅ Solution: Memoization &amp;amp; Avoiding Unnecessary Renders&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-optimize-component-re-renders-with-react-memo&quot;&gt;2️⃣ Optimize Component Re-Renders with &lt;code&gt;React.memo&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;By default, React re-renders a component &lt;strong&gt;when its parent renders&lt;/strong&gt;, even if props haven’t changed.&lt;/p&gt;
&lt;p&gt;🚀 Solution: Use &lt;code&gt;React.memo()&lt;/code&gt; to prevent unnecessary renders&lt;/p&gt;
&lt;p&gt;🔴 Without &lt;code&gt;React.memo&lt;/code&gt; (Unnecessary re-renders)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { useState } from &quot;react&quot;;

const MovieDetails = ({ title, director }) =&amp;gt; {
  console.log(&quot;🎬 MovieDetails component re-rendered&quot;);
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h2&amp;gt;{title}&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;Directed by: {director}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

const App = () =&amp;gt; {
  const [count, setCount] = useState(0);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;React.memo Example&amp;lt;/h1&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;Click Me ({count})&amp;lt;/button&amp;gt;
      &amp;lt;MovieDetails title=&quot;Inception&quot; director=&quot;Christopher Nolan&quot; /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 Here, &lt;code&gt;MovieDetails&lt;/code&gt; &lt;strong&gt;re-renders every time&lt;/strong&gt; the parent renders, even if props hasn't changed.&lt;/p&gt;
&lt;p&gt;🟢 Optimizing with &lt;code&gt;React.memo&lt;/code&gt;&lt;br&gt;
To prevent unnecessary re-renders, wrap &lt;code&gt;MovieDetails&lt;/code&gt; with &lt;code&gt;React.memo()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const MovieDetails = React.memo(({ title, director }) =&amp;gt; {
  console.log(&quot;🎬 MovieDetails component re-rendered&quot;);
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h2&amp;gt;{title}&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;Directed by: {director}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 Now, &lt;code&gt;MovieDetails&lt;/code&gt; will only re-render if its props (title or director) change.&lt;/p&gt;
&lt;p&gt;Clicking the button will no longer trigger a re-render of &lt;code&gt;MovieDetails&lt;/code&gt; unless new props are passed.&lt;/p&gt;
&lt;p&gt;📌 When to use &lt;code&gt;React.memo&lt;/code&gt;?&lt;br&gt;
✅ The component receives static or rarely changing props (e.g., user profile, movie info).&lt;br&gt;
✅ The component is expensive to render (e.g., complex UI elements like charts).&lt;br&gt;
✅ The parent re-renders frequently, but child props remain the same.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-minimize-re-renders-with-derived-state&quot;&gt;3️⃣ Minimize Re-Renders with Derived State&lt;/h4&gt;
&lt;p&gt;Unnecessary state updates can cause frequent re-renders, slowing down your React app. Instead of storing derived state, compute values when needed.&lt;/p&gt;
&lt;p&gt;🔴 Problem: Storing Derived State in useState (Causes Extra Re-Renders)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const [items, setItems] = useState([...]); 
const [filteredItems, setFilteredItems] = useState([]);

useEffect(() =&amp;gt; {
  setFilteredItems(items.filter(item =&amp;gt; item.active));
}, [items]); // Extra re-render every time `items` change
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;📌 Issue:&lt;br&gt;
Every update to items triggers an unnecessary state update for &lt;code&gt;filteredItems&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This causes an extra re-render that could have been avoided.&lt;/p&gt;
&lt;p&gt;🟢 Solution: Use Derived State Instead of Storing It&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const filteredItems = items.filter(item =&amp;gt; item.active);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;✅ Now, &lt;code&gt;filteredItems&lt;/code&gt; is computed only when needed, avoiding extra state updates and re-renders.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;4-optimize-function-props-with-usecallback&quot;&gt;4️⃣ Optimize Function Props with &lt;code&gt;useCallback&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Passing new function instances as props causes child components to unnecessarily re-render.&lt;/p&gt;
&lt;p&gt;🔴 Problem: Without &lt;code&gt;useCallback&lt;/code&gt; (Function re-creates on every render)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ParentComponent = () =&amp;gt; {
  const handleClick = () =&amp;gt; {
    console.log(&quot;Button clicked&quot;);
  };

  return &amp;lt;ButtonComponent handleClick={handleClick} /&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 Even if &lt;code&gt;handleClick&lt;/code&gt; doesn’t change, it gets re-created every time &lt;code&gt;ParentComponent&lt;/code&gt; renders.&lt;/p&gt;
&lt;p&gt;🟢 Solution: Use &lt;code&gt;useCallback&lt;/code&gt; to memoize the function&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const handleClick = useCallback(() =&amp;gt; {
  console.log(&quot;Button clicked&quot;);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Now, &lt;code&gt;handleClick&lt;/code&gt; retains the &lt;strong&gt;same reference&lt;/strong&gt;, preventing unnecessary re-renders.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;5-optimize-expensive-computations-with-usememo&quot;&gt;5️⃣ Optimize Expensive Computations with &lt;code&gt;useMemo&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;If a component performs heavy calculations on every render, it can slow down the UI.&lt;/p&gt;
&lt;p&gt;🔴 Problem: Without &lt;code&gt;useMemo&lt;/code&gt; (Expensive re-computation)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const heavyComputation = (num) =&amp;gt; {
  console.log(&quot;Expensive calculation running...&quot;);
  return num * 2;
};

const Component = ({ number }) =&amp;gt; {
  const result = heavyComputation(number); // Runs on every render
  return &amp;lt;p&amp;gt;Result: {result}&amp;lt;/p&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🟢 Solution: Use &lt;code&gt;useMemo&lt;/code&gt; to cache expensive computations&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const result = useMemo(() =&amp;gt; heavyComputation(number), [number]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ &lt;code&gt;heavyComputation()&lt;/code&gt; only re-runs when &lt;code&gt;number&lt;/code&gt; changes.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;6-optimize-lists-amp-large-data-rendering&quot;&gt;6️⃣ Optimize Lists &amp;amp; Large Data Rendering&lt;/h4&gt;
&lt;p&gt;🔍 Problem: Rendering large lists slows down UI&lt;br&gt;
Solution: Use Virtualization with &lt;code&gt;react-window&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;🔴 Without Virtualization (Slow performance)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const List = ({ items }) =&amp;gt; {
  return (
    &amp;lt;ul&amp;gt;
      {items.map((item) =&amp;gt; (
        &amp;lt;li key={item.id}&amp;gt;{item.name}&amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🟢 With Virtualization (react-window)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { FixedSizeList as List } from &quot;react-window&quot;;

const VirtualizedList = ({ items }) =&amp;gt; (
  &amp;lt;List height={500} itemCount={items.length} itemSize={35} width={300}&amp;gt;
    {({ index, style }) =&amp;gt; (
      &amp;lt;div style={style}&amp;gt;{items[index].name}&amp;lt;/div&amp;gt;
    )}
  &amp;lt;/List&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Only visible items are rendered, improving performance significantly.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;7-reduce-bundle-size-with-code-splitting-amp-lazy-loading&quot;&gt;7️⃣ Reduce Bundle Size with Code Splitting &amp;amp; Lazy Loading&lt;/h4&gt;
&lt;p&gt;Large JavaScript bundles slow down initial page load.&lt;/p&gt;
&lt;p&gt;✅ Solution: Use &lt;code&gt;React.lazy()&lt;/code&gt; for dynamic imports&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const HeavyComponent = React.lazy(() =&amp;gt; import(&quot;./HeavyComponent&quot;));

const App = () =&amp;gt; (
  &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;}&amp;gt;
    &amp;lt;HeavyComponent /&amp;gt;
  &amp;lt;/Suspense&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 Components load only when needed, reducing bundle size.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;conclusion-build-fast-amp-scalable-react-apps&quot;&gt;Conclusion: Build Fast &amp;amp; Scalable React Apps&lt;/h4&gt;
&lt;p&gt;Optimizing React apps is crucial for fast UI interactions and smooth performance. By implementing:&lt;br&gt;
✅ Memoization (&lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;)&lt;br&gt;
✅ Efficient state management&lt;br&gt;
✅ Virtualization for large lists&lt;br&gt;
✅ Lazy loading for faster page loads&lt;/p&gt;
&lt;p&gt;You’ll significantly improve performance! 🚀&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;💡 What performance tricks do you use in React?&lt;/strong&gt; Drop a comment below! 👇&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32367/mastering-react-performance-optimizations-that-actually-matter</guid>
<pubDate>Sun, 25 Oct 2026 05:57:59 +0000</pubDate>
</item>
<item>
<title>Your Next.js Dates Are Wrong for Half Your Users (and the Hydration Warning Is Only the Start)</title>
<link>https://coderlegion.com/31957/your-next-js-dates-are-wrong-for-half-your-users-and-the-hydration-warning-is-only-the-start</link>
<description>&lt;p&gt;You format a date in a component. It looks right on your laptop. Then a user in another country opens the page and either sees a console full of hydration errors, or worse, sees nothing wrong at all and reads the wrong time.&lt;/p&gt;
&lt;p&gt;I live in a timezone that is five hours away from where most servers run, so this one finds me every time.&lt;/p&gt;
&lt;h3 id=&quot;the-code-that-looks-fine&quot;&gt;The Code That Looks Fine&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// components/OrderDate.tsx
export function OrderDate({ date }: { date: string }) {
  return &amp;lt;p&amp;gt;Ordered on {new Date(date).toLocaleString()}&amp;lt;/p&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No hooks, no browser APIs, nothing that looks risky. It even works in &lt;code&gt;next dev&lt;/code&gt;, because on your machine the server and the browser are the same computer.&lt;/p&gt;
&lt;h3 id=&quot;why-it-breaks&quot;&gt;Why It Breaks&lt;/h3&gt;
&lt;p&gt;In Next.js, this component runs &lt;strong&gt;twice&lt;/strong&gt;: once on the server to produce the HTML, and again in the browser to hydrate it. &lt;code&gt;toLocaleString()&lt;/code&gt; with no arguments uses the &lt;strong&gt;timezone and language of whatever machine runs it&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The server is often in UTC, with a default locale such as &lt;code&gt;en-US&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Your user's browser might be in Karachi, with &lt;code&gt;en-GB&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Server HTML:   Ordered on 10/20/2026, 10:15:00 AM
Browser render: Ordered on 20/10/2026, 15:15:00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React compares the two, finds that they differ, and logs a hydration error. In development, you see it. In production, React quietly tries to recover by re-rendering on the client, which costs time and can cause a visible flash.&lt;/p&gt;
&lt;h3 id=&quot;why-it-is-hard-to-catch&quot;&gt;Why It Is Hard to Catch&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It never fails where you test.&lt;/strong&gt; Same machine, same timezone, same locale: the two outputs are identical.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Many users never see an error.&lt;/strong&gt; The page just shows one format for a split second and then another.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It often hides inside other pieces of data.&lt;/strong&gt; &lt;code&gt;Date.now()&lt;/code&gt;, &lt;code&gt;Math.random()&lt;/code&gt;, &lt;code&gt;new Date().getFullYear()&lt;/code&gt; and anything that reads &lt;code&gt;window&lt;/code&gt; or &lt;code&gt;localStorage&lt;/code&gt; can all produce a different value on the server and in the browser.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-fix-that-is-tempting-and-wrong&quot;&gt;The Fix That Is Tempting, and Wrong&lt;/h3&gt;
&lt;p&gt;You will find this advice everywhere:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;p suppressHydrationWarning&amp;gt;
  Ordered on {new Date(date).toLocaleString()}
&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It does silence the warning. But per React's documentation, it only suppresses the warning on that one element and does not patch up mismatched text. The user can end up looking at the &lt;strong&gt;server's&lt;/strong&gt; version of the time, which is in the wrong timezone.&lt;/p&gt;
&lt;p&gt;You did not fix the bug, you removed the alarm for it.&lt;/p&gt;
&lt;h3 id=&quot;fix-1-make-the-output-the-same-on-both-sides&quot;&gt;Fix 1: Make the Output the Same on Both Sides&lt;/h3&gt;
&lt;p&gt;Pass an explicit locale and timezone, so the server and the browser calculate the same string:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function OrderDate({ date }: { date: string }) {
  const text = new Date(date).toLocaleString('en-US', {
    timeZone: 'UTC',
    dateStyle: 'medium',
    timeStyle: 'short',
  });

  return &amp;lt;time dateTime={date}&amp;gt;{text} UTC&amp;lt;/time&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is deterministic, so there is nothing to mismatch. It is the right choice for logs, admin tables and anything where showing UTC is acceptable. Label it as UTC so nobody reads it as local time.&lt;/p&gt;
&lt;h3 id=&quot;fix-2-show-the-local-time-after-the-page-loads&quot;&gt;Fix 2: Show the Local Time After the Page Loads&lt;/h3&gt;
&lt;p&gt;If users should see their own timezone, only compute it in the browser:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;'use client';

import { useEffect, useState } from 'react';

export function LocalDate({ date }: { date: string }) {
  const [local, setLocal] = useState&amp;lt;string | null&amp;gt;(null);

  useEffect(() =&amp;gt; {
    setLocal(
      new Date(date).toLocaleString(undefined, {
        dateStyle: 'medium',
        timeStyle: 'short',
      })
    );
  }, [date]);

  return (
    &amp;lt;time dateTime={date}&amp;gt;
      {local ?? new Date(date).toISOString().slice(0, 10)}
    &amp;lt;/time&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first render, on the server and in the browser, shows a stable fallback (the ISO date). Right after hydration, &lt;code&gt;useEffect&lt;/code&gt; runs only in the browser and swaps in the local time. Nothing mismatches, and the user ends up with the right value.&lt;/p&gt;
&lt;p&gt;The cost is a brief change from the fallback to the local string. Keep the fallback short so the layout does not jump.&lt;/p&gt;
&lt;h3 id=&quot;fix-3-store-and-send-dates-as-iso-strings&quot;&gt;Fix 3: Store and Send Dates as ISO Strings&lt;/h3&gt;
&lt;p&gt;Whichever display option you pick, pass dates around as ISO strings (&lt;code&gt;2026-10-20T10:15:00.000Z&lt;/code&gt;) and format them only at the edge, in one place. An ISO string is plain text, so it means the same thing on the server, in the browser and in your database.&lt;/p&gt;
&lt;h3 id=&quot;which-one-when&quot;&gt;Which One When&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Situation &lt;/th&gt;&lt;th&gt;Fix&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Admin tables, logs, reports &lt;/td&gt;&lt;td&gt;Fixed locale and &lt;code&gt;timeZone: 'UTC'&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Customer-facing times (orders, bookings, messages) &lt;/td&gt;&lt;td&gt;Format in the browser after mount&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;A date that never changes (blog post date) &lt;/td&gt;&lt;td&gt;Fixed locale and timezone, since the author's date is what matters&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;the-rule-2&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Any value that depends on where the code runs, such as timezone, locale, the current time or a random number, will differ between the server and the browser.&lt;/strong&gt; Either make it identical on both sides, or compute it only in the browser after hydration. Do not suppress the warning and hope.&lt;/p&gt;
&lt;p&gt;Quick audit: search your project for &lt;code&gt;toLocaleString(&lt;/code&gt;, &lt;code&gt;toLocaleDateString(&lt;/code&gt;, &lt;code&gt;Date.now()&lt;/code&gt; and &lt;code&gt;Math.random()&lt;/code&gt; inside components that render on the server. Each one is either deterministic on purpose, or a mismatch that has not met a user in another timezone yet.&lt;/p&gt;
&lt;p&gt;Hydration mistakes also slow pages down, because React has to redo work in the browser. I explain what that does to real-world speed scores in &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com/blog/core-web-vitals-explained&quot;&gt;this plain-language guide to Core Web Vitals&lt;/a&gt;, and I keep every date in my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;Next.js templates&lt;/a&gt; formatted through one small helper so the server and the browser always agree.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Which timezone bug has bitten you hardest? Drop it in the comments, especially if it only showed up in production.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31957/your-next-js-dates-are-wrong-for-half-your-users-and-the-hydration-warning-is-only-the-start</guid>
<pubDate>Wed, 21 Oct 2026 11:48:27 +0000</pubDate>
</item>
<item>
<title>Mastering the Critical Path in Performance Budgets for Faster React Apps</title>
<link>https://coderlegion.com/31814/mastering-the-critical-path-in-performance-budgets-for-faster-react-apps</link>
<description>&lt;h3 id=&quot;what-is-the-critical-path-in-performance-budgets-&quot;&gt;What is the Critical Path in Performance Budgets?&lt;/h3&gt;
&lt;p&gt;A performance budget is a set of strict limits agreed upon by a web development team to cap resource sizes, file counts, or loading speeds. The critical path—often called the Critical Rendering Path—is the exact sequence of assets and browser processing steps required to render the initial visible portion of a webpage. Combining these two concepts means setting explicit size and time limits specifically for the code and assets that block the initial page display.&lt;/p&gt;
&lt;h3 id=&quot;the-airport-fast-track-analogy&quot;&gt;The Airport Fast Track Analogy&lt;/h3&gt;
&lt;p&gt;Imagine walking through an airport terminal to board a flight. To get through the boarding gate, you only need two essential items in your hand: your passport and your boarding pass. These two items represent your critical path. &lt;/p&gt;
&lt;p&gt;If you try to haul three oversized suitcases, two heavy coats, and a duty-free shopping bag through the gate check all at once, you will slow down the entire line. A critical path performance budget is like an airport rule stating that passengers can only hold their passport and ticket while stepping up to the gate. Everything else—your checked luggage and heavy items—must be handled separately so you can pass through instantly.&lt;/p&gt;
&lt;h3 id=&quot;why-it-matters-in-daily-software-engineering&quot;&gt;Why it Matters in Daily Software Engineering&lt;/h3&gt;
&lt;p&gt;In modern web development, teams often bundle massive amounts of JavaScript and CSS together. When a user opens an application, the browser must download, parse, and execute all of this code before showing a single pixel of the user interface. &lt;/p&gt;
&lt;p&gt;If software engineers do not restrict what travels down the critical path, the browser gets clogged with non-essential files—such as analytics scripts, chat widgets, or secondary modal dialogues. By defining a strict performance budget for the critical path, engineers guarantee that the essential visual structure loads within milliseconds, keeping users engaged and preventing high bounce rates on mobile networks.&lt;/p&gt;
&lt;h3 id=&quot;practical-code-example-react-code-splitting&quot;&gt;Practical Code Example: React Code Splitting&lt;/h3&gt;
&lt;p&gt;To keep our critical path budget light, we should dynamically import non-essential React components so they do not block the main render. Here is how you can defer non-critical widgets using &lt;code&gt;React.lazy&lt;/code&gt; and &lt;code&gt;Suspense&lt;/code&gt; in a React application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React, { Suspense, lazy } from 'react';

// Critical component: Loaded immediately for the initial render
import Header from './Header';
import MainContent from './MainContent';

// Non-critical component: Loaded on-demand outside the critical path
const HeavyAnalyticsDashboard = lazy(() =&amp;gt; import('./HeavyAnalyticsDashboard'));

function App() {
  return (
    &amp;lt;div className=&quot;app-container&quot;&amp;gt;
      {/* Essential components render without delay */}
      &amp;lt;Header /&amp;gt;
      &amp;lt;MainContent /&amp;gt;

      {/* Non-critical component is deferred with a lightweight fallback */}
      &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading analytics...&amp;lt;/div&amp;gt;}&amp;gt;
        &amp;lt;HeavyAnalyticsDashboard /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By splitting the bundle, &lt;code&gt;HeavyAnalyticsDashboard&lt;/code&gt; is pulled out of the critical rendering budget, ensuring the &lt;code&gt;Header&lt;/code&gt; and &lt;code&gt;MainContent&lt;/code&gt; display almost instantaneously.&lt;/p&gt;
&lt;h3 id=&quot;key-takeaway-1&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;Managing your critical path within a performance budget is not about stripping away feature-rich functionality; it is about controlling &lt;em&gt;when&lt;/em&gt; those features are delivered. By keeping your initial rendering pipeline lean and pushing non-essential scripts into secondary loading phases, you preserve ultra-fast initial page loads without sacrificing modern application capabilities.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/why-your-app-feels-slow-enforcing.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31814/mastering-the-critical-path-in-performance-budgets-for-faster-react-apps</guid>
<pubDate>Tue, 20 Oct 2026 09:05:05 +0000</pubDate>
</item>
<item>
<title>Build a Slide-In Mobile Menu in TanStack Start with Tailwind v4</title>
<link>https://coderlegion.com/31644/build-a-slide-in-mobile-menu-in-tanstack-start-with-tailwind-v4</link>
<description>&lt;p&gt;A mobile menu that pops in and out works, but it feels unfinished. In this article we turn it into a drawer that slides in from the right with a fading backdrop, using only Tailwind classes and no animation library. We also keep it usable for keyboard and screen-reader users, lock the page scroll while it is open, and respect the reduced-motion setting.&lt;/p&gt;
&lt;p&gt;This is part of the &lt;strong&gt;TanStack Start Basics&lt;/strong&gt; series. It builds on the responsive &lt;code&gt;Header&lt;/code&gt; and the separate &lt;code&gt;NavLinks&lt;/code&gt; component from the previous part.&lt;/p&gt;
&lt;p&gt;You can view the video &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=4oE4P7uGF3Y&quot;&gt;Here&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;remove-the-old-mobile-menu&quot;&gt;Remove the old mobile menu&lt;/h3&gt;
&lt;p&gt;In the previous part, the mobile menu lived in the &lt;code&gt;Header&lt;/code&gt;, underneath the flex container, and the hamburger button toggled it. Delete it. This is the code to remove:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;{open &amp;amp;&amp;amp; (
  &amp;lt;nav
    id=&quot;mobile-menu&quot;
    aria-label=&quot;Mobile&quot;
    className=&quot;border-t md:hidden&quot;
  &amp;gt;
    &amp;lt;NavLinks
      listClass=&quot;flex flex-col px-6 py-2&quot;
      linkClass=&quot;block py-3 text-base font-medium text-neutral-600 hover:text-black&quot;
      onNavigate={() =&amp;gt; setOpen(false)}
    /&amp;gt;
  &amp;lt;/nav&amp;gt;
)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When &lt;code&gt;open&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, React removes this menu from the page completely. A CSS transition needs an element that exists in both the start and end states, so there is nothing to animate. The fix is a side panel that is always on the page, which we slide off screen with Tailwind.&lt;/p&gt;
&lt;h3 id=&quot;always-render-the-panel&quot;&gt;Always render the panel&lt;/h3&gt;
&lt;p&gt;Here is the side panel. It replaces the old menu in the same position in the header. It uses a &lt;code&gt;cn&lt;/code&gt; helper, so you will also need this function in the &lt;code&gt;utils&lt;/code&gt; file in your &lt;code&gt;lib&lt;/code&gt; folder. If you use shadcn/ui, you already have it. Otherwise, install &lt;code&gt;clsx&lt;/code&gt; and &lt;code&gt;tailwind-merge&lt;/code&gt; (&lt;code&gt;npm i clsx tailwind-merge&lt;/code&gt;) and add:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/lib/utils.ts
import type { ClassValue } from 'clsx'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The panel also uses the &lt;code&gt;X&lt;/code&gt; icon, so add these imports at the top of &lt;code&gt;Header.tsx&lt;/code&gt; if you don't have them yet:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;nav
  id=&quot;mobile-menu&quot;
  aria-label=&quot;Mobile&quot;
  className={cn(
    'fixed inset-y-0 right-0 z-50 w-72 bg-white p-6 shadow-xl md:hidden',
  )}
&amp;gt;
  &amp;lt;button
    type=&quot;button&quot;
    onClick={() =&amp;gt; setOpen(false)}
    aria-label=&quot;Close menu&quot;
    className=&quot;mb-6 ml-auto block rounded p-2 hover:bg-gray-100&quot;
  &amp;gt;
    &amp;lt;X className=&quot;size-6&quot; /&amp;gt;
  &amp;lt;/button&amp;gt;
  &amp;lt;NavLinks
    listClass=&quot;flex flex-col gap-4&quot;
    linkClass=&quot;text-lg text-gray-600 hover:text-black&quot;
    onNavigate={() =&amp;gt; setOpen(false)}
  /&amp;gt;
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;make-it-jump-in-and-out&quot;&gt;Make it jump in and out&lt;/h3&gt;
&lt;p&gt;To move the panel on and off the screen, add Tailwind's translate classes with a ternary on &lt;code&gt;open&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;open ? 'translate-x-0' : 'translate-x-full',
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add it as a second argument to &lt;code&gt;cn&lt;/code&gt; on the &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;nav
  id=&quot;mobile-menu&quot;
  aria-label=&quot;Mobile&quot;
  className={cn(
    'fixed inset-y-0 right-0 z-50 w-72 bg-white p-6 shadow-xl md:hidden',
    open ? 'translate-x-0' : 'translate-x-full',
  )}
&amp;gt;
  {/* the close button and NavLinks stay the same */}
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The panel now jumps on and off the screen, but it doesn't slide yet.&lt;/p&gt;
&lt;p&gt;You may have noticed the helper function &lt;code&gt;cn&lt;/code&gt;. It joins class names, skips the ones that are false, and if two Tailwind classes conflict, the last one wins.&lt;/p&gt;
&lt;h3 id=&quot;add-the-backdrop&quot;&gt;Add the backdrop&lt;/h3&gt;
&lt;p&gt;Next, a dark layer behind the panel. Clicking it closes the menu, which is what people expect from a drawer. It sits one layer below the panel (&lt;code&gt;z-40&lt;/code&gt; against &lt;code&gt;z-50&lt;/code&gt;) and covers the whole screen. When the menu is closed it must not catch clicks, so &lt;code&gt;pointer-events-none&lt;/code&gt; is not optional here. Without it, an invisible full-screen layer would swallow every tap on the page. Put it right above the &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div
  aria-hidden=&quot;true&quot;
  onClick={() =&amp;gt; setOpen(false)}
  className={cn(
    'fixed inset-0 z-40 bg-black/50 md:hidden',
    open ? 'opacity-100' : 'pointer-events-none opacity-0',
  )}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is how it should look in your code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div
  aria-hidden=&quot;true&quot;
  onClick={() =&amp;gt; setOpen(false)}
  className={cn(
    'fixed inset-0 z-40 bg-black/50 md:hidden',
    open ? 'opacity-100' : 'pointer-events-none opacity-0',
  )}
/&amp;gt;
&amp;lt;nav
  id=&quot;mobile-menu&quot;
  aria-label=&quot;Mobile&quot;
  className={cn(
    'fixed inset-y-0 right-0 z-50 w-72 bg-white p-6 shadow-xl md:hidden',
    open ? 'translate-x-0' : 'translate-x-full',
  )}
&amp;gt;
  &amp;lt;button
    type=&quot;button&quot;
    onClick={() =&amp;gt; setOpen(false)}
    aria-label=&quot;Close menu&quot;
    className=&quot;mb-6 ml-auto block rounded p-2 hover:bg-gray-100&quot;
  &amp;gt;
    &amp;lt;X className=&quot;size-6&quot; /&amp;gt;
  &amp;lt;/button&amp;gt;
  &amp;lt;NavLinks
    listClass=&quot;flex flex-col gap-4&quot;
    linkClass=&quot;text-lg text-gray-600 hover:text-black&quot;
    onNavigate={() =&amp;gt; setOpen(false)}
  /&amp;gt;
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;make-it-slide&quot;&gt;Make it slide&lt;/h3&gt;
&lt;p&gt;Now the fun part, and it takes two classes. &lt;code&gt;transition-transform&lt;/code&gt; tells the browser to animate changes to the position, &lt;code&gt;duration-300&lt;/code&gt; makes it take 300 milliseconds, and &lt;code&gt;ease-out&lt;/code&gt; makes it start fast and settle gently. The backdrop gets the same treatment, but it fades, so it uses &lt;code&gt;transition-opacity&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Add this to the panel:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;transition-transform duration-300 ease-out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add this to the backdrop:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;transition-opacity duration-300
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your code should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div
  aria-hidden=&quot;true&quot;
  onClick={() =&amp;gt; setOpen(false)}
  className={cn(
    'fixed inset-0 z-40 bg-black/50 transition-opacity duration-300 md:hidden',
    open ? 'opacity-100' : 'pointer-events-none opacity-0',
  )}
/&amp;gt;
&amp;lt;nav
  id=&quot;mobile-menu&quot;
  aria-label=&quot;Mobile&quot;
  className={cn(
    'fixed inset-y-0 right-0 z-50 w-72 bg-white p-6 shadow-xl transition-transform duration-300 ease-out md:hidden',
    open ? 'translate-x-0' : 'translate-x-full',
  )}
&amp;gt;
  &amp;lt;button
    type=&quot;button&quot;
    onClick={() =&amp;gt; setOpen(false)}
    aria-label=&quot;Close menu&quot;
    className=&quot;mb-6 ml-auto block rounded p-2 hover:bg-gray-100&quot;
  &amp;gt;
    &amp;lt;X className=&quot;size-6&quot; /&amp;gt;
  &amp;lt;/button&amp;gt;
  &amp;lt;NavLinks
    listClass=&quot;flex flex-col gap-4&quot;
    linkClass=&quot;text-lg text-gray-600 hover:text-black&quot;
    onNavigate={() =&amp;gt; setOpen(false)}
  /&amp;gt;
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One Tailwind v4 detail: &lt;code&gt;translate-x-*&lt;/code&gt; sets the CSS &lt;code&gt;translate&lt;/code&gt; property, not &lt;code&gt;transform&lt;/code&gt;. In v4, &lt;code&gt;transition-transform&lt;/code&gt; includes &lt;code&gt;translate&lt;/code&gt;, so this works. If a slide ever refuses to animate, check that your transition covers &lt;code&gt;translate&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;hide-the-closed-panel&quot;&gt;Hide the closed panel&lt;/h3&gt;
&lt;p&gt;It looks right, but there is a catch. The panel is only parked off screen. Press Tab with the menu closed and focus disappears into links you can't see, and a screen reader will still read them out. The fix is the &lt;code&gt;invisible&lt;/code&gt; class when the menu is closed.&lt;/p&gt;
&lt;p&gt;If we add &lt;code&gt;invisible&lt;/code&gt; and nothing else, the panel vanishes the instant you close it, and the slide-out never shows. So we also tell the transition to animate &lt;code&gt;visibility&lt;/code&gt;, not just position. The browser keeps the panel visible until the slide finishes, then hides it.&lt;/p&gt;
&lt;p&gt;Make two changes to the panel:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Replace &lt;code&gt;transition-transform&lt;/code&gt; with &lt;code&gt;transition-[translate,visibility]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;translate-x-full&lt;/code&gt; with &lt;code&gt;invisible translate-x-full&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Your code should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div
  aria-hidden=&quot;true&quot;
  onClick={() =&amp;gt; setOpen(false)}
  className={cn(
    'fixed inset-0 z-40 bg-black/50 transition-opacity duration-300 md:hidden',
    open ? 'opacity-100' : 'pointer-events-none opacity-0',
  )}
/&amp;gt;
&amp;lt;nav
  id=&quot;mobile-menu&quot;
  aria-label=&quot;Mobile&quot;
  className={cn(
    'fixed inset-y-0 right-0 z-50 w-72 bg-white p-6 shadow-xl transition-[translate,visibility] duration-300 ease-out md:hidden',
    open ? 'translate-x-0' : 'invisible translate-x-full',
  )}
&amp;gt;
  &amp;lt;button
    type=&quot;button&quot;
    onClick={() =&amp;gt; setOpen(false)}
    aria-label=&quot;Close menu&quot;
    className=&quot;mb-6 ml-auto block rounded p-2 hover:bg-gray-100&quot;
  &amp;gt;
    &amp;lt;X className=&quot;size-6&quot; /&amp;gt;
  &amp;lt;/button&amp;gt;
  &amp;lt;NavLinks
    listClass=&quot;flex flex-col gap-4&quot;
    linkClass=&quot;text-lg text-gray-600 hover:text-black&quot;
    onNavigate={() =&amp;gt; setOpen(false)}
  /&amp;gt;
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;stop-the-page-from-scrolling&quot;&gt;Stop the page from scrolling&lt;/h3&gt;
&lt;p&gt;With the drawer open, scrolling still moves the page behind it, which feels sloppy. We already have an effect that listens for the Escape key, so we extend it: lock scrolling when the menu opens, and unlock it in the cleanup. Replace the &lt;code&gt;useEffect&lt;/code&gt; from the previous part, the one that closes the menu on Escape, with this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&amp;gt; {
  if (!open) return

  const onKeyDown = (e: KeyboardEvent) =&amp;gt; {
    if (e.key === 'Escape') setOpen(false)
  }

  document.addEventListener('keydown', onKeyDown)
  document.body.style.overflow = 'hidden'

  return () =&amp;gt; {
    document.removeEventListener('keydown', onKeyDown)
    document.body.style.overflow = ''
  }
}, [open])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The cleanup function runs when the menu closes and also if the &lt;code&gt;Header&lt;/code&gt; ever unmounts, so the page can never get stuck locked.&lt;/p&gt;
&lt;h3 id=&quot;respect-reduced-motion-optional-&quot;&gt;Respect reduced motion (optional)&lt;/h3&gt;
&lt;p&gt;Some people turn on &quot;reduce motion&quot; in their system settings because animation makes them dizzy. Tailwind gives us a variant for that, &lt;code&gt;motion-reduce:&lt;/code&gt;, so honoring it costs one class per element. If that's you, add &lt;code&gt;motion-reduce:transition-none&lt;/code&gt; to the panel and the backdrop, like so.&lt;/p&gt;
&lt;p&gt;Panel:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;className={cn(
  'fixed inset-y-0 right-0 z-50 w-72 bg-white p-6 shadow-xl transition-[translate,visibility] duration-300 ease-out motion-reduce:transition-none md:hidden',
  open ? 'translate-x-0' : 'invisible translate-x-full',
)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Backdrop:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;className={cn(
  'fixed inset-0 z-40 bg-black/50 transition-opacity duration-300 motion-reduce:transition-none md:hidden',
  open ? 'opacity-100' : 'pointer-events-none opacity-0',
)}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;test-it-and-wrap-up&quot;&gt;Test it and wrap up&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Open the menu: the panel slides in and the backdrop fades.&lt;/li&gt;
&lt;li&gt;Click the backdrop: the menu closes.&lt;/li&gt;
&lt;li&gt;Open it again and press Escape: it closes.&lt;/li&gt;
&lt;li&gt;Open it and click a link: it navigates and closes.&lt;/li&gt;
&lt;li&gt;Close the menu and press Tab: focus skips the hidden links.&lt;/li&gt;
&lt;li&gt;Open the menu and try to scroll the page: it stays put.&lt;/li&gt;
&lt;li&gt;Turn on reduced motion in your system settings: the menu appears with no animation.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let's recap. We always render the panel and move it with &lt;code&gt;translate&lt;/code&gt;. The backdrop fades and closes the menu. &lt;code&gt;transition-[translate,visibility]&lt;/code&gt; keeps the slide-out visible, then hides the panel from keyboards and screen readers. We locked the page scroll, and we respect reduced motion. All of it is plain Tailwind classes and one small effect.&lt;/p&gt;
&lt;h3 id=&quot;good-to-know-and-what-to-try-next&quot;&gt;Good to know and what to try next&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;A gotcha:&lt;/strong&gt; a &lt;code&gt;fixed&lt;/code&gt; element is positioned against the viewport unless an ancestor has a &lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt; or &lt;code&gt;backdrop-filter&lt;/code&gt;. If your header uses &lt;code&gt;backdrop-blur&lt;/code&gt;, move the backdrop and panel outside the &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt; element or the drawer will be sized to the header instead of the screen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tell screen readers the menu state:&lt;/strong&gt; the hamburger button from the previous part should announce whether the menu is open. Add &lt;code&gt;aria-expanded&lt;/code&gt; and &lt;code&gt;aria-controls&lt;/code&gt; (the &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; already has &lt;code&gt;id=&quot;mobile-menu&quot;&lt;/code&gt;), and keep the label constant, because the state comes from &lt;code&gt;aria-expanded&lt;/code&gt; and a label that flips between &quot;Open&quot; and &quot;Close&quot; can make it get announced twice. If your button already has a label like &quot;Open menu&quot;, change it to a fixed one:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;button
  type=&quot;button&quot;
  onClick={() =&amp;gt; setOpen((o) =&amp;gt; !o)}
  aria-expanded={open}
  aria-controls=&quot;mobile-menu&quot;
  aria-label=&quot;Menu&quot;
&amp;gt;
  {/* your hamburger icon */}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Try next:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Close the menu automatically when the window grows past the &lt;code&gt;md&lt;/code&gt; breakpoint, so the scroll lock can never be left on.&lt;/li&gt;
&lt;li&gt;Move focus to the Close button when the menu opens, and back to the hamburger when it closes.&lt;/li&gt;
&lt;li&gt;Swap the slide for a left-side drawer by changing &lt;code&gt;right-0&lt;/code&gt; to &lt;code&gt;left-0&lt;/code&gt; and &lt;code&gt;translate-x-full&lt;/code&gt; to &lt;code&gt;-translate-x-full&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next in the series: error handling and a proper not-found page.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31644/build-a-slide-in-mobile-menu-in-tanstack-start-with-tailwind-v4</guid>
<pubDate>Sun, 18 Oct 2026 18:49:44 +0000</pubDate>
</item>
<item>
<title>Fix: useSearchParams() Should Be Wrapped in a Suspense Boundary (and Why It Only Breaks at Build</title>
<link>https://coderlegion.com/31610/fix-usesearchparams-should-be-wrapped-in-suspense-boundary-and-why-it-only-breaks-at-build</link>
<description>&lt;p&gt;You add a small search box to your navbar. It reads the query string, so you reach for &lt;code&gt;useSearchParams()&lt;/code&gt;. Everything works in &lt;code&gt;next dev&lt;/code&gt;. You push, the deploy fails, and the build log says:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useSearchParams() should be wrapped in a suspense boundary at page &quot;/about&quot;.
Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the page name. You did not touch &lt;code&gt;/about&lt;/code&gt;. That detail is the whole story.&lt;/p&gt;
&lt;h3 id=&quot;the-code-that-triggers-it&quot;&gt;The Code That Triggers It&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// components/SearchBox.tsx
'use client';

import { useSearchParams } from 'next/navigation';

export function SearchBox() {
  const searchParams = useSearchParams();
  const query = searchParams.get('q') ?? '';

  return &amp;lt;input defaultValue={query} placeholder=&quot;Search...&quot; /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/layout.tsx
import { SearchBox } from '@/components/SearchBox';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    &amp;lt;html lang=&quot;en&quot;&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;nav&amp;gt;
          &amp;lt;SearchBox /&amp;gt;
        &amp;lt;/nav&amp;gt;
        {children}
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing here looks wrong, and &lt;code&gt;next dev&lt;/code&gt; agrees with you.&lt;/p&gt;
&lt;h3 id=&quot;why-only-the-build-complains&quot;&gt;Why Only the Build Complains&lt;/h3&gt;
&lt;p&gt;At build time, Next.js tries to pre-render every page that does not obviously need request data into static HTML. That is the fast path, and it is the default.&lt;/p&gt;
&lt;p&gt;But a static page is built once, before any visitor exists, so it has no query string. &lt;code&gt;useSearchParams()&lt;/code&gt; needs one. Next.js handles this by &lt;strong&gt;bailing out of static rendering for that part of the tree&lt;/strong&gt;: the component is skipped on the server and rendered in the browser instead.&lt;/p&gt;
&lt;p&gt;The catch is how far the bail-out spreads. Without a &lt;code&gt;Suspense&lt;/code&gt; boundary around the component, the nearest boundary is the page itself, which would turn the whole page into client-side rendering and quietly throw away the static HTML you were about to ship. Instead of letting that happen silently, the build fails and asks you to say where the boundary should be.&lt;/p&gt;
&lt;p&gt;Dev mode renders every page on demand, so it never has to make that decision, which is why the error only shows up when you build.&lt;/p&gt;
&lt;h3 id=&quot;why-it-hits-every-page-at-once&quot;&gt;Why It Hits Every Page at Once&lt;/h3&gt;
&lt;p&gt;I put the component in &lt;code&gt;layout.tsx&lt;/code&gt; on purpose. A shared layout wraps every page, so one unguarded &lt;code&gt;useSearchParams()&lt;/code&gt; in a navbar breaks the build for &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;/about&lt;/code&gt;, &lt;code&gt;/pricing&lt;/code&gt; and every other static route at the same time.&lt;/p&gt;
&lt;p&gt;That is also why the error names a page you never edited. The problem lives in a shared component.&lt;/p&gt;
&lt;h3 id=&quot;fix-1-wrap-the-component-in-suspense&quot;&gt;Fix 1: Wrap the Component in Suspense&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/layout.tsx
import { Suspense } from 'react';
import { SearchBox } from '@/components/SearchBox';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    &amp;lt;html lang=&quot;en&quot;&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;nav&amp;gt;
          &amp;lt;Suspense fallback={&amp;lt;input disabled placeholder=&quot;Search...&quot; /&amp;gt;}&amp;gt;
            &amp;lt;SearchBox /&amp;gt;
          &amp;lt;/Suspense&amp;gt;
        &amp;lt;/nav&amp;gt;
        {children}
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now only the search box is rendered in the browser. Everything else stays static HTML. The fallback is what visitors see until the client takes over, so make it the same size as the real component to avoid the layout jumping when it swaps in.&lt;/p&gt;
&lt;p&gt;This is the fix to reach for first. It keeps your pages static and limits the client-side part to the one component that needs it.&lt;/p&gt;
&lt;h3 id=&quot;fix-2-read-the-params-on-the-server-and-pass-them-down&quot;&gt;Fix 2: Read the Params on the Server and Pass Them Down&lt;/h3&gt;
&lt;p&gt;If the value is only needed on one page, skip the hook entirely. Pages receive &lt;code&gt;searchParams&lt;/code&gt; as a prop:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/search/page.tsx
export default async function SearchPage({
  searchParams,
}: {
  searchParams: Promise&amp;lt;{ q?: string }&amp;gt;;
}) {
  const { q } = await searchParams;
  return &amp;lt;SearchResults query={q ?? ''} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Next.js 15, &lt;code&gt;searchParams&lt;/code&gt; is a Promise, so it has to be awaited. Reading it marks that page as dynamic, so it renders per request instead of at build time. For a real search results page that is exactly what you want.&lt;/p&gt;
&lt;h3 id=&quot;fix-3-force-the-route-to-be-dynamic&quot;&gt;Fix 3: Force the Route to Be Dynamic&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export const dynamic = 'force-dynamic';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This also makes the error disappear, but it does so by giving up static rendering for the route. Use it when the page genuinely depends on the request, not as a way to silence the build. On a marketing page it trades speed for nothing.&lt;/p&gt;
&lt;h3 id=&quot;which-fix-when&quot;&gt;Which Fix When&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Situation &lt;/th&gt;&lt;th&gt;Fix&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Small interactive component reads the URL (navbar search, filters, tabs) &lt;/td&gt;&lt;td&gt;Suspense around that component&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;An entire page depends on the query string &lt;/td&gt;&lt;td&gt;Read &lt;code&gt;searchParams&lt;/code&gt; in the page&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;The page really is per-request &lt;/td&gt;&lt;td&gt;&lt;code&gt;force-dynamic&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;a-detail-worth-knowing-about-the-fallback&quot;&gt;A Detail Worth Knowing About the Fallback&lt;/h3&gt;
&lt;p&gt;Whatever sits inside the Suspense boundary is client-rendered, so it will not be in the initial HTML. For a search input that does not matter. For content you want indexed, keep it outside the boundary. A fallback that does not match the final size also adds layout shift, which counts against your Core Web Vitals. I cover what those metrics measure in &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com/blog/core-web-vitals-explained&quot;&gt;this plain-language guide&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-rule-3&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;A client component that reads request-time data needs a Suspense boundary, so Next.js knows how much of the page to make dynamic.&lt;/strong&gt; Put the boundary as close to the component as you can, and the rest of the page stays static.&lt;/p&gt;
&lt;p&gt;I wrap every URL-driven component this way across my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;Next.js templates&lt;/a&gt;, so the production build never depends on where a navbar or filter bar ends up in the layout.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Search your project for &lt;code&gt;useSearchParams&lt;/code&gt; and check that every use sits inside a &lt;code&gt;Suspense&lt;/code&gt; boundary, especially anything imported by &lt;code&gt;layout.tsx&lt;/code&gt;. If you have hit a different build-only error, drop it in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31610/fix-usesearchparams-should-be-wrapped-in-suspense-boundary-and-why-it-only-breaks-at-build</guid>
<pubDate>Sun, 18 Oct 2026 12:23:34 +0000</pubDate>
</item>
<item>
<title>Your Server Action Error Messages Vanish in Production, and Throwing Is the Wrong Tool</title>
<link>https://coderlegion.com/31506/your-server-action-error-messages-vanish-in-production-and-throwing-is-the-wrong-tool</link>
<description>&lt;p&gt;Here is a bug report you will eventually get: &quot;The signup form told me what was wrong on your machine, but on the live site it just says something went wrong.&quot;&lt;/p&gt;
&lt;p&gt;Nothing is broken. Next.js is doing this on purpose. But the code that triggers it looks completely normal, and it is in a lot of Server Actions tutorials, including a version in my own earlier post on replacing API routes.&lt;/p&gt;
&lt;h3 id=&quot;the-code-that-works-locally&quot;&gt;The Code That Works Locally&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// actions/signup.ts
'use server';

export async function signup(formData: FormData) {
  const email = formData.get('email') as string;

  const existing = await User.findOne({ email });
  if (existing) {
    throw new Error('That email is already registered');
  }

  await User.create({ email });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// components/SignupForm.tsx
'use client';

export function SignupForm() {
  const [error, setError] = useState('');

  async function handleSubmit(formData: FormData) {
    try {
      await signup(formData);
    } catch (e) {
      setError((e as Error).message);
    }
  }

  return (
    &amp;lt;form action={handleSubmit}&amp;gt;
      &amp;lt;input name=&quot;email&quot; type=&quot;email&quot; /&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;Sign up&amp;lt;/button&amp;gt;
      {error &amp;amp;&amp;amp; &amp;lt;p&amp;gt;{error}&amp;lt;/p&amp;gt;}
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In development, a duplicate email shows &quot;That email is already registered&quot;. After &lt;code&gt;next build&lt;/code&gt; and &lt;code&gt;next start&lt;/code&gt;, or on Vercel, the same submission shows a generic message about an error occurring on the server, with a short digest string instead of your text.&lt;/p&gt;
&lt;h3 id=&quot;why-next-js-hides-it&quot;&gt;Why Next.js Hides It&lt;/h3&gt;
&lt;p&gt;An error thrown on the server can carry anything: a database connection string inside a driver message, a file path, a query, a stack trace. If Next.js forwarded every thrown message to the browser, a single unexpected failure could leak internal details to anyone watching the network tab.&lt;/p&gt;
&lt;p&gt;So in production builds, Next.js replaces the message of a thrown error with a generic one and attaches a &lt;strong&gt;digest&lt;/strong&gt;, a short hash you can search for in your server logs. In development it shows you the real message because you are the only one looking.&lt;/p&gt;
&lt;p&gt;That is the correct default. The mistake is using &lt;code&gt;throw&lt;/code&gt; for something that is not an unexpected failure.&lt;/p&gt;
&lt;h3 id=&quot;two-kinds-of-errors-two-different-tools&quot;&gt;Two Kinds of Errors, Two Different Tools&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Expected errors&lt;/strong&gt; are things that will happen during normal use: a duplicate email, a wrong password, a failed validation, a card that was declined. The user should see a specific message and fix it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unexpected errors&lt;/strong&gt; are things that should not happen: the database is down, a bug, a null that should not be null. The user should see a safe generic message and you should see the details in your logs.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;throw&lt;/code&gt; is the right tool for the second kind. It is the wrong tool for the first, because the framework treats every thrown error as potentially sensitive and strips it.&lt;/p&gt;
&lt;h3 id=&quot;the-fix-return-expected-errors-as-data&quot;&gt;The Fix: Return Expected Errors as Data&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// actions/signup.ts
'use server';

import { z } from 'zod';

const SignupSchema = z.object({
  email: z.string().email('Enter a valid email'),
});

export type SignupState = {
  success: boolean;
  message?: string;
  fieldErrors?: Record&amp;lt;string, string[]&amp;gt;;
};

export async function signup(
  prevState: SignupState | null,
  formData: FormData
): Promise&amp;lt;SignupState&amp;gt; {
  const parsed = SignupSchema.safeParse({
    email: formData.get('email'),
  });

  if (!parsed.success) {
    return {
      success: false,
      fieldErrors: parsed.error.flatten().fieldErrors,
    };
  }

  const existing = await User.findOne({ email: parsed.data.email });
  if (existing) {
    return { success: false, message: 'That email is already registered' };
  }

  await User.create({ email: parsed.data.email });
  return { success: true, message: 'Account created' };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// components/SignupForm.tsx
'use client';

import { useActionState } from 'react';
import { signup } from '@/actions/signup';

export function SignupForm() {
  const [state, action, isPending] = useActionState(signup, null);

  return (
    &amp;lt;form action={action}&amp;gt;
      &amp;lt;input name=&quot;email&quot; type=&quot;email&quot; /&amp;gt;
      {state?.fieldErrors?.email &amp;amp;&amp;amp; &amp;lt;p&amp;gt;{state.fieldErrors.email[0]}&amp;lt;/p&amp;gt;}
      &amp;lt;button type=&quot;submit&quot; disabled={isPending}&amp;gt;
        {isPending ? 'Creating account...' : 'Sign up'}
      &amp;lt;/button&amp;gt;
      {state?.message &amp;amp;&amp;amp; &amp;lt;p&amp;gt;{state.message}&amp;lt;/p&amp;gt;}
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A returned value is just data. Next.js does not treat it as sensitive, so the exact same message reaches the user in development and in production. You also get &lt;code&gt;useActionState&lt;/code&gt; handling pending state and the previous result for free, with no &lt;code&gt;try/catch&lt;/code&gt; in the component.&lt;/p&gt;
&lt;h3 id=&quot;keep-throw-for-the-real-failures&quot;&gt;Keep &lt;code&gt;throw&lt;/code&gt; for the Real Failures&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export async function signup(prevState, formData) {
  // ...expected errors returned as data above...

  try {
    await User.create({ email: parsed.data.email });
  } catch (error) {
    console.error('signup failed', error); // full details stay in your logs
    throw error; // user gets the safe generic message
  }

  return { success: true, message: 'Account created' };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When this throws in production, the user sees the generic message and an error boundary can take over. You search your logs for the digest and find the real error. Nothing sensitive reaches the browser.&lt;/p&gt;
&lt;h3 id=&quot;why-this-survives-testing&quot;&gt;Why This Survives Testing&lt;/h3&gt;
&lt;p&gt;Every manual test happens in &lt;code&gt;next dev&lt;/code&gt;, where thrown messages are shown in full. The code looks perfect right up until the first production build, which is usually when real users start signing up.&lt;/p&gt;
&lt;h3 id=&quot;the-rule-4&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Return expected errors as data, throw only for unexpected failures.&lt;/strong&gt; If a message is meant for the user to read, it should travel as a returned value. If it is meant for you to debug, it should go to your logs and surface to the user as something generic.&lt;/p&gt;
&lt;p&gt;I use this return-a-state pattern in the forms and dashboard actions across my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;Next.js templates&lt;/a&gt;, so validation messages behave identically in development and on the live site.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Search your own codebase for &lt;code&gt;throw new Error&lt;/code&gt; inside files marked &lt;code&gt;'use server'&lt;/code&gt;. For each one, ask whether a user is supposed to read that message. If yes, it is probably already invisible in production. Drop what you find in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31506/your-server-action-error-messages-vanish-in-production-and-throwing-is-the-wrong-tool</guid>
<pubDate>Sat, 17 Oct 2026 09:48:36 +0000</pubDate>
</item>
<item>
<title>Responsive Navigation in TanStack Start: Hamburger Menu and Desktop Navbar</title>
<link>https://coderlegion.com/31251/responsive-navigation-in-tanstack-start-hamburger-menu-and-desktop-navbar</link>
<description>&lt;p&gt;A header that works on a phone and on a desktop is one of the first things every site needs. In this part of the TanStack Start Basics series, we'll build one with inline links on wide screens, a hamburger menu on small ones, and a few accessibility details that are easy to skip.&lt;/p&gt;
&lt;p&gt;You can watch the video here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=_-0DDuQseSo&quot;&gt;Responsive Navigation&lt;/a&gt;. Everything below is the written version.&lt;/p&gt;
&lt;h4 id=&quot;write-the-links-once&quot;&gt;Write the links once&lt;/h4&gt;
&lt;p&gt;Start with a single list of links that both menus will read from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/lib/nav.ts
export const navLinks = [
  { to: '/', label: 'Home' },
  { to: '/products', label: 'Products' },
  { to: '/about', label: 'About' },
  { to: '/contact', label: 'Contact' },
] as const;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;as const&lt;/code&gt; lets TypeScript treat every &lt;code&gt;to&lt;/code&gt; as an exact route, so a typo is a compile error and not a broken link.&lt;/p&gt;
&lt;h4 id=&quot;think-mobile-first&quot;&gt;Think mobile first&lt;/h4&gt;
&lt;p&gt;Tailwind is mobile first. Classes without a prefix apply to every screen size, and a prefix such as &lt;code&gt;md:&lt;/code&gt; applies from that width upward. So the plan is to style the phone version first, and then add the desktop changes on top:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hidden md:block&lt;/code&gt; is hidden on phones and shown from 768px up (the desktop links)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;md:hidden&lt;/code&gt; is shown on phones and hidden from 768px up (the menu button and the mobile menu)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;a-separate-navlinks-component&quot;&gt;A separate NavLinks component&lt;/h4&gt;
&lt;p&gt;Both menus need the same list of links, so it gets its own component. Start with the props type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/types/index.ts
export type NavLinksProps = {
  listClass: string;
  linkClass: string;
  activeClass?: string;
  onNavigate?: () =&amp;gt; void;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the component:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/components/NavLinks.tsx
import { Link } from '@tanstack/react-router';
import { navLinks } from '@/lib/nav';
import type { NavLinksProps } from '@/types';

export default function NavLinks({
  listClass,
  linkClass,
  activeClass = 'text-black underline underline-offset-4',
  onNavigate,
}: NavLinksProps) {
  return (
    &amp;lt;ul className={listClass}&amp;gt;
      {navLinks.map((link) =&amp;gt; (
        &amp;lt;li key={link.to}&amp;gt;
          &amp;lt;Link
            to={link.to}
            activeOptions={{ exact: link.to === '/' }}
            className={linkClass}
            activeProps={{ className: activeClass }}
            onClick={onNavigate}
          &amp;gt;
            {link.label}
          &amp;lt;/Link&amp;gt;
        &amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The two class props let each menu style the list its own way. &lt;code&gt;activeClass&lt;/code&gt; has a default, so most callers can leave it out, and &lt;code&gt;onNavigate&lt;/code&gt; is optional, so the desktop menu doesn't need it. Because the component lives in its own file, you can reuse it later, in a footer for example, without copying anything.&lt;/p&gt;
&lt;h4 id=&quot;the-header&quot;&gt;The header&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/components/Header.tsx
import { useEffect, useState } from 'react';
import { Link } from '@tanstack/react-router';
import { Menu, X } from 'lucide-react';
import NavLinks from '@/components/NavLinks';

export default function Header() {
  const [open, setOpen] = useState(false);

  useEffect(() =&amp;gt; {
    if (!open) return;
    const onKeyDown = (e: KeyboardEvent) =&amp;gt; {
      if (e.key === 'Escape') setOpen(false);
    };
    window.addEventListener('keydown', onKeyDown);
    return () =&amp;gt; window.removeEventListener('keydown', onKeyDown);
  }, [open]);

  return (
    &amp;lt;header className=&quot;border-b bg-white&quot;&amp;gt;
      &amp;lt;div className=&quot;mx-auto flex h-16 max-w-6xl items-center justify-between px-6&quot;&amp;gt;
        &amp;lt;Link to=&quot;/&quot; className=&quot;text-lg font-bold&quot;&amp;gt;
          My Store
        &amp;lt;/Link&amp;gt;

        &amp;lt;nav aria-label=&quot;Main&quot; className=&quot;hidden md:block&quot;&amp;gt;
          &amp;lt;NavLinks
            listClass=&quot;flex items-center gap-6&quot;
            linkClass=&quot;text-sm font-medium text-neutral-600 hover:text-black&quot;
          /&amp;gt;
        &amp;lt;/nav&amp;gt;

        &amp;lt;button
          type=&quot;button&quot;
          onClick={() =&amp;gt; setOpen((o) =&amp;gt; !o)}
          aria-label={open ? 'Close menu' : 'Open menu'}
          aria-expanded={open}
          aria-controls=&quot;mobile-menu&quot;
          className=&quot;flex size-11 items-center justify-center rounded-md md:hidden&quot;
        &amp;gt;
          {open ? &amp;lt;X className=&quot;size-6&quot; /&amp;gt; : &amp;lt;Menu className=&quot;size-6&quot; /&amp;gt;}
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;

      {open &amp;amp;&amp;amp; (
        &amp;lt;nav
          id=&quot;mobile-menu&quot;
          aria-label=&quot;Mobile&quot;
          className=&quot;border-t md:hidden&quot;
        &amp;gt;
          &amp;lt;NavLinks
            listClass=&quot;flex flex-col px-6 py-2&quot;
            linkClass=&quot;block py-3 text-base font-medium text-neutral-600 hover:text-black&quot;
            onNavigate={() =&amp;gt; setOpen(false)}
          /&amp;gt;
        &amp;lt;/nav&amp;gt;
      )}
    &amp;lt;/header&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;closing-the-menu-when-you-navigate&quot;&gt;Closing the menu when you navigate&lt;/h4&gt;
&lt;p&gt;If you skip this, you'll notice a bug: you tap a link, the page changes, and the menu stays open. The header lives in a layout route, and a layout stays mounted while the pages inside it change, so its state is never reset.&lt;/p&gt;
&lt;p&gt;The fix is to close the menu in the link's click handler, which is where the user's action happens. React's documentation recommends handling things caused by a user action in the event handler for that action, and not in an effect. So &lt;code&gt;NavLinks&lt;/code&gt; gets an optional &lt;code&gt;onNavigate&lt;/code&gt; prop, and only the mobile menu passes &lt;code&gt;() =&amp;gt; setOpen(false)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This also closes the menu when you tap the link for the page you're already on, which an effect that watches the path would miss. If you also want the menu to close when the user presses the browser's back button, you can add an effect that watches the path on top of this, but the click handler covers the main case.&lt;/p&gt;
&lt;h4 id=&quot;active-links&quot;&gt;Active links&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Link&lt;/code&gt; knows when it points at the current page, and &lt;code&gt;activeProps&lt;/code&gt; lets you style that state. Every path starts with a slash, so the home link would look active everywhere. &lt;code&gt;activeOptions={{ exact: true }}&lt;/code&gt; fixes that, and the code above applies it only to the home link.&lt;/p&gt;
&lt;h4 id=&quot;accessibility-basics&quot;&gt;Accessibility basics&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Use a real &lt;code&gt;button&lt;/code&gt; for the toggle, with &lt;code&gt;type=&quot;button&quot;&lt;/code&gt;, so the keyboard can reach it&lt;/li&gt;
&lt;li&gt;Label it with &lt;code&gt;aria-label&lt;/code&gt;, and update the label with the state&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;aria-expanded&lt;/code&gt; and &lt;code&gt;aria-controls&lt;/code&gt; so assistive technology knows whether the menu is open&lt;/li&gt;
&lt;li&gt;Close the menu with the Escape key&lt;/li&gt;
&lt;li&gt;Give each &lt;code&gt;nav&lt;/code&gt; its own label, so a screen reader user can tell them apart&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;use-it-in-the-layout&quot;&gt;Use it in the layout&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/routes/_appLayout.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router';
import Header from '@/components/Header';

export const Route = createFileRoute('/_appLayout')({
  component: AppLayout,
});

function AppLayout() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Header /&amp;gt;
      &amp;lt;main className=&quot;mx-auto max-w-6xl px-6 py-8&quot;&amp;gt;
        &amp;lt;Outlet /&amp;gt;
      &amp;lt;/main&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;try-it&quot;&gt;Try it&lt;/h4&gt;
&lt;p&gt;Open your browser's device toolbar and drag the width between 375px and 1024px. The hamburger should appear below 768px, and the inline links above it. Tap a link in the mobile menu and check that it closes.&lt;/p&gt;
&lt;h4 id=&quot;what-s-next&quot;&gt;What's next&lt;/h4&gt;
&lt;p&gt;In the next part, we'll look at slide-in animation and a dark mode toggle. If you got stuck anywhere, leave a comment on the video.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31251/responsive-navigation-in-tanstack-start-hamburger-menu-and-desktop-navbar</guid>
<pubDate>Fri, 16 Oct 2026 02:14:20 +0000</pubDate>
</item>
<item>
<title>Why Your Tailwind Classes Work in Development and Silently Vanish in Production</title>
<link>https://coderlegion.com/31201/why-your-tailwind-classes-work-in-development-and-silently-vanish-in-production</link>
<description>&lt;p&gt;Almost every Tailwind developer hits this exact bug once, usually right after a deploy, and the confusing part is that nothing about it looks wrong in the code, nothing throws an error, and it worked completely fine five minutes earlier on the same machine.&lt;/p&gt;
&lt;h3 id=&quot;the-code-that-looks-completely-reasonable&quot;&gt;The Code That Looks Completely Reasonable&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function StatusBadge({ status }: { status: 'success' | 'warning' | 'error' }) {
  const color = status === 'success' ? 'green' : status === 'warning' ? 'yellow' : 'red';

  return (
    &amp;lt;span className={`bg-${color}-100 text-${color}-800 px-2 py-1 rounded`}&amp;gt;
      {status}
    &amp;lt;/span&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In development, this renders exactly as expected, colored badges for each status, built dynamically from a single reusable component instead of three separate hardcoded ones. Then you deploy, and every badge renders as a plain, unstyled span. No color, no padding in some setups, nothing, depending on exactly what else got purged alongside it.&lt;/p&gt;
&lt;h3 id=&quot;why-this-happens-tailwind-doesn-t-actually-execute-your-code&quot;&gt;Why This Happens: Tailwind Doesn't Actually Execute Your Code&lt;/h3&gt;
&lt;p&gt;Tailwind doesn't run your JavaScript to figure out which classes you're using. It scans your source files as plain text, looking for strings that match known utility class patterns, and only generates CSS for the classes it actually finds written out, literally, somewhere in that scanned text.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;className={`bg-${color}-100 text-${color}-800`}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tailwind's scanner sees the literal text &lt;code&gt;bg-${color}-100&lt;/code&gt;, not &lt;code&gt;bg-green-100&lt;/code&gt;, &lt;code&gt;bg-yellow-100&lt;/code&gt;, or &lt;code&gt;bg-red-100&lt;/code&gt;. Template literal interpolation happens at runtime, in the browser or on your server, long after Tailwind's build step has already finished scanning your source files and generating the final CSS. Since none of the three actual color classes your component needs ever appears as literal, complete text anywhere in your codebase, Tailwind never generates CSS for any of them, and the class names that do get applied to the DOM at runtime simply don't exist in your stylesheet at all.&lt;/p&gt;
&lt;h3 id=&quot;why-this-specifically-survives-local-development&quot;&gt;Why This Specifically Survives Local Development&lt;/h3&gt;
&lt;p&gt;This is the part that makes the bug genuinely sneaky rather than just a misunderstanding: in development mode, especially with certain dev server configurations or cached builds from before the dynamic class was introduced, you can sometimes see the correct styling anyway, either because an earlier full build still has the class generated from some other literal usage elsewhere in the project, or because of how some dev tooling handles incremental rebuilds. The failure shows up specifically in a clean production build, which scans and purges from scratch, with nothing left over from earlier development sessions to accidentally mask the problem.&lt;/p&gt;
&lt;h3 id=&quot;the-fix-always-write-complete-class-names-literally&quot;&gt;The Fix: Always Write Complete Class Names, Literally&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function StatusBadge({ status }: { status: 'success' | 'warning' | 'error' }) {
  const styles = {
    success: 'bg-green-100 text-green-800',
    warning: 'bg-yellow-100 text-yellow-800',
    error: 'bg-red-100 text-red-800',
  };

  return (
    &amp;lt;span className={`${styles[status]} px-2 py-1 rounded`}&amp;gt;
      {status}
    &amp;lt;/span&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every complete, literal class name Tailwind needs now appears as actual text somewhere in the source file, &lt;code&gt;bg-green-100&lt;/code&gt;, &lt;code&gt;bg-yellow-100&lt;/code&gt;, &lt;code&gt;bg-red-100&lt;/code&gt;, &lt;code&gt;text-green-800&lt;/code&gt;, and so on, even though the specific one that gets applied is still chosen dynamically at runtime based on &lt;code&gt;status&lt;/code&gt;. Tailwind's scanner finds all three as literal strings during the build, generates CSS for all three, and the correct one gets applied correctly regardless of which branch of the mapping object actually executes.&lt;/p&gt;
&lt;h3 id=&quot;the-rule-that-generalizes-beyond-this-one-example&quot;&gt;The Rule That Generalizes Beyond This One Example&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Tailwind only generates CSS for class names that exist as complete, literal text somewhere in your scanned source files, string concatenation and template literal interpolation don't count, no matter how confident the resulting string looks at runtime.&lt;/strong&gt; This applies to any dynamic styling pattern, not just color variants, conditionally built spacing classes, responsive variants built from a variable, state-based classes constructed from a prop, all of them need the complete literal class name written out somewhere Tailwind's scanner can see it, usually in a lookup object exactly like the one above, rather than assembled through string interpolation.&lt;/p&gt;
&lt;p&gt;I keep every dynamic variant mapped out explicitly like this across the component libraries in my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;Next.js and Tailwind templates&lt;/a&gt;, specifically because this exact bug is so easy to reintroduce months later when someone, often future-you, refactors a component back toward the more compact-looking interpolated version without remembering why it was written the longer way.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Go check your own codebase for &lt;code&gt;className={&lt;/code&gt;...${variable}...&lt;code&gt;}&lt;/code&gt; patterns building Tailwind classes dynamically. If you find one, do a clean production build and actually look at it, don't trust how it renders in dev. Drop what you find in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31201/why-your-tailwind-classes-work-in-development-and-silently-vanish-in-production</guid>
<pubDate>Thu, 15 Oct 2026 12:05:37 +0000</pubDate>
</item>
<item>
<title>Your Next.js Page Might Be Caching One User's Data and Serving It to Everyone Else</title>
<link>https://coderlegion.com/31024/your-next-js-page-might-be-caching-one-users-data-and-serving-it-to-everyone-else</link>
<description>&lt;p&gt;This is the kind of bug that goes from &quot;works perfectly in every test&quot; to &quot;actively leaking one customer's data to another customer&quot; without a single code change in between, purely because of how caching and personalization interact in a way that isn't obvious from reading the component code.&lt;/p&gt;
&lt;h3 id=&quot;the-setup&quot;&gt;The Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/dashboard/page.tsx
export default async function DashboardPage() {
  const session = await getServerSession();
  const user = await User.findById(session.userId);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Welcome back, {user.name}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;Account balance: {user.balance}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This looks obviously personalized, it reads the session, looks up the specific user, renders their specific data. There's nothing here that looks like it needs special caching considerations. That assumption is exactly where this goes wrong.&lt;/p&gt;
&lt;h3 id=&quot;why-next-js-can-get-this-wrong&quot;&gt;Why Next.js Can Get This Wrong&lt;/h3&gt;
&lt;p&gt;Next.js's App Router tries to statically render as much as possible by default, for good performance reasons. A page gets treated as static (cacheable, shared across all visitors) unless something in it signals that it needs to be dynamic (rendered fresh per-request). Specific APIs, reading &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;, or &lt;code&gt;searchParams&lt;/code&gt; directly, are what tell Next.js a page actually depends on request-specific data and needs dynamic rendering.&lt;/p&gt;
&lt;p&gt;The problem is that &lt;code&gt;getServerSession()&lt;/code&gt; in the example above may, depending on exactly how it's implemented and which auth library version you're on, read the session cookie in a way that doesn't always propagate that dynamic signal up to the page itself, especially if the cookie read happens inside a library function several layers removed from the page component, rather than a direct &lt;code&gt;cookies()&lt;/code&gt; call visible in the page's own code. When that signal doesn't propagate correctly, Next.js can determine the page is eligible for static optimization, render it once during the build or on first request, and then serve that exact same rendered HTML, including the first user's name and balance, to every subsequent visitor who hits that route, until the cache naturally expires or gets invalidated.&lt;/p&gt;
&lt;h3 id=&quot;why-this-is-specifically-dangerous-not-just-buggy&quot;&gt;Why This Is Specifically Dangerous, Not Just Buggy&lt;/h3&gt;
&lt;p&gt;A caching bug that shows stale content is annoying. A caching bug that shows a different user's personal data is a real data exposure incident, and the specific danger here is that it doesn't look like a caching bug from the outside, it looks like the application is broken for that specific user, showing someone else's name and balance, which can easily get reported and triaged as &quot;a display bug&quot; rather than correctly identified as a serious caching and session isolation issue.&lt;/p&gt;
&lt;h3 id=&quot;why-this-survives-normal-testing&quot;&gt;Why This Survives Normal Testing&lt;/h3&gt;
&lt;p&gt;Testing a dashboard page almost always means logging in as a specific test user and confirming their own correct data renders. That test passes regardless of whether the response got cached and shared or genuinely rendered fresh for that specific request, because from a single test account's perspective, the result looks identical either way. The bug only becomes visible with two separate user sessions hitting the same route in a way that reveals the cached response is actually shared, which isn't a scenario most manual or automated testing naturally covers.&lt;/p&gt;
&lt;h3 id=&quot;the-fix-force-dynamic-rendering-explicitly-for-anything-personalized&quot;&gt;The Fix: Force Dynamic Rendering Explicitly for Anything Personalized&lt;/h3&gt;
&lt;p&gt;Don't rely on an auth helper's internal implementation to correctly signal dynamic rendering. State it explicitly for any route serving per-user data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';

export default async function DashboardPage() {
  const session = await getServerSession();
  const user = await User.findById(session.userId);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Welcome back, {user.name}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;Account balance: {user.balance}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;export const dynamic = 'force-dynamic'&lt;/code&gt; line removes any ambiguity about whether the page qualifies for static optimization. It costs the performance benefit of static rendering for that specific route, which is the correct tradeoff for anything genuinely personalized, that cost is real but small compared to the alternative.&lt;/p&gt;
&lt;p&gt;An equally valid alternative is calling &lt;code&gt;cookies()&lt;/code&gt; or &lt;code&gt;headers()&lt;/code&gt; directly inside the page itself, rather than only inside a wrapped auth helper function, since a direct call from the page component is what reliably signals dynamic rendering without depending on how a library internally implements its own session check.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { cookies } from 'next/headers';

export default async function DashboardPage() {
  cookies(); // explicit, direct call signals dynamic rendering regardless of what getServerSession does internally
  const session = await getServerSession();
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-rule-5&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Any page serving per-user or per-request data needs to explicitly and verifiably opt out of static rendering, not just assume an auth library's internals do that correctly on its behalf.&lt;/strong&gt; This matters more, not less, as auth libraries and caching layers evolve between versions, since a signal that correctly propagated in one version can silently stop doing so after an update you didn't closely audit.&lt;/p&gt;
&lt;p&gt;I treat every authenticated dashboard route as explicitly &lt;code&gt;force-dynamic&lt;/code&gt; by default in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://neurodash-dashbord.vercel.app/&quot;&gt;Next.js dashboard templates&lt;/a&gt; I build, specifically because the cost of guessing wrong here is a real user data leak, not just a stale page.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Go check your own authenticated pages for an explicit &lt;code&gt;dynamic&lt;/code&gt; export or a direct &lt;code&gt;cookies()&lt;/code&gt;/&lt;code&gt;headers()&lt;/code&gt; call, rather than relying on an auth helper to signal it correctly on your behalf. If you're not sure, this is worth verifying with two separate logged-in sessions today, not assuming. Drop what you find in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31024/your-next-js-page-might-be-caching-one-users-data-and-serving-it-to-everyone-else</guid>
<pubDate>Wed, 14 Oct 2026 07:51:52 +0000</pubDate>
</item>
<item>
<title>Isolate to Elevate: Why Component Testing in Isolation Saves Your Codebase</title>
<link>https://coderlegion.com/30819/isolate-to-elevate-why-component-testing-in-isolation-saves-your-codebase</link>
<description>&lt;h2 id=&quot;isolate-to-elevate-why-component-testing-in-isolation-saves-your-codebase&quot;&gt;Isolate to Elevate: Why Component Testing in Isolation Saves Your Codebase&lt;/h2&gt;
&lt;p&gt;Component testing in isolation is a software verification strategy where an individual piece of an application—such as a button, user profile card, or navigation bar—is evaluated entirely on its own. Instead of connecting the component to real databases, live servers, or neighboring UI elements, developers replace external dependencies with controlled, artificial placeholders called &lt;strong&gt;mocks&lt;/strong&gt; or &lt;strong&gt;stubs&lt;/strong&gt;. This guarantees that if a test fails, the issue is guaranteed to exist inside that specific component rather than in an external network or backend service.&lt;/p&gt;
&lt;h3 id=&quot;a-real-life-analogy-the-bench-tested-alternator&quot;&gt;A Real-Life Analogy: The Bench-Tested Alternator&lt;/h3&gt;
&lt;p&gt;Imagine a mechanic inspecting a newly manufactured car alternator. Instead of installing the alternator inside a fully assembled luxury vehicle, turning the key in the ignition, and hoping the dashboard lights illuminate, the mechanic places the alternator onto a specialized workbench.&lt;/p&gt;
&lt;p&gt;On this test bench, artificial wires power the alternator, and a standalone machine spins its rotor while measuring electrical voltage. If the meter shows an improper charge, the mechanic instantly knows the alternator itself is defective. There is zero ambiguity—the problem cannot be blamed on a dead car battery, a frayed ignition wire, or a blown fuse elsewhere in the vehicle. Component testing in isolation applies this exact bench-test philosophy to user interface elements.&lt;/p&gt;
&lt;h3 id=&quot;why-component-testing-in-isolation-matters-daily&quot;&gt;Why Component Testing in Isolation Matters Daily&lt;/h3&gt;
&lt;p&gt;In modern web development, frontend components frequently rely on complex API (Application Programming Interface) calls—the background communication channels that fetch data from databases over the internet. If you attempt to test a component while connected to live production or staging servers, your automated tests become fragile and unpredictable. A minor network hiccup or scheduled database maintenance will cause UI tests to fail, triggering false alarms and wasting valuable engineering hours.&lt;/p&gt;
&lt;p&gt;Testing components in isolation eliminates this instability. It yields significant daily benefits:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Lightning-Fast Execution:&lt;/strong&gt; Tests run in a local memory environment without waiting for HTTP network responses, executing hundreds of checks in milliseconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deterministic Feedback:&lt;/strong&gt; Tests yield identical, repeatable results regardless of backend availability or database state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effortless Edge-Case Coverage:&lt;/strong&gt; Developers can effortlessly simulate tricky scenarios, such as slow network connections, server crash codes (like 500 errors), or extremely long text strings, without needing to manipulate a real server.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;code-example-testing-a-react-component-in-isolation&quot;&gt;Code Example: Testing a React Component in Isolation&lt;/h3&gt;
&lt;p&gt;Below is a practical example using React (a popular JavaScript library for building user interfaces) and React Testing Library.&lt;/p&gt;
&lt;p&gt;First, here is our component, &lt;code&gt;UserProfile&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// UserProfile.jsx
import React from 'react';

export function UserProfile({ user, isLoading, error }) {
  if (isLoading) {
    return &amp;lt;div data-testid=&quot;loading&quot;&amp;gt;Loading user details...&amp;lt;/div&amp;gt;;
  }

  if (error) {
    return &amp;lt;div data-testid=&quot;error&quot;&amp;gt;Error loading user profile.&amp;lt;/div&amp;gt;;
  }

  return (
    &amp;lt;div className=&quot;user-card&quot;&amp;gt;
      &amp;lt;h2&amp;gt;{user.name}&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;Email: {user.email}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we test &lt;code&gt;UserProfile&lt;/code&gt; in total isolation. Notice how we pass fake JavaScript object data directly through &lt;strong&gt;props&lt;/strong&gt; (inputs passed to React components) without making real network requests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// UserProfile.test.jsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import { UserProfile } from './UserProfile';

test('renders user details correctly when data is provided', () =&amp;gt; {
  // 1. Arrange: Create mock user data
  const mockUser = { name: 'Sarah Connor', email: 'sarah@skynet.com' };

  // 2. Act: Render component in isolation
  render(&amp;lt;UserProfile user={mockUser} isLoading={false} error={false} /&amp;gt;);

  // 3. Assert: Verify the rendered output
  expect(screen.getByText('Sarah Connor')).toBeInTheDocument();
  expect(screen.getByText('Email: sarah@skynet.com')).toBeInTheDocument();
});

test('renders error message during failure state', () =&amp;gt; {
  render(&amp;lt;UserProfile user={null} isLoading={false} error={true} /&amp;gt;);

  expect(screen.getByTestId('error')).toHaveTextContent('Error loading user profile.');
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In these tests, the &lt;code&gt;render&lt;/code&gt; function mounts &lt;code&gt;UserProfile&lt;/code&gt; inside a simulated virtual browser. We control every input state directly, ensuring complete certainty over what we are testing.&lt;/p&gt;
&lt;h3 id=&quot;summary-takeaway&quot;&gt;Summary Takeaway&lt;/h3&gt;
&lt;p&gt;Building resilient digital experiences requires modular confidence. By isolating individual UI components during automated testing, engineering teams replace slow, flaky test pipelines with deterministic feedback loops that pinpoint bugs in milliseconds. Mastering this technique ensures that every piece of your user interface is proven battle-ready before it ever integrates into a larger software ecosystem.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/mastering-component-testing-in.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30819/isolate-to-elevate-why-component-testing-in-isolation-saves-your-codebase</guid>
<pubDate>Mon, 12 Oct 2026 13:46:09 +0000</pubDate>
</item>
<item>
<title>https://dev.to/senthil_kumar_b6b9dcdaa4e/stop-rewriting-browser-fingerprinting-logic-in-react-meet-u</title>
<link>https://coderlegion.com/30747/https-senthilkumarb6b9dcdaa4e-stop-rewriting-browser-fingerprinting-logic-react-meet</link>
<description>&lt;h2 id=&quot;stop-rewriting-browser-fingerprinting-logic-in-react-meet-usefingerprint&quot;&gt;Stop Rewriting Browser Fingerprinting Logic in React: Meet &lt;code&gt;useFingerprint&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Browser fingerprinting is an interesting technique for understanding whether multiple browser visits might come from the same browser environment. But if you're building React applications, implementing browser-signal collection and managing its lifecycle can quickly become repetitive.&lt;/p&gt;
&lt;p&gt;What if you could encapsulate that functionality in a reusable React hook and use it wherever you need it?&lt;/p&gt;
&lt;p&gt;That's the idea behind &lt;code&gt;useFingerprint&lt;/code&gt;, one of the browser-focused hooks in &lt;strong&gt;useThisHook&lt;/strong&gt;, an open-source collection of reusable React hooks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Explore the live demo:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://usethishook.mentorbridge.in/useFingerprint&quot;&gt;https://usethishook.mentorbridge.in/useFingerprint&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-is-browser-fingerprinting-&quot;&gt;What Is Browser Fingerprinting?&lt;/h3&gt;
&lt;p&gt;Every browser exposes information about its environment. Depending on browser capabilities and permissions, this may include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Browser and platform characteristics&lt;/li&gt;
&lt;li&gt;Screen dimensions and display properties&lt;/li&gt;
&lt;li&gt;Language and locale settings&lt;/li&gt;
&lt;li&gt;Time zone information&lt;/li&gt;
&lt;li&gt;Other browser-exposed signals&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Individually, these signals may not be particularly distinctive. Combined, they can contribute to a browser fingerprint.&lt;/p&gt;
&lt;p&gt;A browser fingerprint is a derived identifier based on characteristics of the browser environment. It can help applications assess whether visits may be associated with a similar environment.&lt;/p&gt;
&lt;p&gt;However, there is an important distinction:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A browser fingerprint is not a guaranteed, permanent identity for a person or device.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Different browsers on the same device may produce different fingerprints. Browser updates, privacy protections, configuration changes, and fingerprinting defenses can also affect the result.&lt;/p&gt;
&lt;h3 id=&quot;why-use-a-custom-react-hook-&quot;&gt;Why Use a Custom React Hook?&lt;/h3&gt;
&lt;p&gt;Imagine you're developing an application that needs to work with a browser fingerprint.&lt;/p&gt;
&lt;p&gt;Without a reusable abstraction, you might end up implementing the following concerns in individual components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Collecting the required browser signals&lt;/li&gt;
&lt;li&gt;Handling asynchronous initialization, where applicable&lt;/li&gt;
&lt;li&gt;Managing the resulting value&lt;/li&gt;
&lt;li&gt;Integrating browser-specific logic with React's component lifecycle&lt;/li&gt;
&lt;li&gt;Handling environments where browser APIs are unavailable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Repeating this logic across components increases maintenance costs and makes it harder to keep implementations consistent.&lt;/p&gt;
&lt;p&gt;React custom hooks offer a better way to organize reusable stateful behavior.&lt;/p&gt;
&lt;p&gt;Instead of coupling browser-related logic to a particular component, you encapsulate it behind a reusable function.&lt;/p&gt;
&lt;p&gt;That is where &lt;code&gt;useFingerprint&lt;/code&gt; comes in.&lt;/p&gt;
&lt;h3 id=&quot;introducing-usefingerprint-from-usethishook&quot;&gt;Introducing &lt;code&gt;useFingerprint&lt;/code&gt; from useThisHook&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useFingerprint&lt;/code&gt; is a browser-focused custom hook in &lt;a rel=&quot;nofollow&quot; href=&quot;https://usethishook.mentorbridge.in/&quot;&gt;useThisHook&lt;/a&gt;, a reusable React hooks library.&lt;/p&gt;
&lt;p&gt;The library follows a straightforward philosophy:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Stop rewriting the same React hooks in every project.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Rather than rebuilding common behavior for every application, developers can install the library and import the hooks they need.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;useFingerprint&lt;/code&gt; hook provides an abstraction for working with a client-side visitor identifier derived from browser signals.&lt;/p&gt;
&lt;p&gt;You can explore its interactive documentation here:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://usethishook.mentorbridge.in/useFingerprint&quot;&gt;Try useFingerprint&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;installation&quot;&gt;Installation&lt;/h3&gt;
&lt;p&gt;If you haven't installed useThisHook yet, add it to your React project.&lt;/p&gt;
&lt;p&gt;Using npm:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install usethishook
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using pnpm:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pnpm add usethishook
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using Yarn:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yarn add usethishook
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;using-usefingerprint-in-a-react-component&quot;&gt;Using &lt;code&gt;useFingerprint&lt;/code&gt; in a React Component&lt;/h3&gt;
&lt;p&gt;Once the package is installed, import the hook from the library.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { useFingerprint } from 'usethishook';

function FingerprintExample() {
  const fingerprint = useFingerprint();

  return (
    &amp;lt;section&amp;gt;
      &amp;lt;h2&amp;gt;Browser Fingerprint&amp;lt;/h2&amp;gt;
      &amp;lt;pre&amp;gt;{String(fingerprint ?? '')}&amp;lt;/pre&amp;gt;
    &amp;lt;/section&amp;gt;
  );
}

export default FingerprintExample;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example illustrates the intended integration pattern: call the hook inside a React component and render the resulting value.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Verify the hook's exact return type and initialization behavior against the &lt;a rel=&quot;nofollow&quot; href=&quot;https://usethishook.mentorbridge.in/useFingerprint&quot;&gt;official &lt;code&gt;useFingerprint&lt;/code&gt; documentation&lt;/a&gt; before using this snippet. The example assumes that the hook returns a directly renderable value; if its documented API returns an object or exposes a loading state, adapt the component accordingly.&lt;/p&gt;
&lt;p&gt;The key idea is to keep browser fingerprinting behavior separate from the UI that consumes it.&lt;/p&gt;
&lt;h3 id=&quot;where-could-you-use-browser-fingerprinting-&quot;&gt;Where Could You Use Browser Fingerprinting?&lt;/h3&gt;
&lt;p&gt;Browser fingerprinting can be relevant to several application scenarios, depending on the implementation and applicable privacy requirements.&lt;/p&gt;
&lt;h4 id=&quot;1-analytics-and-visitor-analysis&quot;&gt;1. Analytics and visitor analysis&lt;/h4&gt;
&lt;p&gt;Applications may use browser-derived signals as one input when analyzing repeat visits or understanding browsing patterns.&lt;/p&gt;
&lt;p&gt;A fingerprint should not be treated as a definitive count of unique people because one person may use multiple browser environments, and multiple people may share one environment.&lt;/p&gt;
&lt;h4 id=&quot;2-fraud-prevention&quot;&gt;2. Fraud prevention&lt;/h4&gt;
&lt;p&gt;Browser signals can contribute to a broader fraud-detection system.&lt;/p&gt;
&lt;p&gt;For example, a backend service might consider browser-derived signals alongside account activity, transaction patterns, authentication events, and other risk indicators.&lt;/p&gt;
&lt;p&gt;A fingerprint alone should never be treated as proof of identity or sufficient evidence to block a legitimate user.&lt;/p&gt;
&lt;h4 id=&quot;3-session-risk-assessment&quot;&gt;3. Session risk assessment&lt;/h4&gt;
&lt;p&gt;Applications may incorporate browser characteristics into risk assessments for sensitive workflows.&lt;/p&gt;
&lt;p&gt;For instance, a financial application might evaluate changes in browser signals alongside authentication and session information.&lt;/p&gt;
&lt;p&gt;For security-sensitive applications, keep the following principle in mind:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Browser fingerprints are supporting signals, not authentication credentials.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use server-side authorization, secure session management, and appropriate authentication controls to protect sensitive operations.&lt;/p&gt;
&lt;h3 id=&quot;is-browser-fingerprinting-reliable-&quot;&gt;Is Browser Fingerprinting Reliable?&lt;/h3&gt;
&lt;p&gt;Not completely. Understanding its limitations is essential before integrating it into a production application.&lt;/p&gt;
&lt;h4 id=&quot;fingerprints-can-change&quot;&gt;Fingerprints can change&lt;/h4&gt;
&lt;p&gt;A browser update, operating system change, or altered browser configuration may affect the collected signals.&lt;/p&gt;
&lt;h4 id=&quot;different-users-may-share-similar-signals&quot;&gt;Different users may share similar signals&lt;/h4&gt;
&lt;p&gt;Fingerprint collisions are possible. A fingerprint should not be assumed to be globally unique.&lt;/p&gt;
&lt;h4 id=&quot;privacy-focused-browsers-may-reduce-fingerprintability&quot;&gt;Privacy-focused browsers may reduce fingerprintability&lt;/h4&gt;
&lt;p&gt;Some browsers and extensions intentionally restrict or standardize exposed characteristics to make tracking more difficult.&lt;/p&gt;
&lt;h4 id=&quot;fingerprinting-has-privacy-implications&quot;&gt;Fingerprinting has privacy implications&lt;/h4&gt;
&lt;p&gt;Browser fingerprinting can contribute to tracking users without traditional cookies. Depending on the purpose, implementation, and jurisdiction, collecting or using fingerprint data may trigger transparency, consent, or other legal requirements.&lt;/p&gt;
&lt;p&gt;If your application serves users in the European Union, assess your implementation against the applicable GDPR and ePrivacy requirements. Do not assume that browser-derived identifiers are exempt from privacy obligations simply because they are not stored in cookies.&lt;/p&gt;
&lt;p&gt;Collect only what you need, explain the purpose clearly, and obtain consent where required.&lt;/p&gt;
&lt;h3 id=&quot;best-practices-when-using-usefingerprint&quot;&gt;Best Practices When Using &lt;code&gt;useFingerprint&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Before using browser fingerprinting in a production application, keep these practices in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Define the purpose first.&lt;/strong&gt; Avoid collecting browser signals without a clear product or security requirement.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do not use fingerprints as authentication.&lt;/strong&gt; A fingerprint does not prove who a user is.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan for changes.&lt;/strong&gt; Your application should handle fingerprint changes and missing values gracefully.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consider server-side rendering.&lt;/strong&gt; Browser-specific APIs are unavailable during server rendering. Verify that the hook handles server environments safely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test privacy protections.&lt;/strong&gt; Check how your application behaves when browser signals are restricted or unavailable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review privacy requirements.&lt;/strong&gt; Document the data collected, retention policy, and intended use.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-i-built-usethishook&quot;&gt;Why I Built useThisHook&lt;/h3&gt;
&lt;p&gt;As React developers, we repeatedly solve similar problems across projects.&lt;/p&gt;
&lt;p&gt;Sometimes the repeated logic is simple, such as managing a boolean. Other times, it involves browser APIs, asynchronous behavior, state management, or interactions that require more careful lifecycle handling.&lt;/p&gt;
&lt;p&gt;I built &lt;a rel=&quot;nofollow&quot; href=&quot;https://usethishook.mentorbridge.in/&quot;&gt;useThisHook&lt;/a&gt; to make reusable React behavior easier to discover, understand, and integrate.&lt;/p&gt;
&lt;p&gt;The library brings together practical hooks with documentation and interactive examples, allowing developers to focus more on application requirements and less on repeatedly rebuilding common functionality.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;useFingerprint&lt;/code&gt; is one example of the browser-focused utilities available in the collection.&lt;/p&gt;
&lt;h3 id=&quot;try-it-yourself&quot;&gt;Try It Yourself&lt;/h3&gt;
&lt;p&gt;Explore the hook, review its documented API, and experiment with the interactive example.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://usethishook.mentorbridge.in/useFingerprint&quot;&gt;https://usethishook.mentorbridge.in/useFingerprint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Library:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://usethishook.mentorbridge.in/&quot;&gt;https://usethishook.mentorbridge.in/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/senthilkumar979/useThisHook&quot;&gt;https://github.com/senthilkumar979/useThisHook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;npm package:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/usethishook&quot;&gt;https://www.npmjs.com/package/usethishook&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you find a useful improvement, encounter an edge case, or want to contribute another reusable hook, contributions and feedback are welcome.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Reusable hooks are valuable when they encapsulate behavior that would otherwise be duplicated across components.&lt;/p&gt;
&lt;p&gt;Browser fingerprinting is one example where a focused abstraction can make browser-related logic easier to integrate into a React application.&lt;/p&gt;
&lt;p&gt;But good engineering involves more than reducing boilerplate. It also means understanding the limitations, privacy implications, and security boundaries of the underlying technology.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build reusable behavior. Keep the UI focused. Use browser signals responsibly.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That is the philosophy behind useThisHook.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;— Senthil Kumar&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Building practical tools for React developers through &lt;a rel=&quot;nofollow&quot; href=&quot;https://usethishook.mentorbridge.in/&quot;&gt;useThisHook&lt;/a&gt;.&lt;/em&gt; &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30747/https-senthilkumarb6b9dcdaa4e-stop-rewriting-browser-fingerprinting-logic-react-meet</guid>
<pubDate>Sun, 11 Oct 2026 21:10:23 +0000</pubDate>
</item>
<item>
<title>🔥 30 Days of Frontend — Day 4: Memoization in React</title>
<link>https://coderlegion.com/30728/30-days-of-frontend-day-4-memoization-in-react</link>
<description>&lt;p&gt;Have you ever looked at a React component and thought:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;“Maybe I should wrap this function with &lt;code&gt;useCallback&lt;/code&gt;, this calculation with &lt;code&gt;useMemo&lt;/code&gt;, and the component with &lt;code&gt;React.memo&lt;/code&gt;?”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You're not alone.&lt;/p&gt;
&lt;p&gt;Memoization is one of the most frequently discussed React performance techniques. But it's also one of the easiest to misuse.&lt;/p&gt;
&lt;p&gt;You can add memoization to an application, introduce more complexity, and still fail to improve its performance.&lt;/p&gt;
&lt;p&gt;Why?&lt;/p&gt;
&lt;p&gt;Because memoization is not about preventing every render. It's about avoiding work that doesn't need to happen again.&lt;/p&gt;
&lt;p&gt;Let's understand how it works, when it helps, and when it becomes unnecessary complexity.&lt;/p&gt;
&lt;h3 id=&quot;1-what-is-memoization-&quot;&gt;1. What Is Memoization?&lt;/h3&gt;
&lt;p&gt;Memoization is an optimization technique that caches the result of a computation so it can be reused when the relevant inputs haven't changed.&lt;/p&gt;
&lt;p&gt;Imagine you have an expensive function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function calculateTotalPrice(products: Product[]) {
  console.log(&quot;Calculating total price...&quot;);

  return products.reduce(
    (total, product) =&amp;gt; total + product.price,
    0
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every time you call this function, it calculates the total again.&lt;/p&gt;
&lt;p&gt;If the product list hasn't changed, repeating the same calculation may be unnecessary.&lt;/p&gt;
&lt;p&gt;Memoization lets us reuse a previous result when the inputs are still valid.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Inputs
  |
  v
Have relevant inputs changed?
  |
  +---- Yes ----&amp;gt; Recalculate and cache the result
  |
  +---- No -----&amp;gt; Reuse the cached result
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React provides three commonly used memoization APIs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useMemo&lt;/code&gt; — caches a calculated value.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useCallback&lt;/code&gt; — caches a function reference.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;React.memo&lt;/code&gt; — can skip a component render when its props are unchanged.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They solve different problems, so they shouldn't be treated as interchangeable.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-usememo-cache-an-expensive-calculation&quot;&gt;2. &lt;code&gt;useMemo&lt;/code&gt;: Cache an Expensive Calculation&lt;/h3&gt;
&lt;p&gt;Consider a product dashboard containing thousands of products.&lt;/p&gt;
&lt;p&gt;You need to filter products by category and calculate which products match a search query.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function ProductList({
  products,
  search,
}: {
  products: Product[];
  search: string;
}) {
  const filteredProducts = products.filter((product) =&amp;gt;
    product.name.toLowerCase().includes(search.toLowerCase())
  );

  return (
    &amp;lt;ul&amp;gt;
      {filteredProducts.map((product) =&amp;gt; (
        &amp;lt;li key={product.id}&amp;gt;{product.name}&amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The filtering operation runs whenever &lt;code&gt;ProductList&lt;/code&gt; renders.&lt;/p&gt;
&lt;p&gt;That may be perfectly fine for a small list. But imagine the filtering logic is expensive and the component also renders because some unrelated state changes.&lt;/p&gt;
&lt;p&gt;We can cache the calculated array with &lt;code&gt;useMemo&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { useMemo } from &quot;react&quot;;

function ProductList({
  products,
  search,
}: {
  products: Product[];
  search: string;
}) {
  const filteredProducts = useMemo(() =&amp;gt; {
    return products.filter((product) =&amp;gt;
      product.name.toLowerCase().includes(search.toLowerCase())
    );
  }, [products, search]);

  return (
    &amp;lt;ul&amp;gt;
      {filteredProducts.map((product) =&amp;gt; (
        &amp;lt;li key={product.id}&amp;gt;{product.name}&amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;what-changed-&quot;&gt;What changed?&lt;/h4&gt;
&lt;p&gt;React recalculates &lt;code&gt;filteredProducts&lt;/code&gt; when &lt;code&gt;products&lt;/code&gt; or &lt;code&gt;search&lt;/code&gt; changes.&lt;/p&gt;
&lt;p&gt;If the component renders again while both dependencies remain unchanged, React can reuse the previous result.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Initial render
  └── products + search
        └── Calculate filtered products

Unrelated state changes
  └── products + search unchanged
        └── Reuse cached result

Search changes
  └── New search value
        └── Calculate filtered products again
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;an-important-limitation&quot;&gt;An important limitation&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;useMemo&lt;/code&gt; does not make every search update faster.&lt;/p&gt;
&lt;p&gt;When the user types another character, &lt;code&gt;search&lt;/code&gt; changes. The filtering calculation must run again to produce the correct results.&lt;/p&gt;
&lt;p&gt;Memoization is most useful when a calculation is expensive and its dependencies remain unchanged across renders.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-usecallback-cache-a-function-reference&quot;&gt;3. &lt;code&gt;useCallback&lt;/code&gt;: Cache a Function Reference&lt;/h3&gt;
&lt;p&gt;Now let's look at &lt;code&gt;useCallback&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Consider a parent component that passes an event handler to a child:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function ProductPage() {
  const [count, setCount] = useState(0);

  const handleAddToCart = (productId: number) =&amp;gt; {
    console.log(&quot;Added product:&quot;, productId);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;
        Count: {count}
      &amp;lt;/button&amp;gt;

      &amp;lt;ProductCard onAddToCart={handleAddToCart} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whenever &lt;code&gt;ProductPage&lt;/code&gt; renders, the function expression creates a new function reference.&lt;/p&gt;
&lt;p&gt;Even though the function's logic hasn't changed, the reference is different.&lt;/p&gt;
&lt;p&gt;That matters if &lt;code&gt;ProductCard&lt;/code&gt; is memoized and receives the handler as a prop.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const ProductCard = React.memo(function ProductCard({
  onAddToCart,
}: {
  onAddToCart: (productId: number) =&amp;gt; void;
}) {
  console.log(&quot;ProductCard rendered&quot;);

  return (
    &amp;lt;button onClick={() =&amp;gt; onAddToCart(42)}&amp;gt;
      Add to cart
    &amp;lt;/button&amp;gt;
  );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default, &lt;code&gt;React.memo&lt;/code&gt; compares props. A new function reference means the handler prop is considered changed, so the child may render again.&lt;/p&gt;
&lt;p&gt;We can stabilize the function reference with &lt;code&gt;useCallback&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { useCallback, useState } from &quot;react&quot;;

function ProductPage() {
  const [count, setCount] = useState(0);

  const handleAddToCart = useCallback((productId: number) =&amp;gt; {
    console.log(&quot;Added product:&quot;, productId);
  }, []);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;
        Count: {count}
      &amp;lt;/button&amp;gt;

      &amp;lt;ProductCard onAddToCart={handleAddToCart} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because the callback doesn't depend on any reactive values from the component, its dependency array is empty.&lt;/p&gt;
&lt;p&gt;Now, when &lt;code&gt;count&lt;/code&gt; changes, React can reuse the same callback reference. That allows the memoized &lt;code&gt;ProductCard&lt;/code&gt; to skip rendering if its other props also remain unchanged.&lt;/p&gt;
&lt;h4 id=&quot;what-about-dependencies-&quot;&gt;What about dependencies?&lt;/h4&gt;
&lt;p&gt;Consider this callback:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const handleAddToCart = useCallback(() =&amp;gt; {
  setCartCount(cartCount + 1);
}, [cartCount]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The callback depends on &lt;code&gt;cartCount&lt;/code&gt;, so its reference changes whenever &lt;code&gt;cartCount&lt;/code&gt; changes.&lt;/p&gt;
&lt;p&gt;For state updates that depend on the previous state, a functional update can sometimes avoid that dependency:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const handleAddToCart = useCallback(() =&amp;gt; {
  setCartCount((currentCount) =&amp;gt; currentCount + 1);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works because the updater receives the latest state value.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; &lt;code&gt;useCallback&lt;/code&gt; caches a function reference; it doesn't make the function's execution inherently faster.&lt;/p&gt;
&lt;p&gt;If you're not passing the function to a memoized child or using it in another dependency-sensitive context, &lt;code&gt;useCallback&lt;/code&gt; may provide no meaningful benefit.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-react-memo-skip-unnecessary-component-renders&quot;&gt;4. &lt;code&gt;React.memo&lt;/code&gt;: Skip Unnecessary Component Renders&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;React.memo&lt;/code&gt; is different from the two hooks.&lt;/p&gt;
&lt;p&gt;It wraps a component and lets React skip rendering it when its props are unchanged, subject to React's normal rendering behavior.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const ProductCard = React.memo(function ProductCard({
  name,
  price,
}: {
  name: string;
  price: number;
}) {
  console.log(&quot;Rendering ProductCard:&quot;, name);

  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h3&amp;gt;{name}&amp;lt;/h3&amp;gt;
      &amp;lt;p&amp;gt;${price}&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;
  );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Suppose the parent updates a separate counter:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function ProductPage() {
  const [count, setCount] = useState(0);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount((current) =&amp;gt; current + 1)}&amp;gt;
        Count: {count}
      &amp;lt;/button&amp;gt;

      &amp;lt;ProductCard name=&quot;Keyboard&quot; price={120} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The parent renders when the counter changes, but the &lt;code&gt;ProductCard&lt;/code&gt; props remain the same.&lt;/p&gt;
&lt;p&gt;React can skip rendering &lt;code&gt;ProductCard&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;However, &lt;code&gt;React.memo&lt;/code&gt; is not an absolute rendering lock. The component can still render because of its own state changes, consumed context changes, or other React behavior.&lt;/p&gt;
&lt;h4 id=&quot;a-common-mistake-unstable-object-props&quot;&gt;A common mistake: unstable object props&lt;/h4&gt;
&lt;p&gt;Consider this example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;ProductCard
  product={{
    id: 1,
    name: &quot;Keyboard&quot;,
    price: 120,
  }}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new object is created whenever the parent renders.&lt;/p&gt;
&lt;p&gt;Even if the object contains the same data, its reference is different.&lt;/p&gt;
&lt;p&gt;Consequently, a memoized child can still render.&lt;/p&gt;
&lt;p&gt;This is why understanding referential equality matters when optimizing React applications.&lt;/p&gt;
&lt;p&gt;Two objects can contain identical properties and still be different references:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const first = { name: &quot;Keyboard&quot; };
const second = { name: &quot;Keyboard&quot; };

console.log(first === second); // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Memoization works best when the values that cross component boundaries have the stability needed for the optimization to be effective.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-how-these-three-apis-work-together&quot;&gt;5. How These Three APIs Work Together&lt;/h3&gt;
&lt;p&gt;Let's put the concepts together.&lt;/p&gt;
&lt;p&gt;Imagine a dashboard with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A product search field&lt;/li&gt;
&lt;li&gt;A product list&lt;/li&gt;
&lt;li&gt;A shopping cart counter&lt;/li&gt;
&lt;li&gt;Expensive product filtering&lt;/li&gt;
&lt;li&gt;Memoized product cards&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A possible optimization strategy is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ProductPage
    |
    ├── useCallback
    |      └── Stabilize a handler passed to a memoized child
    |
    ├── useMemo
    |      └── Cache filtered products when dependencies are unchanged
    |
    └── ProductList
           |
           └── React.memo
                  └── Skip rendering when props are unchanged
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The APIs address different sources of repeated work:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;API &lt;/th&gt;&lt;th&gt;What it caches or optimizes &lt;/th&gt;&lt;th&gt;Useful when&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;useMemo&lt;/code&gt; &lt;/td&gt;&lt;td&gt;A calculated value &lt;/td&gt;&lt;td&gt;A calculation is expensive and dependencies often stay unchanged&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;useCallback&lt;/code&gt; &lt;/td&gt;&lt;td&gt;A function reference &lt;/td&gt;&lt;td&gt;Stable identity helps a memoized child or dependency-sensitive hook&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;React.memo&lt;/code&gt; &lt;/td&gt;&lt;td&gt;A component's render work based on props &lt;/td&gt;&lt;td&gt;A component renders often with unchanged props and rendering is costly&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The key is not to use all three together automatically. Use only the optimization that addresses the measured bottleneck.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-when-memoization-doesn-t-help&quot;&gt;6. When Memoization Doesn't Help&lt;/h3&gt;
&lt;p&gt;Memoization has costs too.&lt;/p&gt;
&lt;p&gt;It adds some combination of dependency tracking, comparisons, cached values, and code complexity.&lt;/p&gt;
&lt;p&gt;Consider this example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const fullName = useMemo(() =&amp;gt; {
  return `${firstName} ${lastName}`;
}, [firstName, lastName]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unless there is a specific reason to cache this value, the direct version is simpler:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const fullName = `${firstName} ${lastName}`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Combining two strings is inexpensive. Memoizing it is unlikely to produce a measurable improvement.&lt;/p&gt;
&lt;p&gt;Another example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const handleClick = useCallback(() =&amp;gt; {
  console.log(&quot;Clicked&quot;);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If this function is used only by a regular button in the same component, stabilizing its identity may not improve anything.&lt;/p&gt;
&lt;p&gt;The lesson is simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Not every calculation is expensive, not every render is a problem, and not every new function reference matters.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-react-compiler-changes-the-conversation&quot;&gt;7. React Compiler Changes the Conversation&lt;/h3&gt;
&lt;p&gt;Modern React projects may use the React Compiler, which can automatically memoize components, values, and functions when it can safely do so.&lt;/p&gt;
&lt;p&gt;This means manually adding &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, and &lt;code&gt;React.memo&lt;/code&gt; everywhere is less necessary in compiler-enabled applications.&lt;/p&gt;
&lt;p&gt;However, the compiler does not eliminate the need to understand performance.&lt;/p&gt;
&lt;p&gt;It cannot make every expensive algorithm cheap, remove the cost of rendering thousands of unnecessary elements, or guarantee that a slow interaction will become fast.&lt;/p&gt;
&lt;p&gt;You should still understand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What triggers a render&lt;/li&gt;
&lt;li&gt;How props and references change&lt;/li&gt;
&lt;li&gt;Which calculations are expensive&lt;/li&gt;
&lt;li&gt;When state should live closer to the components that use it&lt;/li&gt;
&lt;li&gt;How to measure actual rendering performance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your project uses React Compiler, follow its guidance and avoid adding manual memoization without a reason. Manual memoization can still be useful in appropriate cases, but it shouldn't be automatic boilerplate.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-final-takeaway&quot;&gt;8. Final Takeaway&lt;/h3&gt;
&lt;p&gt;Memoization is a tool, not a coding style, and the goal isn't to eliminate every render, the goal is to make the application responsive while keeping the code maintainable.&lt;/p&gt;
&lt;p&gt;My preferred performance workflow remains:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Measure → Identify the bottleneck → Optimize → Measure again.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Don't optimize because a component rendered, optimize because you've found work that is unnecessarily expensive.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/30728/30-days-of-frontend-day-4-memoization-in-react</guid>
<pubDate>Sun, 11 Oct 2026 17:45:13 +0000</pubDate>
</item>
<item>
<title>Your useEffect Search Bar Has a Race Condition, and It Only Shows Up When Users Type Fast</title>
<link>https://coderlegion.com/30278/your-useeffect-search-bar-has-a-race-condition-and-it-only-shows-up-when-users-type-fast</link>
<description>&lt;p&gt;This is one of those bugs where the code looks completely reasonable, passes every manual test you'd naturally think to run, and still ships with a real defect that only shows up under a specific, very common usage pattern: someone typing faster than your network requests resolve.&lt;/p&gt;
&lt;h3 id=&quot;the-setup-1&quot;&gt;The Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// components/SearchBox.tsx
'use client';

export function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState&amp;lt;Result[]&amp;gt;([]);

  useEffect(() =&amp;gt; {
    if (!query) {
      setResults([]);
      return;
    }

    async function fetchResults() {
      const res = await fetch(`/api/search?q=${query}`);
      const data = await res.json();
      setResults(data.results);
    }

    fetchResults();
  }, [query]);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;input value={query} onChange={(e) =&amp;gt; setQuery(e.target.value)} /&amp;gt;
      &amp;lt;ul&amp;gt;
        {results.map((r) =&amp;gt; (
          &amp;lt;li key={r.id}&amp;gt;{r.title}&amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Type slowly, pause between letters, and this works exactly as expected. It's also wrong in a way that's specifically invisible to that kind of careful, deliberate testing.&lt;/p&gt;
&lt;h3 id=&quot;what-actually-breaks&quot;&gt;What Actually Breaks&lt;/h3&gt;
&lt;p&gt;Say a user types &quot;react&quot; quickly, letter by letter: &lt;code&gt;r&lt;/code&gt;, &lt;code&gt;re&lt;/code&gt;, &lt;code&gt;rea&lt;/code&gt;, &lt;code&gt;reac&lt;/code&gt;, &lt;code&gt;react&lt;/code&gt;. Each keystroke triggers the effect, which fires a new fetch request for that exact partial query. These five requests are now all in flight simultaneously, and nothing about this code guarantees they resolve in the order they were sent.&lt;/p&gt;
&lt;p&gt;Network conditions are not guaranteed to preserve request order. It's entirely possible, and in practice fairly common under real-world network jitter, for the request for &lt;code&gt;&quot;r&quot;&lt;/code&gt; to resolve after the request for &lt;code&gt;&quot;react&quot;&lt;/code&gt; does. When that happens, &lt;code&gt;setResults(data.results)&lt;/code&gt; runs last for the &lt;code&gt;&quot;r&quot;&lt;/code&gt; response, overwriting the correct, complete results for &lt;code&gt;&quot;react&quot;&lt;/code&gt; with a handful of broad, barely-relevant matches for a single letter the user stopped looking at four keystrokes ago.&lt;/p&gt;
&lt;p&gt;The user sees a results list that doesn't match what's sitting in the input field, with no error, no warning, nothing to indicate anything went wrong. It just looks like the search is broken or laggy, which is a uniquely frustrating kind of bug to report because it's intermittent and timing-dependent, not consistently reproducible by just repeating the same steps slowly.&lt;/p&gt;
&lt;h3 id=&quot;why-this-specifically-survives-testing&quot;&gt;Why This Specifically Survives Testing&lt;/h3&gt;
&lt;p&gt;Manual testing during development almost always involves typing at a normal, deliberate pace, often pausing to look at the screen between each change. That pace gives each request time to resolve well before the next one fires, which means the race condition's actual precondition, overlapping in-flight requests for different queries, simply never occurs during the kind of testing most developers naturally do. The bug needs genuinely fast typing, a slow or inconsistent network, or both, to reliably surface, and automated tests using scripted input often type unrealistically fast without that being the specific thing anyone's checking for.&lt;/p&gt;
&lt;h3 id=&quot;the-fix-ignore-stale-responses&quot;&gt;The Fix: Ignore Stale Responses&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&amp;gt; {
  if (!query) {
    setResults([]);
    return;
  }

  let ignore = false;

  async function fetchResults() {
    const res = await fetch(`/api/search?q=${query}`);
    const data = await res.json();

    if (!ignore) {
      setResults(data.results);
    }
  }

  fetchResults();

  return () =&amp;gt; {
    ignore = true;
  };
}, [query]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;ignore&lt;/code&gt; flag is set in the effect's cleanup function, which React runs right before the effect re-runs for a new &lt;code&gt;query&lt;/code&gt; value, or when the component unmounts. Every in-flight request from a now-outdated &lt;code&gt;query&lt;/code&gt; gets its &lt;code&gt;ignore&lt;/code&gt; flag flipped to &lt;code&gt;true&lt;/code&gt; the moment a newer request starts, so even if the old request resolves later, its result is silently discarded instead of overwriting the current, correct state.&lt;/p&gt;
&lt;h3 id=&quot;a-cleaner-version-with-abortcontroller&quot;&gt;A Cleaner Version With AbortController&lt;/h3&gt;
&lt;p&gt;The ignore-flag pattern works, but it still lets the outdated network request complete in the background, wasting bandwidth and a server round trip for a result nobody will ever see. &lt;code&gt;AbortController&lt;/code&gt; actually cancels the request itself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&amp;gt; {
  if (!query) {
    setResults([]);
    return;
  }

  const controller = new AbortController();

  async function fetchResults() {
    try {
      const res = await fetch(`/api/search?q=${query}`, {
        signal: controller.signal,
      });
      const data = await res.json();
      setResults(data.results);
    } catch (err) {
      if ((err as Error).name !== 'AbortError') {
        console.error('Search failed:', err);
      }
    }
  }

  fetchResults();

  return () =&amp;gt; {
    controller.abort();
  };
}, [query]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now a stale request is actually cancelled, not just ignored, which both fixes the race condition and avoids burning server resources on searches the user has already moved past.&lt;/p&gt;
&lt;h3 id=&quot;where-else-this-exact-pattern-bites&quot;&gt;Where Else This Exact Pattern Bites&lt;/h3&gt;
&lt;p&gt;Any &lt;code&gt;useEffect&lt;/code&gt; that fetches data based on a value that can change again before the fetch resolves has this same underlying issue, not just search boxes. A filter dropdown firing a new query on every change, a tab switcher loading different data per tab, pagination where a user clicks &quot;next&quot; rapidly, all of these share the identical race condition shape. It's the same category of timing bug I build defensively around in the dashboard and data-table components across my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;Next.js templates&lt;/a&gt;, same root cause, different trigger.&lt;/p&gt;
&lt;h3 id=&quot;the-rule-6&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Any effect that fetches data based on a value that can change again before the request resolves needs a way to discard or cancel the outdated response.&lt;/strong&gt; Either an ignore flag or an &lt;code&gt;AbortController&lt;/code&gt;, but not neither. The bug is invisible in slow, deliberate testing and reliably present the moment a real user interacts with your app faster than your network responds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Go check your own search boxes, filters, and tab switchers for a &lt;code&gt;useEffect&lt;/code&gt; fetch with no cleanup-based cancellation. If you find one, try typing into it as fast as you actually can. Drop what you find in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30278/your-useeffect-search-bar-has-a-race-condition-and-it-only-shows-up-when-users-type-fast</guid>
<pubDate>Thu, 08 Oct 2026 12:08:17 +0000</pubDate>
</item>
<item>
<title>🧠 **Coder Legion Dev Log — Building the MyZubster Metaverse from the infrastructure up**</title>
<link>https://coderlegion.com/30192/coder-legion-dev-log-building-the-myzubster-metaverse-from-the-infrastructure-up</link>
<description>&lt;p&gt;🧠 &lt;strong&gt;Coder Legion Dev Log — Building the MyZubster Metaverse from the infrastructure up&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I’ve been working on the MyZubster Metaverse with a very specific engineering approach:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;do not start from the 3D layer. Start from identity, authorization, state, lifecycle and failure handling.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The current stack already includes several production-oriented building blocks.&lt;/p&gt;
&lt;h4 id=&quot;identity-and-session-model&quot;&gt;Identity and session model&lt;/h4&gt;
&lt;p&gt;The Metaverse supports both:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;guest characters&lt;/li&gt;
&lt;li&gt;authenticated, account-linked identities&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For authenticated users, canonical character data is resolved server-side so the client cannot freely override trusted identity state.&lt;/p&gt;
&lt;p&gt;This matters because virtual-world logic becomes much harder to secure once identity, permissions and session state are mixed together.&lt;/p&gt;
&lt;h4 id=&quot;realtime-authorization&quot;&gt;Realtime authorization&lt;/h4&gt;
&lt;p&gt;One of the more important backend changes was introducing a &lt;strong&gt;fail-closed authorization path&lt;/strong&gt; for community realtime channels.&lt;/p&gt;
&lt;p&gt;If the membership authority is unavailable, access is denied instead of waiting on buffered database operations or leaving socket authorization in an ambiguous state.&lt;/p&gt;
&lt;p&gt;I added coverage for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;disconnected MongoDB state&lt;/li&gt;
&lt;li&gt;active persisted membership&lt;/li&gt;
&lt;li&gt;membership query failures&lt;/li&gt;
&lt;li&gt;unauthorized access attempts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a small change in code, but a meaningful system-design decision.&lt;/p&gt;
&lt;h4 id=&quot;room-lifecycle&quot;&gt;Room lifecycle&lt;/h4&gt;
&lt;p&gt;The room layer already supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;public / authenticated / private access policies&lt;/li&gt;
&lt;li&gt;invitations&lt;/li&gt;
&lt;li&gt;session creation&lt;/li&gt;
&lt;li&gt;participant join/leave&lt;/li&gt;
&lt;li&gt;host controls&lt;/li&gt;
&lt;li&gt;stage requests&lt;/li&gt;
&lt;li&gt;speaker approval/revocation&lt;/li&gt;
&lt;li&gt;participant moderation&lt;/li&gt;
&lt;li&gt;message reporting&lt;/li&gt;
&lt;li&gt;bounded moderation history&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The important point is that the room is treated as a stateful system, not just a visual container.&lt;/p&gt;
&lt;h4 id=&quot;frontend-state-hardening&quot;&gt;Frontend state hardening&lt;/h4&gt;
&lt;p&gt;I’ve also been cleaning up React lifecycle behavior.&lt;/p&gt;
&lt;p&gt;Recent fixes include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reloading room state when authentication changes&lt;/li&gt;
&lt;li&gt;stabilizing async data loading with &lt;code&gt;useCallback&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;tightening effect dependencies&lt;/li&gt;
&lt;li&gt;keeping UI state aligned with server-side session state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example recent commits:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0b5e7fa&lt;/code&gt; — refresh Metaverse room when auth state changes&lt;br&gt;
&lt;code&gt;fa94a88&lt;/code&gt; — stabilize frontend loading callback&lt;br&gt;
&lt;code&gt;0347827&lt;/code&gt; — make paid analytics explicit instead of automatic&lt;/p&gt;
&lt;h4 id=&quot;auth-and-observability-testing&quot;&gt;Auth and observability testing&lt;/h4&gt;
&lt;p&gt;I also aligned tests around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;authenticated Metaverse flows&lt;/li&gt;
&lt;li&gt;server-backed sessions&lt;/li&gt;
&lt;li&gt;auth expiry&lt;/li&gt;
&lt;li&gt;same-origin authenticated requests&lt;/li&gt;
&lt;li&gt;health endpoint behavior&lt;/li&gt;
&lt;li&gt;storage failure semantics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A previous checkpoint also hardened realtime membership authorization and added test coverage around failure states:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0af298e&lt;/code&gt; — fail-closed realtime auth&lt;br&gt;
&lt;code&gt;9ac9f53&lt;/code&gt; — auth session test alignment&lt;br&gt;
&lt;code&gt;5bf365d&lt;/code&gt; — auth + observability test alignment&lt;/p&gt;
&lt;h4 id=&quot;deployment-discipline&quot;&gt;Deployment discipline&lt;/h4&gt;
&lt;p&gt;Another part of the work was CI/CD hygiene.&lt;/p&gt;
&lt;p&gt;I removed automatic activation of paid Vercel Web Analytics from the deployment workflow.&lt;/p&gt;
&lt;p&gt;That means deployment stays deterministic and billing-related features remain explicit operational decisions.&lt;/p&gt;
&lt;h4 id=&quot;current-architecture-direction&quot;&gt;Current architecture direction&lt;/h4&gt;
&lt;p&gt;The development sequence I’m following is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;identity&lt;br&gt;
→ authorization&lt;br&gt;
→ state&lt;br&gt;
→ room lifecycle&lt;br&gt;
→ moderation&lt;br&gt;
→ observability&lt;br&gt;
→ realtime rendering&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is intentional.&lt;/p&gt;
&lt;p&gt;A visually impressive world with weak session handling, unclear authorization or fragile failure modes is still a fragile system.&lt;/p&gt;
&lt;p&gt;So the current priority is to make the platform behavior coherent before connecting the final immersive realtime rendering layer.&lt;/p&gt;
&lt;h4 id=&quot;skills-this-work-is-exercising&quot;&gt;Skills this work is exercising&lt;/h4&gt;
&lt;p&gt;This project has been a practical exercise in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React hooks and state synchronization&lt;/li&gt;
&lt;li&gt;Node.js backend architecture&lt;/li&gt;
&lt;li&gt;REST API design&lt;/li&gt;
&lt;li&gt;realtime authorization&lt;/li&gt;
&lt;li&gt;authentication and session handling&lt;/li&gt;
&lt;li&gt;MongoDB / Mongoose&lt;/li&gt;
&lt;li&gt;fail-closed security patterns&lt;/li&gt;
&lt;li&gt;moderation systems&lt;/li&gt;
&lt;li&gt;automated testing&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;CI/CD&lt;/li&gt;
&lt;li&gt;Vercel deployment workflows&lt;/li&gt;
&lt;li&gt;production debugging&lt;/li&gt;
&lt;li&gt;incremental system hardening&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The immersive layer is still the next milestone.&lt;/p&gt;
&lt;p&gt;But the system underneath it is becoming much more real.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build the world after the rules of the world are reliable.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;coderlegion-softwareengineering-react-nodejs-mongodb-realtime-systemdesign-devops-webdevelopment-metaverse-opensource&quot;&gt;CoderLegion #SoftwareEngineering #React #NodeJS #MongoDB #Realtime #SystemDesign #DevOps #WebDevelopment #Metaverse #OpenSource&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30192/coder-legion-dev-log-building-the-myzubster-metaverse-from-the-infrastructure-up</guid>
<pubDate>Thu, 08 Oct 2026 02:32:06 +0000</pubDate>
</item>
<item>
<title>useThisHook</title>
<link>https://coderlegion.com/30131/usethishook</link>
<description>&lt;p&gt;Stop rewriting the same React hooks in every project.&lt;/p&gt;
&lt;p&gt;Install once. Import only what you need — toggles, storage, forms, browser APIs, and dialogs. Live demos, clear APIs, copy-paste examples. Built for React 18+.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why useThisHook:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Less boilerplate&lt;/strong&gt;&lt;br&gt;
Toggles, debounce, timers, forms, lists, and URL state — ready to import so you stop rewriting the same helpers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dialogs without a UI kit&lt;/strong&gt;&lt;br&gt;
Await confirm(), prompt(), overlays, and wizards in your click handler. Mount render() once. No modal library.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zero runtime dependencies&lt;/strong&gt;&lt;br&gt;
The published package only peers on React 18+. Works with Vite, Next.js, CRA, and Module Federation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docs that match the code&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every page has a live preview, an argument/return reference, and an example you can paste into your app.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/30131/usethishook</guid>
<pubDate>Wed, 07 Oct 2026 12:03:22 +0000</pubDate>
</item>
<item>
<title>Cursor vs Offset Pagination: A Frontend Engineer's Perspective in 2026</title>
<link>https://coderlegion.com/29826/cursor-vs-offset-pagination-a-frontend-engineers-perspective-in-2026</link>
<description>&lt;p&gt;We talk about pagination as if it's purely a backend concern – the database does the heavy lifting, the API returns pages, and the frontend just renders them. But in 2026, that mental model is outdated. The frontend now &lt;em&gt;owns&lt;/em&gt; more of the data-fetching lifecycle than ever: server components prefetch, client caches hydrate, optimistic updates mutate, and streaming responses trickle in chunk by chunk.&lt;/p&gt;
&lt;p&gt;The choice between &lt;strong&gt;cursor pagination&lt;/strong&gt; and &lt;strong&gt;offset pagination&lt;/strong&gt; has real consequences for how you write your React components, how your cache behaves, how scroll feels on the phone, and what happens when a user navigates back. This post is about those tradeoffs – from the frontend seat.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-landscape-has-changed&quot;&gt;The Landscape Has Changed&lt;/h3&gt;
&lt;p&gt;A few things are different in 2026 that make this conversation more nuanced than it was three or four years ago:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;React Server Components are mainstream.&lt;/strong&gt; Data fetching happens on the server in many apps, which shifts where pagination state lives and how navigation works.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TanStack Query is the de-facto standard&lt;/strong&gt; for client-side async state, with first-class infinite query support baked in.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &quot;infinite scroll vs pagination&quot; debate is mostly settled&lt;/strong&gt; — infinite scroll wins for feeds and content-heavy apps; numbered pages win for dense data tables. Your pagination strategy should serve that decision, not fight it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM-powered search and filtering&lt;/strong&gt; are becoming common, and those use cases have their own quirks around pagination stability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge caching and CDN-level pagination&lt;/strong&gt; mean that certain offset-paginated responses can be cached by URL – a genuine advantage offset still holds.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-frontend-engineers-actually-care-about&quot;&gt;What Frontend Engineers Actually Care About&lt;/h3&gt;
&lt;p&gt;When you strip away the SQL theory, here's what the pagination choice actually affects on the frontend:&lt;/p&gt;
&lt;h4 id=&quot;1-cache-key-design&quot;&gt;1. Cache Key Design&lt;/h4&gt;
&lt;p&gt;With offset pagination, the cache key is simple and predictable: &lt;code&gt;posts?page=3&amp;amp;limit=20&lt;/code&gt;. Every page is independently cacheable by URL — your CDN loves this. TanStack Query, SWR, and Apollo all handle this naturally.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Offset — clean, predictable cache keys
const { data } = useQuery({
  queryKey: ['posts', { page, limit }],
  queryFn: () =&amp;gt; fetchPosts({ page, limit }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With cursor pagination, the cache key depends on the &lt;em&gt;previous&lt;/em&gt; response. You can't know page 3's key without having fetched pages 1 and 2. This is why TanStack Query has a dedicated &lt;code&gt;useInfiniteQuery&lt;/code&gt; API — the standard &lt;code&gt;useQuery&lt;/code&gt; just doesn't model this shape.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Cursor — requires infinite query primitives
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam }) =&amp;gt; fetchPosts({ cursor: pageParam, limit: 20 }),
  getNextPageParam: (lastPage) =&amp;gt; lastPage.pagination.nextCursor ?? undefined,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The tradeoff: Offset gives you individually shareable, bookmarkable, CDN-cacheable page URLs. Cursor gives you a stable, append-only data structure that's ideal for infinite scroll but harder to deep-link into.&lt;/p&gt;
&lt;h4 id=&quot;2-navigation-and-the-back-button&quot;&gt;2. Navigation and the Back Button&lt;/h4&gt;
&lt;p&gt;This is where offset pagination has a quiet, underappreciated advantage: &lt;strong&gt;it maps onto URL state naturally.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;/products?page=4&amp;amp;sort=price_asc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A user on page 4 can bookmark it, share it, or hit back and land exactly where they were. The server can render the correct page server-side with no hydration complexity.&lt;/p&gt;
&lt;p&gt;With cursor pagination, you'd need to store the cursor in the URL too:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;/products?cursor=eyJpZCI6MTIzfQ&amp;amp;sort=price_asc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That works – but it's opaque to users, it breaks if the cursor expires server-side (a real issue with time-limited cursors), and it requires your RSC or SSR layer to be cursor-aware. More importantly, if the user wants to share page 4 with a colleague, that cursor only means &quot;after item 123&quot; — not &quot;the 4th page of results sorted by price.&quot; These are semantically different things, and users don't distinguish between them until something breaks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The 2026 wrinkle:&lt;/strong&gt; With React Server Components and the App Router, navigating back often re-fetches rather than restoring scroll position. Offset pagination + URL state handles this correctly out of the box. Cursor-based infinite scroll requires extra work — typically saving scroll position to &lt;code&gt;sessionStorage&lt;/code&gt; and restoring it, or using the &lt;code&gt;experimental_useOptimistic&lt;/code&gt; pattern with a pre-fetched cache.&lt;/p&gt;
&lt;h4 id=&quot;3-optimistic-updates-and-mutations&quot;&gt;3. Optimistic Updates and Mutations&lt;/h4&gt;
&lt;p&gt;Suppose a user creates a new post. With offset pagination:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The new post appears at position 1 in the sorted order.&lt;/li&gt;
&lt;li&gt;Pages 1 through N all shift.&lt;/li&gt;
&lt;li&gt;Any cached pages are now stale.&lt;/li&gt;
&lt;li&gt;You invalidate all queries with &lt;code&gt;queryClient.invalidateQueries(['posts'])&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The user sees a brief loading state before the refetch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With cursor pagination:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You prepend the new post optimistically to the top of the feed.&lt;/li&gt;
&lt;li&gt;The cursor chain stays intact — future &quot;load more&quot; fetches are unaffected.&lt;/li&gt;
&lt;li&gt;You can invalidate only the first page while keeping the rest of the cache valid.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Optimistic prepend with cursor pagination
queryClient.setQueryData(['posts'], (old) =&amp;gt; ({
  ...old,
  pages: [
    { data: [newPost, ...old.pages[0].data], pagination: old.pages[0].pagination },
    ...old.pages.slice(1),
  ],
}));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cursor pagination is genuinely better here. Offset pagination invalidation is coarse — you either refetch everything or accept a stale cache. Cursor pagination lets you surgically update the head of the feed while trusting the tail.&lt;/p&gt;
&lt;h4 id=&quot;4-scroll-performance-and-virtualization&quot;&gt;4. Scroll Performance and Virtualization&lt;/h4&gt;
&lt;p&gt;For long feeds rendered in the browser, you'll eventually reach for a virtualizer — TanStack Virtual, React Window, or similar. The pagination strategy affects how you feed data into the virtualizer.&lt;/p&gt;
&lt;p&gt;With offset pagination, you know the total count upfront. You can pre-size the scroll container and render placeholder rows for unloaded pages:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// You can pre-allocate rows because you know totalCount
const rowVirtualizer = useVirtualizer({
  count: totalCount,
  getScrollElement: () =&amp;gt; parentRef.current,
  estimateSize: () =&amp;gt; 72,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With cursor pagination, you don't know the total. You render what you have, append more as the user scrolls, and use a sentinel element to trigger the next fetch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Sentinel-based infinite scroll
const { ref: sentinelRef } = useIntersectionObserver({
  onChange: (inView) =&amp;gt; { if (inView &amp;amp;&amp;amp; hasNextPage) fetchNextPage(); },
});

// At the bottom of your list:
&amp;lt;div ref={sentinelRef} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Neither is strictly better — they serve different UX patterns. But in 2026, the &lt;strong&gt;intersection observer + cursor&lt;/strong&gt; pattern is so well-supported and so well-understood that it should be your default for any feed-like UI.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-2026-tradeoff-matrix&quot;&gt;The 2026 Tradeoff Matrix&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Concern &lt;/th&gt;&lt;th&gt;Offset &lt;/th&gt;&lt;th&gt;Cursor&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;URL shareability / deep linking &lt;/td&gt;&lt;td&gt;✅ Natural &lt;/td&gt;&lt;td&gt;⚠️ Possible but awkward&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Infinite scroll UX &lt;/td&gt;&lt;td&gt;⚠️ Requires workarounds &lt;/td&gt;&lt;td&gt;✅ Native fit&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Numbered page UI (&quot;Page 3 of 47&quot;) &lt;/td&gt;&lt;td&gt;✅ Trivial &lt;/td&gt;&lt;td&gt;❌ Not supported&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Back-button behavior (RSC/App Router) &lt;/td&gt;&lt;td&gt;✅ Works with URL state &lt;/td&gt;&lt;td&gt;⚠️ Needs extra work&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CDN-level page caching &lt;/td&gt;&lt;td&gt;✅ URL is the cache key &lt;/td&gt;&lt;td&gt;❌ Cursor is opaque&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Optimistic prepend on new items &lt;/td&gt;&lt;td&gt;⚠️ Invalidates full cache &lt;/td&gt;&lt;td&gt;✅ Surgical cache update&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Stable results under concurrent writes &lt;/td&gt;&lt;td&gt;❌ Skips / duplicates &lt;/td&gt;&lt;td&gt;✅ Stable&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;TanStack Query / SWR compatibility &lt;/td&gt;&lt;td&gt;✅ &lt;code&gt;useQuery&lt;/code&gt; &lt;/td&gt;&lt;td&gt;✅ &lt;code&gt;useInfiniteQuery&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;LLM search result pagination &lt;/td&gt;&lt;td&gt;✅ Page-level caching useful &lt;/td&gt;&lt;td&gt;⚠️ Stream-based, different model&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Large dataset performance (&amp;gt; 1M rows) &lt;/td&gt;&lt;td&gt;❌ Deep offset is slow &lt;/td&gt;&lt;td&gt;✅ O(log N) always&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;SSR / RSC initial render &lt;/td&gt;&lt;td&gt;✅ Simple prop passing &lt;/td&gt;&lt;td&gt;⚠️ Needs cursor threading&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-emerging-middle-ground-hybrid-pagination&quot;&gt;The Emerging Middle Ground: Hybrid Pagination&lt;/h3&gt;
&lt;p&gt;A pattern gaining traction in 2026 is the &lt;strong&gt;hybrid approach&lt;/strong&gt; — especially in apps that have both a data-table view and a feed view of the same resource.&lt;/p&gt;
&lt;p&gt;The idea: expose cursor pagination at the API level (for stability and performance), but compute and expose a virtual page number on top of it. The frontend stores a map of &lt;code&gt;pageNumber → cursor&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Client-side page-to-cursor map
const cursorMap = useRef&amp;lt;Map&amp;lt;number, string&amp;gt;&amp;gt;(new Map([[1, '']]));

function goToPage(pageNumber: number) {
  const cursor = cursorMap.current.get(pageNumber);
  if (cursor !== undefined) {
    setCurrentCursor(cursor);
  }
  // If not in map yet, must paginate forward to reach it
}

// After each page fetch, store the cursor for the next page
function onPageFetched(pageNumber: number, nextCursor: string) {
  cursorMap.current.set(pageNumber + 1, nextCursor);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is more complex but gives you the best of both worlds: server-side cursor performance and stability, with a client-side illusion of numbered pages. Libraries like &lt;strong&gt;Relay&lt;/strong&gt; have done a version of this for years; it's increasingly practical with modern client-side state tooling.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-about-react-server-components-specifically-&quot;&gt;What About React Server Components Specifically?&lt;/h3&gt;
&lt;p&gt;RSC changes the calculus a bit. When the server renders the initial page, it can use a cursor or offset equally well — the component is async and can await either query. But navigation changes things.&lt;/p&gt;
&lt;p&gt;With offset + URL params, navigation is a first-class RSC operation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/posts/page.tsx
export default async function PostsPage({ searchParams }) {
  const page = Number(searchParams.page ?? 1);
  const posts = await fetchPosts({ page, limit: 20 });
  return &amp;lt;PostList posts={posts} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Navigating to &lt;code&gt;?page=4&lt;/code&gt; re-runs the server component with new params — clean, cacheable at the RSC level, and SSR-friendly.&lt;/p&gt;
&lt;p&gt;With cursors and RSC, you need the cursor in the URL or in a server-side session. The URL is simpler and more robust:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/posts/page.tsx
export default async function PostsPage({ searchParams }) {
  const cursor = searchParams.cursor ?? null;
  const { posts, nextCursor } = await fetchPosts({ cursor, limit: 20 });
  return &amp;lt;PostList posts={posts} nextCursor={nextCursor} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both work. But the cursor approach requires your client to manage and push the cursor into the URL, which pushes complexity back toward the client. For RSC-heavy apps, offset pagination for paginated &lt;em&gt;pages&lt;/em&gt; and cursor pagination for client-rendered &lt;em&gt;feeds&lt;/em&gt; is a reasonable split.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;practical-recommendation-for-2026&quot;&gt;Practical Recommendation for 2026&lt;/h3&gt;
&lt;p&gt;The decision tree is simpler than it used to be:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use cursor pagination if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The UI is a feed, timeline, or &quot;load more&quot; pattern&lt;/li&gt;
&lt;li&gt;The dataset grows fast or has high write concurrency&lt;/li&gt;
&lt;li&gt;You're building a mobile-first or infinite-scroll experience&lt;/li&gt;
&lt;li&gt;Correctness under concurrent updates matters&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Use offset pagination if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The UI is a data table with numbered pages and sorting controls&lt;/li&gt;
&lt;li&gt;Users need to deep-link to or bookmark a specific page&lt;/li&gt;
&lt;li&gt;You need CDN-level caching of paginated responses&lt;/li&gt;
&lt;li&gt;The dataset is bounded and relatively static&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Use the hybrid approach if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You have both UX patterns on the same data&lt;/li&gt;
&lt;li&gt;You're building a high-quality admin interface with a public-facing feed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The old &quot;just use offset, it's simple&quot; advice is actively harmful at scale. But the opposite — &quot;always use cursor, it's correct&quot; — ignores real frontend constraints around navigation, SSR, and URL state that still matter in 2026.&lt;/p&gt;
&lt;p&gt;Pick based on your UI pattern first, your dataset characteristics second, and your caching strategy third. The backend team will thank you for having a clear answer.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29826/cursor-vs-offset-pagination-a-frontend-engineers-perspective-in-2026</guid>
<pubDate>Mon, 05 Oct 2026 08:51:24 +0000</pubDate>
</item>
<item>
<title>🔥 30 Days of Frontend — Day 3: React Fiber</title>
<link>https://coderlegion.com/29744/30-days-of-frontend-day-3-react-fiber</link>
<description>&lt;p&gt;You've probably heard about &lt;strong&gt;React Fiber&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But why does it matter?&lt;/p&gt;
&lt;p&gt;And more importantly:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What problem was Fiber actually designed to solve?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Fiber is React's internal architecture for representing and processing the work needed to update the UI.&lt;/p&gt;
&lt;p&gt;You don't normally interact with Fiber directly, but understanding the idea behind it gives you a much better mental model of how modern React rendering works.&lt;/p&gt;
&lt;p&gt;A useful simplified mental model is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Component Tree
      ↓
   Fiber Tree
      ↓
 Scheduled Work
      ↓
Render / Reconciliation
      ↓
    Commit
      ↓
 Updated UI
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part isn't memorizing React's internal Fiber fields.&lt;/p&gt;
&lt;p&gt;It's understanding why this architecture exists.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-why-did-react-need-fiber-&quot;&gt;🧠 Why Did React Need Fiber?&lt;/h3&gt;
&lt;p&gt;Imagine a React application with a large component tree.&lt;/p&gt;
&lt;p&gt;A state update happens:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;State Update
     ↓
React needs to render
     ↓
Large component tree
     ↓
A lot of JavaScript work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rendering isn't free.&lt;/p&gt;
&lt;p&gt;React may need to evaluate components, compare the new result with the previous one, determine what changed, and eventually apply the required updates.&lt;/p&gt;
&lt;p&gt;If a large amount of work has to happen as one uninterrupted operation, it can keep the browser's main thread busy.&lt;/p&gt;
&lt;p&gt;And the main thread isn't only responsible for React.&lt;/p&gt;
&lt;p&gt;It also needs to handle things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User interactions&lt;/li&gt;
&lt;li&gt;JavaScript execution&lt;/li&gt;
&lt;li&gt;Layout&lt;/li&gt;
&lt;li&gt;Painting&lt;/li&gt;
&lt;li&gt;Animations&lt;/li&gt;
&lt;li&gt;Browser events&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the problem becomes:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;How can React manage rendering work without allowing expensive updates to unnecessarily block more important work?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is where Fiber becomes important.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-fiber-as-a-unit-of-work&quot;&gt;⚙️ Fiber as a Unit of Work&lt;/h3&gt;
&lt;p&gt;One of the important ideas behind Fiber is that React can represent rendering work as smaller units rather than treating an entire update as one indivisible operation.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Large Rendering Task
        ↓
┌───────────────────────┐
│ Work A                │
│ Work B                │
│ Work C                │
│ Work D                │
│ Work E                │
└───────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of thinking only in terms of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;Render the entire application&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React can reason about individual pieces of work.&lt;/p&gt;
&lt;p&gt;That gives React more flexibility when deciding how and when that work should be processed.&lt;/p&gt;
&lt;p&gt;This is one of the architectural ideas that enables React to support more sophisticated scheduling and concurrent rendering capabilities.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-component-tree-vs-fiber-tree&quot;&gt;🌳 Component Tree vs Fiber Tree&lt;/h3&gt;
&lt;p&gt;As developers, we usually think about our application as a component tree:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;App
├── Header
├── Sidebar
├── Dashboard
│   ├── Statistics
│   ├── Chart
│   └── ProductList
│       ├── ProductCard
│       ├── ProductCard
│       └── ProductCard
└── Footer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Internally, React maintains a corresponding structure involving Fiber nodes.&lt;/p&gt;
&lt;p&gt;A simplified mental model is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Your Components
      ↓
React's Fiber representation
      ↓
Units of rendering work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A Fiber node contains information React needs while processing a particular part of the tree.&lt;/p&gt;
&lt;p&gt;As frontend developers, we normally don't need to interact with these nodes directly.&lt;/p&gt;
&lt;p&gt;They're an implementation detail.&lt;/p&gt;
&lt;p&gt;But understanding that this internal representation exists helps explain how React can perform reconciliation and manage rendering work.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-render-phase-vs-commit-phase&quot;&gt;🔄 Render Phase vs Commit Phase&lt;/h3&gt;
&lt;p&gt;Another important concept is that React's work can be understood through two major phases:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Update
  ↓
Render Phase
  ↓
Commit Phase
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These phases have different responsibilities.&lt;/p&gt;
&lt;h4 id=&quot;render-phase&quot;&gt;Render Phase&lt;/h4&gt;
&lt;p&gt;During the render phase, React determines what the next UI should look like.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;State / Props Change
        ↓
Component Rendering
        ↓
Reconciliation
        ↓
Determine what changed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React is essentially trying to answer:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;Given the new state and props, what should the UI look like?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is where React performs much of the computational work involved in rendering and reconciliation.&lt;/p&gt;
&lt;p&gt;Importantly, rendering does not mean React has already changed the DOM.&lt;/p&gt;
&lt;p&gt;It's primarily about figuring out the next result.&lt;/p&gt;
&lt;h4 id=&quot;commit-phase&quot;&gt;Commit Phase&lt;/h4&gt;
&lt;p&gt;Once React has finished determining the required changes, it can move to the commit phase.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Finished Render Work
        ↓
      Commit
        ↓
Apply Required Changes
        ↓
      DOM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The commit phase is where React applies the necessary changes to the host environment, such as the browser DOM.&lt;/p&gt;
&lt;p&gt;This distinction is useful when thinking about React performance because expensive rendering work and the actual DOM mutations are not the same thing.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-scheduling-and-prioritization&quot;&gt;🚦 Scheduling and Prioritization&lt;/h3&gt;
&lt;p&gt;One of the most interesting ideas behind modern React is that not all updates necessarily have the same urgency.&lt;/p&gt;
&lt;p&gt;Consider two updates:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User clicks a button
        ↓
Should feel responsive immediately
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;versus:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Large list needs to update
        ↓
Can potentially happen with lower urgency
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A useful conceptual model is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                    React Work
                        │
            ┌───────────┴───────────┐
            ↓                       ↓
       More urgent              Less urgent
            │                       │
            ↓                       ↓
    User interaction          Background UI work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ability to reason about work and prioritize it is an important part of the architecture behind React's concurrent features.&lt;/p&gt;
&lt;p&gt;It's worth being precise here:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Concurrent rendering does not mean React is rendering everything on multiple threads.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;JavaScript in the browser still primarily runs on the main thread.&lt;/p&gt;
&lt;p&gt;The important idea is that React has more flexibility around &lt;strong&gt;when rendering work is performed, interrupted, resumed, or abandoned&lt;/strong&gt; before committing the final result.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-why-this-matters-for-performance&quot;&gt;🧩 Why This Matters for Performance&lt;/h3&gt;
&lt;p&gt;Understanding Fiber changes how you think about React performance.&lt;/p&gt;
&lt;p&gt;It's easy to think:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;React is fast because it uses a Virtual DOM.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's an oversimplification.&lt;/p&gt;
&lt;p&gt;Modern React performance involves several different concepts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;State Updates
     ↓
Scheduling
     ↓
Rendering
     ↓
Reconciliation
     ↓
Commit
     ↓
Browser Rendering
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each stage can contribute to the overall cost of an interaction.&lt;/p&gt;
&lt;p&gt;For example, imagine a product page rendering 2,000 product cards.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;2,000 Products
      ↓
2,000 ProductCard components
      ↓
Large amount of rendering work
      ↓
Slower interaction
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A developer might immediately think:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;I should add &lt;code&gt;React.memo&lt;/code&gt;.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But profiling might reveal a more fundamental problem:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Why are we rendering 2,000 products in the first place?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If the user can only see 20 products at a time, reducing the number of rendered components can be much more effective than trying to make every individual component slightly faster.&lt;/p&gt;
&lt;p&gt;That leads to an important performance principle:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Before optimizing individual units of work, ask whether you can reduce the amount of work altogether.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-fiber-and-react-profiler&quot;&gt;🔍 Fiber and React Profiler&lt;/h3&gt;
&lt;p&gt;This is also why tools such as the React Profiler are useful.&lt;/p&gt;
&lt;p&gt;Instead of guessing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;Maybe useMemo will fix it.&quot;
&quot;Maybe React.memo will fix it.&quot;
&quot;Maybe useCallback will fix it.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;you can first ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What work is actually happening during this interaction?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For example, you might see something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ProductGrid        79.9ms
ProductCard          0.4ms
ProductCard          0.3ms
ProductCard          0.4ms
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first glance, you might think: &quot;ProductGrid is slow&quot;, But if thousands of ProductCards are being processed underneath it, the real problem may be the quantity of work, rather than one particularly slow component.&lt;/p&gt;
&lt;p&gt;After reducing the visible list:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ProductGrid         1.2ms
ProductCard         0.2ms
ProductGridHeader   0.1ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the performance characteristics are completely different.&lt;/p&gt;
&lt;p&gt;This is a good example of why performance optimization should usually follow this process:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Measure
   ↓
Identify the expensive work
   ↓
Understand why it happens
   ↓
Reduce unnecessary work
   ↓
Optimize what remains
   ↓
Measure again
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-the-important-takeaway&quot;&gt;🎯 The Important Takeaway&lt;/h3&gt;
&lt;p&gt;I don't think frontend developers need to memorize every Fiber field or understand every internal implementation detail.&lt;/p&gt;
&lt;p&gt;The valuable part is understanding the reason Fiber exists.&lt;/p&gt;
&lt;p&gt;Rendering UI can be expensive, so React needs an architecture that allows it to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Represent rendering work&lt;/li&gt;
&lt;li&gt;Break work into manageable units&lt;/li&gt;
&lt;li&gt;Schedule work&lt;/li&gt;
&lt;li&gt;Prioritize updates&lt;/li&gt;
&lt;li&gt;Reconcile changes&lt;/li&gt;
&lt;li&gt;Commit the final result&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of immediately asking:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;Which optimization should I use?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I think a better first question is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;What work is React doing, why is it doing that work, and can I reduce it?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Understanding Fiber gives us a much stronger mental model for reasoning about all of those decisions.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/29744/30-days-of-frontend-day-3-react-fiber</guid>
<pubDate>Sun, 04 Oct 2026 16:46:36 +0000</pubDate>
</item>
<item>
<title>Your Server Component Might Be Fetching Data Three Times Slower Than It Needs To</title>
<link>https://coderlegion.com/29565/your-server-component-might-be-fetching-data-three-times-slower-than-it-needs-to</link>
<description>&lt;p&gt;This is one of the easiest performance mistakes to write without noticing, since the code reads completely naturally, top to bottom, each line waiting for the one before it to finish, exactly the way most people learn to write async code in the first place.&lt;/p&gt;
&lt;h3 id=&quot;the-setup-that-reads-perfectly-naturally&quot;&gt;The Setup That Reads Perfectly Naturally&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/dashboard/page.tsx
export default async function DashboardPage() {
  const user = await getUser();
  const posts = await getPosts();
  const comments = await getComments();
  const analytics = await getAnalytics();

  return (
    &amp;lt;Dashboard
      user={user}
      posts={posts}
      comments={comments}
      analytics={analytics}
    /&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four &lt;code&gt;await&lt;/code&gt; calls, one after another. Clean, readable, and exactly how sequential code naturally gets written when each line is just &quot;get the next thing I need.&quot; The problem is that none of these four calls actually depend on the results of the others, &lt;code&gt;getPosts&lt;/code&gt; doesn't need anything from &lt;code&gt;getUser&lt;/code&gt;, &lt;code&gt;getComments&lt;/code&gt; doesn't need anything from &lt;code&gt;getPosts&lt;/code&gt;, and so on. They're all independently fetchable at the same time, and writing them sequentially means the total page load time is the sum of all four, not the time of the single slowest one.&lt;/p&gt;
&lt;h3 id=&quot;why-this-costs-more-than-it-looks-like-it-should&quot;&gt;Why This Costs More Than It Looks Like It Should&lt;/h3&gt;
&lt;p&gt;If each of these four calls takes roughly 150 milliseconds, a genuinely reasonable time for a real database query, the sequential version takes roughly 600 milliseconds total, each one waiting for the previous one to fully complete before even starting. Run the same four calls concurrently instead, and the total time is roughly 150 milliseconds, the time of the single slowest call, since they're all genuinely happening at once rather than queued up one behind another for no actual reason.&lt;/p&gt;
&lt;p&gt;This is a real, measurable difference on a page real users are waiting for, and it's entirely avoidable, since nothing about these four pieces of data actually requires this sequential order, the code just happens to be written in a shape that imposes one anyway.&lt;/p&gt;
&lt;h3 id=&quot;the-actual-fix-promise-all-for-independent-fetches&quot;&gt;The Actual Fix: Promise.all for Independent Fetches&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default async function DashboardPage() {
  const [user, posts, comments, analytics] = await Promise.all([
    getUser(),
    getPosts(),
    getComments(),
    getAnalytics(),
  ]);

  return (
    &amp;lt;Dashboard
      user={user}
      posts={posts}
      comments={comments}
      analytics={analytics}
    /&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Promise.all&lt;/code&gt; starts all four requests essentially simultaneously and waits for all of them to complete together, rather than starting each one only after the last one finished. Same data, same final render, a fraction of the total wait, purely by removing an ordering constraint that was never actually necessary in the first place.&lt;/p&gt;
&lt;h3 id=&quot;where-this-gets-genuinely-trickier-partial-dependencies&quot;&gt;Where This Gets Genuinely Trickier: Partial Dependencies&lt;/h3&gt;
&lt;p&gt;Not every case is this clean. Sometimes one piece of data genuinely depends on another, and mixing dependent and independent fetches needs a bit more care than a single flat &lt;code&gt;Promise.all&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// ❌ Everything sequential, even though posts and analytics don't need each other
export default async function DashboardPage() {
  const user = await getUser();
  const posts = await getPostsForUser(user.id); // genuinely depends on user
  const analytics = await getAnalytics(); // does NOT depend on user at all
  return &amp;lt;Dashboard user={user} posts={posts} analytics={analytics} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// ✅ Only the genuine dependency stays sequential, everything else runs alongside it
export default async function DashboardPage() {
  const analyticsPromise = getAnalytics(); // starts immediately, not awaited yet

  const user = await getUser();
  const posts = await getPostsForUser(user.id); // genuinely needs user.id first

  const analytics = await analyticsPromise; // was already running this whole time

  return &amp;lt;Dashboard user={user} posts={posts} analytics={analytics} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Starting &lt;code&gt;getAnalytics()&lt;/code&gt; without immediately awaiting it lets that request begin running in the background while &lt;code&gt;getUser&lt;/code&gt; and &lt;code&gt;getPostsForUser&lt;/code&gt; proceed through their genuine, necessary sequence. By the time the code actually needs the analytics result, it's very likely already finished, or at least has been running the whole time instead of only starting after everything else completed.&lt;/p&gt;
&lt;h3 id=&quot;how-to-actually-spot-this-in-your-own-code&quot;&gt;How to Actually Spot This in Your Own Code&lt;/h3&gt;
&lt;p&gt;Look for multiple &lt;code&gt;await&lt;/code&gt; statements in a row where each one calls a genuinely independent function, nothing on the left side of an earlier line appears as an argument to a later one. That absence of a real data dependency is the signal, these calls have no actual reason to be sequential, and wrapping them in &lt;code&gt;Promise.all&lt;/code&gt; is very likely a free, meaningful performance improvement with zero behavior change.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;grep -B2 -A2 &quot;await get&quot; app/**/page.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Scanning for clusters of consecutive &lt;code&gt;await&lt;/code&gt; calls to different functions, then checking whether any of them genuinely reference the previous line's result, is a quick, practical way to find real waterfall candidates across an existing codebase.&lt;/p&gt;
&lt;h3 id=&quot;why-this-matters-more-on-pages-with-several-independent-data-sources&quot;&gt;Why This Matters More on Pages With Several Independent Data Sources&lt;/h3&gt;
&lt;p&gt;A dashboard, a page with several distinct widgets or sections each backed by their own query, is exactly where this pattern accumulates the most cost, since it's common to have four, five, or more genuinely independent pieces of data feeding one page, each one innocently written as its own &lt;code&gt;await&lt;/code&gt; line, each one adding its full latency on top of all the others instead of overlapping.&lt;/p&gt;
&lt;h3 id=&quot;the-actual-rule&quot;&gt;The Actual Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Before awaiting multiple async calls sequentially, check whether any of them genuinely need a previous call's result. If none do, &lt;code&gt;Promise.all&lt;/code&gt; turns their combined wait time from a sum into a maximum, for free, with no behavior change.&lt;/strong&gt; For a genuine mix of dependent and independent calls, start the independent ones early without immediately awaiting them, so they run in the background while the real, necessary sequence proceeds.&lt;/p&gt;
&lt;p&gt;I check every Server Component I write for exactly this pattern now, across client work and the dashboards and templates I build at &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pixelanas.com&quot;&gt;pixelanas.com&lt;/a&gt;, since it's one of the highest-value, lowest-effort performance fixes available, and I've written about related data-fetching patterns on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pixelanas.com/blog&quot;&gt;blog&lt;/a&gt; too.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Go check your own dashboard or any data-heavy page for consecutive &lt;code&gt;await&lt;/code&gt; calls with no genuine dependency between them. If you find some, wrapping them in &lt;code&gt;Promise.all&lt;/code&gt; is very likely a free win sitting right there. Drop what you find in the comments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29565/your-server-component-might-be-fetching-data-three-times-slower-than-it-needs-to</guid>
<pubDate>Fri, 02 Oct 2026 18:05:04 +0000</pubDate>
</item>
<item>
<title>Optimizing User Idle Detection in React</title>
<link>https://coderlegion.com/29465/optimizing-user-idle-detection-in-react</link>
<description>&lt;p&gt;Managing user idle states efficiently is critical for performance-heavy applications. In the latest update to &lt;code&gt;react-hook-lab&lt;/code&gt;, I have refined the &lt;code&gt;useIdle&lt;/code&gt; hook to provide more granular control over event throttling.&lt;/p&gt;
&lt;h4 id=&quot;what-changed--1&quot;&gt;What Changed?&lt;/h4&gt;
&lt;p&gt;Previously, the throttle mechanism was rigid. To prevent unnecessary re-renders during high-frequency events (like mouse movement), I have updated the logic to use a dynamic throttle window. The hook now calculates &lt;code&gt;effectiveThrottle&lt;/code&gt; as the minimum of your defined timeout or half the total duration, ensuring responsiveness while keeping the main thread lean.&lt;/p&gt;
&lt;h4 id=&quot;implementation-examples&quot;&gt;Implementation Examples&lt;/h4&gt;
&lt;h5 id=&quot;basic-usage&quot;&gt;Basic Usage&lt;/h5&gt;
&lt;p&gt;Simply track when a user is inactive for 5 seconds:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const isIdle = useIdle(5000);

return &amp;lt;div&amp;gt;{isIdle ? 'User is away' : 'User is active'}&amp;lt;/div&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;custom-throttle-behavior&quot;&gt;Custom Throttle Behavior&lt;/h5&gt;
&lt;p&gt;The hook now automatically balances the throttle duration based on your timeout setting to prevent performance bottlenecks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// The hook will now self-adjust the throttle to be 
// more aggressive for shorter idle timeouts.
const isIdle = useIdle(1000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;resources&quot;&gt;Resources&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Saurav-TB-Pandey/react-hook-lab&quot;&gt;GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;NPM Package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/pandeysaurav/&quot;&gt;LinkedIn Profile&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/better-user-engagement-tracking-with.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29465/optimizing-user-idle-detection-in-react</guid>
<pubDate>Fri, 02 Oct 2026 02:38:12 +0000</pubDate>
</item>
<item>
<title>Searching a Billion Records: A Field Manual for Frontend Ninjas</title>
<link>https://coderlegion.com/29262/searching-a-billion-records-a-field-manual-for-frontend-ninjas</link>
<description>&lt;p&gt;Listen up, Ninjas.&lt;/p&gt;
&lt;p&gt;If your plan is to drag a billion usernames into the browser and filter() them like a street brawl in the town square, just sheath that idea. That is not stealth. That is yelling your location at the enemy camp while carrying a temple bell.&lt;/p&gt;
&lt;p&gt;A billion-row search is won in the shadows. The dojo rule is simple:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The server guards the treasure. The frontend throws one shuriken, then&lt;br&gt;
vanishes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You do not slay the whole army. You take the head that matters and you are gone before the lanterns flicker.&lt;/p&gt;
&lt;p&gt;First law of the clan: the haystack is not your backpack&lt;/p&gt;
&lt;p&gt;This scroll is forbidden:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;js

// Dishonor.

// Your tab will join its ancestors.

const matches = allUsernames.filter((name) =&amp;gt;

  name.startsWith(query)

)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A billion handles is not a list. It is a mountain. Stuffing the mountain into RAM is how junior mercenaries become a loading spinner in the sky.&lt;/p&gt;
&lt;p&gt;The Ninja does not carry the mountain. The Ninja asks the mountain a tiny question.&lt;/p&gt;
&lt;p&gt;Know your quarry&lt;/p&gt;
&lt;p&gt;Instagram-style handles are clean targets. Unique. Short. Prefix-shaped.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;habeeb&lt;/li&gt;
&lt;li&gt;&lt;p&gt;habeeb_lateef&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;habitat.ng&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;habibi.eats&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The civilian types hab and expects a whisper of names, exact blade first. They do not want you to recite the census.&lt;/p&gt;
&lt;p&gt;Honorable question&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Names that start with hab. Eight only. If hab itself lives, put it on the throne.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Dishonorable question&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Walk every soul in the empire and see if hab hides in their robes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Prefix plus a limit is a surgical strike when the store has an index. “Contains” on a billion rows is a siege. Ninjas do not lay sieges. That is army work. Leave it.&lt;/p&gt;
&lt;p&gt;The silent kata (what your UI actually does)&lt;/p&gt;
&lt;p&gt;keystroke → hide in the reeds → kill the old request → ask for a prefix → paint eight shadows&lt;/p&gt;
&lt;p&gt;Four techniques. Master them and you look instant. Miss them and you look like a tourist with cymbals.&lt;/p&gt;
&lt;p&gt; Technique 1: Patience of the reed — debounce&lt;/p&gt;
&lt;p&gt;Civilians tap h a b e e b like rain on a tin roof. If you leap at every drop, you are not a Ninja. You are a doorbell.&lt;/p&gt;
&lt;p&gt;Wait until the rain pauses. Then strike once.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;js

function debounce(fn, ms) {

  let smokeBomb

  return (...args) =&amp;gt; {

    clearTimeout(smokeBomb)

    smokeBomb = setTimeout(() =&amp;gt; fn(...args), ms)

  }

}

const hunt = debounce((query) =&amp;gt; {

  loadSuggestions(query)

}, 200)

input.addEventListener(&quot;input&quot;, (e) =&amp;gt; {

  hunt(e.target.value.trim().toLowerCase())

})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
200ms is a decent crouch. Too eager and you spam the dojo gates. Too sleepy and the user thinks you died behind the curtain.

Also: do not unsheathe the blade for one letter. h is not a mission. It is a rumor.

```js

if (query.length &amp;lt; 2) {

  renderEmpty(&quot;Keep typing, Ninja. The shadows need more letters.&quot;)

  return

}
Technique 2: Assassinate yesterday’s mission — abort

You sent a crow for hab. The civilian already typed habeeb. If the old crow lands last, your dropdown lies. Lying is for politicians. Ninjas deal in current intel.

js

let livingCrow = null

async function loadSuggestions(query) {

  if (livingCrow) livingCrow.abort() // one puff of smoke. gone.

  const controller = new AbortController()

  livingCrow = controller

  const url = /api/usernames?prefix=${encodeURIComponent(query)}&amp;amp;limit=8

  try {

    const res = await fetch(url, { signal: controller.signal })

    const data = await res.json()

    // The box may have changed while you were in the rafters

    if (query !== input.value.trim().toLowerCase()) return

    renderList(data.results)

  } catch (err) {

    if (err.name === &quot;AbortError&quot;) return // honorable death

    renderEmpty(&quot;The crow never returned.&quot;)

  }

}
Clan proverb:

&amp;gt; The latest query wears the crown. Late mail is not wisdom. It is a ghost.

Technique 3: Pocket scrolls — cache

You already asked for hab ten seconds ago. Asking again is not diligence. It is amnesia.

js

const sleeve = new Map() // query → { inkedAt, results }

const FRESHNESS = 30_000

function peekScroll(query) {

  const scroll = sleeve.get(query)

  if (!scroll) return null

  if (Date.now() - scroll.inkedAt &amp;gt; FRESHNESS) {

    sleeve.delete(query)

    return null

  }

  return scroll.results

}

async function loadSuggestions(query) {

  const remembered = peekScroll(query)

  if (remembered) {

    renderList(remembered) // silent win

    return

  }

  // fetch, then tuck the answer into the sleeve

  sleeve.set(query, { inkedAt: Date.now(), results: data.results })

}
Cache the empty caves too. “Nobody named xyzzy99” is still intel. Do not kick the same hollow tree twice.

Keep the sleeve small. Fifty missions, then forget the oldest. You are hoarding whispers, not the imperial archive.

Technique 4: Eight shadows, not eight thousand

Your API should look like a note slipped under a door:

http

GET /api/usernames?prefix=hab&amp;amp;limit=8

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;
{

  &quot;query&quot;: &quot;hab&quot;,

  &quot;results&quot;: [

    { &quot;username&quot;: &quot;habeeb&quot;, &quot;exact&quot;: true },

    { &quot;username&quot;: &quot;habeeb_lateef&quot; },

    { &quot;username&quot;: &quot;habitat&quot; },

    { &quot;username&quot;: &quot;habitat.ng&quot; },

    { &quot;username&quot;: &quot;habibi.eats&quot; }

  ]

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you paint only what the night gave you:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;
function renderList(results) {

  list.innerHTML = &quot;&quot;

  if (results.length === 0) {

    list.textContent = &quot;No clan member starts with that name.&quot;

    return

  }

  for (const row of results) {

    const item = document.createElement(&quot;li&quot;)

    item.textContent = &quot;@&quot; + row.username

    if (row.exact) item.dataset.soke = &quot;true&quot; // the true heir

    list.appendChild(item)

  }

}
Eight &amp;lt;li&amp;gt;s is a tea ceremony. A thousand DOM nodes is a parade. Virtualize only if some sensei demands an admin scroll that can grow. Even then: pages from the server, never “summon everyone.”

What the mountain does while you crouch (know your allies)

You do not build this in the browser. You should still understand why the strike is cheap.

Sorted names are a line of soldiers in alphabetical armor:

gabe
habitat

habitat.ng

habeeb

habeeb_lateef

habibi.eats

hackclub



“Starts with hab” is not “interview every soldier.” It is:

1. Leap to the first name ≥ hab (the index is your grappling hook).

2. Walk forward while the name still bows to hab.

3. Stop at eight. Disappear.

A trie is a tree of letters. h → a → b and you are already in the right branch. The rest of the forest never hears your footsteps.

Exact habeeb is even quieter: one hash, one kill, go home.

So the frontend’s job is not “be a database.” It is “throw a prefix, not a novel.

Two missions, two blades

The dropdown is reconnaissance. Opening a profile is the raid.

js

// Smoke bomb: names only

GET /api/usernames?prefix=hab&amp;amp;limit=8

// Katana: one soul, after they choose

GET /api/users/habeeb

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do not strap a biography, a portrait gallery, and a follower census to every suggestion. Fat payloads are clanking armor. Ninjas wear black for a reason.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;
list.addEventListener(&quot;click&quot;, (e) =&amp;gt; {

  const name = e.target.textContent.replace(/^@/, &quot;&quot;)

  window.location.href = &quot;/u/&quot; + name

})

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;rank-the-squad-you-have-not-the-empire&quot;&gt;Rank the squad you have, not the empire&lt;/h3&gt;
&lt;p&gt;Once eight names kneel before you, you may sort that squad:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;
function sortSuggestions(query, rows) {

  return [...rows].sort((a, b) =&amp;gt; {

    if (a.username === query) return -1

    if (b.username === query) return 1

    return a.username.length - b.username.length

  })

}
Exact match on the throne. Short names next. Popularity if the API handed you a score.

Fuzzy “did you mean habeeb?” is dessert on those eight plates. Running Levenshtein across a billion rows in the tab is how you become a cautionary campfire story.

Empty air is still a message

js

function decideWhatToShow(query) {

  if (query.length === 0) return &quot;recent&quot;  // local ghosts

  if (query.length &amp;lt; 2)  return &quot;hint&quot;    // “more letters, traveler”

  return &quot;network&quot;

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Recent hunts live in the village, not the mountain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;
const recent = JSON.parse(localStorage.getItem(&quot;recentHandles&quot;) || &quot;[]&quot;)

function remember(username) {

  const next = [username, ...recent.filter((x) =&amp;gt; x !== username)].slice(0, 5)

  localStorage.setItem(&quot;recentHandles&quot;, JSON.stringify(next))

}


Five names in localStorage. That is a pocket. That is allowed.

The full night raid (assemble the forms)

js

const sleeve = new Map()
let livingCrow = null
const onType = debounce((raw) =&amp;gt; {
  const query = raw.trim().toLowerCase()
  if (query.length &amp;lt; 2) {
    if (livingCrow) livingCrow.abort()
    renderHint(&quot;Two letters, Ninja. Then we hunt.&quot;)
    return
  }
  const scroll = sleeve.get(query)
  if (scroll &amp;amp;&amp;amp; Date.now() - scroll.inkedAt &amp;lt; 30_000) {
    renderList(scroll.results)
    return
  }
  loadSuggestions(query)
}, 200)
async function loadSuggestions(query) {
  if (livingCrow) livingCrow.abort()
  const controller = new AbortController()
  livingCrow = controller
  renderHint(&quot;Moving through the trees…&quot;)
  const url =
    /api/usernames?prefix=${encodeURIComponent(query)}&amp;amp;limit=8
  try {
    const res = await fetch(url, { signal: controller.signal })
    const data = await res.json()
    if (query !== currentQuery()) return
    sleeve.set(query, { inkedAt: Date.now(), results: data.results })
    renderList(data.results)
  } catch (err) {
    if (err.name !== &quot;AbortError&quot;) renderHint(&quot;The path is blocked.&quot;)
  }
} 
No mountain in the backpack. No honor lost.

Scroll of shame (do not)

| Tempting jutsu | Why the elders will sigh |

|---|---|

| Load all usernames locally | You become the mountain. The mountain wins. |

| Fetch on every key | You are a festival drum, not a Ninja. |

| Client-side %term% / regex over the world | Siege warfare. Resign. |

| Return 1,000 hits “just in case” | JSON avalanche. DOM parade. Shame. |

| Ignore abort | Ghost intel overwrites living intel. |

| Fuzzy-match the entire table in the tab | Your laptop achieves enlightenment, then death. |

Final proverb

A billion-row search is fast when the Ninja never meets a billion rows.

Crouch. Cancel. Ask for a prefix of eight names. Paint them. Disappear.

The index is the silent ally in the rafters. Your job is not to be louder than the database. Your job is to be narrower, later, and true.

Now go, Ninjas. Type hab. Take habeeb. Leave no footprints in the DOM.





&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29262/searching-a-billion-records-a-field-manual-for-frontend-ninjas</guid>
<pubDate>Wed, 30 Sep 2026 14:48:26 +0000</pubDate>
</item>
<item>
<title>Extracting a React presence hook from a production event app</title>
<link>https://coderlegion.com/29252/extracting-a-react-presence-hook-from-a-production-event-app</link>
<description>&lt;p&gt;&lt;em&gt;How we turned a battle-tested Supabase Realtime Presence layer into react-supabase-presence&lt;/em&gt;&lt;br&gt;
Real-time “who is in the room” sounds simple until you ship it on phones.&lt;/p&gt;
&lt;p&gt;Tabs freeze. Networks drop. React Strict Mode double-mounts effects. Supabase will often restore the socket for you — but presence track state does not magically re-announce itself. If you do not re-track after resubscribe, other clients still think you are offline.&lt;/p&gt;
&lt;p&gt;We hit that in production while building an event networking product (QR check-in, live roster, ephemeral rooms). The presence logic had to be honest in the UI: not a single boolean “connected,” but a small state machine the interface could show without lying.&lt;/p&gt;
&lt;p&gt;That code is now open source:&lt;br&gt;
npm: react-supabase-presence&lt;br&gt;
GitHub: devhabeeblateef/react-supabase-presence&lt;br&gt;
License: MIT&lt;/p&gt;
&lt;p&gt;The problem we actually had&lt;br&gt;
Supabase Realtime Presence is a good primitive. The gaps show up at the edges:&lt;/p&gt;
&lt;p&gt;Lifecycle — subscribe → track → sync/join/leave. Miss a re-track after reconnect and your user vanishes for everyone else.&lt;br&gt;
Mobile browsers — background tabs can freeze sockets without a clean CLOSED event. Coming back to the tab should re-announce.&lt;br&gt;
UI honesty — “loading” vs “live” vs “trying again” are different states. Collapsing them into one spinner trains users to ignore the product.&lt;/p&gt;
&lt;p&gt;App boundaries — roster, roles, and business logic should not live inside the presence hook. The hook should only answer: who is online, and is the channel healthy?&lt;/p&gt;
&lt;p&gt;We implemented that as a React hook inside the product first, then extracted it so the package has zero knowledge of events, QR codes, or billing.&lt;/p&gt;
&lt;p&gt;What the hook does&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;`import { useEventPresence } from &quot;react-supabase-presence&quot;;
import type { EventPresence, PresenceStatus } from &quot;react-supabase-presence&quot;;
TypeScriptconst { online, status, version } = useEventPresence(
  supabase,   // your browser Supabase client
  channelKey, // stable room / event id
  selfId      // stable presence key for the current user
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Behaviour that mattered in production&lt;/p&gt;
&lt;p&gt;Channel name is derived as presence:${channelKey} with config.presence.key = selfId.&lt;br&gt;
On SUBSCRIBED, the hook tracks { user_id, at }.&lt;br&gt;
On CHANNEL_ERROR / TIMED_OUT / CLOSED, status becomes reconnecting (the client library may still recover underneath).&lt;br&gt;
On visibilitychange → visible, it re-tracks so a returning mobile tab reappears.&lt;/p&gt;
&lt;p&gt;Cleanup removes the channel; no shared global channel name that breaks under Strict Mode remounts.&lt;/p&gt;
&lt;p&gt;You own the SupabaseClient. The library does not create clients, read env vars, or assume your schema.&lt;/p&gt;
&lt;p&gt;Minimal example&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;`&quot;use client&quot;;
import { createClient } from &quot;@supabase/supabase-js&quot;;
import { useEventPresence } from &quot;react-supabase-presence&quot;;

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

export function Room({ roomId, userId }: { roomId: string; userId: string }) {
  const { online, status, version } = useEventPresence(
    supabase,
    roomId,
    userId
  );

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;Status: {status}&amp;lt;/p&amp;gt;
      &amp;lt;p&amp;gt;
        {online.size} online (v{version})
      &amp;lt;/p&amp;gt;
      &amp;lt;ul&amp;gt;
        {[...online].map((id) =&amp;gt; (
          &amp;lt;li key={id}&amp;gt;{id}&amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install: npm install react-supabase-presence&lt;br&gt;
Peer dependencies: react (≥18) and @supabase/supabase-js (^2.40).&lt;/p&gt;
&lt;p&gt;Design choices (and non-goals)&lt;br&gt;
In scope for v0.1&lt;/p&gt;
&lt;p&gt;Presence subscribe + track + state aggregation&lt;br&gt;
Explicit connection state for UI&lt;br&gt;
Tab-visible re-track&lt;br&gt;
TypeScript types exported from the package root&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Out of scope (on purpose)&lt;/strong&gt;&lt;br&gt;
Authentication&lt;br&gt;
Authorization / RLS policy design&lt;br&gt;
Handshakes, chat, or matching&lt;br&gt;
Rendering components (dots, avatars, lists)&lt;/p&gt;
&lt;p&gt;Those belong in the product. Keeping them out is what makes the package reusable and reviewable.&lt;br&gt;
The product that motivated this work still uses the published package for its live room roster — same API, one implementation path.&lt;/p&gt;
&lt;p&gt;Why open source this slice&lt;br&gt;
Presence is a sharp edge many Supabase + React apps re-implement poorly. Publishing a small, MIT-licensed hook:&lt;/p&gt;
&lt;p&gt;Documents the reconnect and visibility behaviour in public&lt;br&gt;
Gives others a starting point that is not tied to one startup’s domain model&lt;br&gt;
Forces a clean boundary: if it needs your event table, it does not belong in the library&lt;/p&gt;
&lt;p&gt;If you find bugs or want a multi-tab leader election helper, issues and PRs are welcome on the repo.&lt;/p&gt;
&lt;p&gt;Links&lt;br&gt;
npm: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-supabase-presence&quot;&gt;https://www.npmjs.com/package/react-supabase-presence&lt;/a&gt;&lt;br&gt;
Source: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/devhabeeblateef/react-supabase-presence&quot;&gt;https://github.com/devhabeeblateef/react-supabase-presence&lt;/a&gt;&lt;br&gt;
License: MIT&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29252/extracting-a-react-presence-hook-from-a-production-event-app</guid>
<pubDate>Wed, 30 Sep 2026 14:10:55 +0000</pubDate>
</item>
<item>
<title>Beyond a Country Explorer: Accessibility &amp; Architecture in React</title>
<link>https://coderlegion.com/29012/beyond-a-country-explorer-accessibility-architecture-in-react</link>
<description>&lt;p&gt;A country explorer can look like a simple React project: fetch some data, render a list of countries, and add a search field.&lt;/p&gt;
&lt;p&gt;But building a frontend that remains &lt;strong&gt;accessible, predictable, and maintainable&lt;/strong&gt; requires thinking beyond the visual interface.&lt;/p&gt;
&lt;p&gt;Here is how I structured my Rest Countries application with a focus on component boundaries, user interaction, and accessibility.&lt;/p&gt;
&lt;p&gt;⚡ &lt;strong&gt;The Architecture Highlights:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Feature-Oriented Structure:&lt;/strong&gt; Country-related functionality is organized around the feature itself, separating the data layer, application logic, and UI components instead of grouping everything by file type.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom State Logic:&lt;/strong&gt; Country filtering and selection are handled through a dedicated &lt;code&gt;useCountries&lt;/code&gt; hook, keeping stateful behavior separate from presentation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keyboard-Accessible Region Selection:&lt;/strong&gt; The custom region selector supports keyboard interaction including &lt;code&gt;ArrowUp&lt;/code&gt;, &lt;code&gt;ArrowDown&lt;/code&gt;, &lt;code&gt;Home&lt;/code&gt;, &lt;code&gt;End&lt;/code&gt;, and &lt;code&gt;Escape&lt;/code&gt;, providing an interaction model closer to a native control.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Focus Management:&lt;/strong&gt; Focus is deliberately managed when navigating between the country list and country details. When returning from a country detail view, focus can be restored to the element that originally triggered the navigation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Semantic Interaction:&lt;/strong&gt; Interactive elements use appropriate HTML semantics and accessible labels rather than relying only on visual styling to communicate their purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Controlled UI Boundaries:&lt;/strong&gt; The country grid, controls, and country detail view have separate responsibilities, making the interface easier to reason about and modify.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Intentional Interaction Design:&lt;/strong&gt; Country information remains selectable and usable as text, while the country flag acts as the primary interactive target for opening details. This avoids turning an entire information card into an unnecessarily large interactive control.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; &lt;strong&gt;The Result:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A React country explorer designed not only to display data, but to provide a more predictable experience for keyboard users while maintaining clear boundaries between state, UI, and interaction logic.&lt;/p&gt;
&lt;p&gt;The project is built with accessibility as part of the architecture rather than as a final visual polish step.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://ahmed-rest-countries.vercel.app/&quot;&gt;https://ahmed-rest-countries.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;GitHub Repo:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/a-sayed123/Rest_Countries_API&quot;&gt;https://github.com/a-sayed123/Rest_Countries_API&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;react-accessibility-frontendarchitecture-webdevelopment-cleancode-a11y&quot;&gt;React #Accessibility #FrontendArchitecture #WebDevelopment #CleanCode #A11y&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29012/beyond-a-country-explorer-accessibility-architecture-in-react</guid>
<pubDate>Tue, 29 Sep 2026 08:01:34 +0000</pubDate>
</item>
<item>
<title>Forgetting default.tsx in a Next.js Parallel Route Can 404 Your Entire Page, Not Just One Slot</title>
<link>https://coderlegion.com/28790/forgetting-default-tsx-in-next-js-parallel-route-can-404-your-entire-page-not-just-one-slot</link>
<description>&lt;p&gt;This is a genuinely surprising failure mode the first time you hit it, since the natural assumption is that an unmatched parallel route slot would just render nothing, not take down a page that has nothing to do with the slot in question.&lt;/p&gt;
&lt;h3 id=&quot;the-setup-from-an-earlier-post-extended&quot;&gt;The Setup From an Earlier Post, Extended&lt;/h3&gt;
&lt;p&gt;Building on the intercepting route modal pattern from an earlier post, the &lt;code&gt;@modal&lt;/code&gt; slot renders the modal when a specific route is intercepted. But a parallel route slot needs to render &lt;em&gt;something&lt;/em&gt; for every URL within its scope, not just the one specific route it was built for.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app/
├── @modal/
│   ├── (.)photo/
│   │   └── [id]/
│   │       └── page.tsx    // renders when a photo route is intercepted
│   └── (missing: default.tsx)
├── photo/
│   └── [id]/
│       └── page.tsx
├── settings/
│   └── page.tsx             // a completely unrelated route
├── layout.tsx
└── page.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;why-visiting-an-unrelated-page-can-break-entirely&quot;&gt;Why Visiting an Unrelated Page Can Break Entirely&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;@modal&lt;/code&gt; slot is defined at the root layout level here, which means it's part of every route rendered under that layout, not just the photo routes it was actually built for. Next.js needs to know what &lt;code&gt;@modal&lt;/code&gt; should render for every single route within its scope, including &lt;code&gt;/settings&lt;/code&gt;, which has nothing to do with photos or modals at all. If there's no &lt;code&gt;page.tsx&lt;/code&gt; inside &lt;code&gt;@modal&lt;/code&gt; matching &lt;code&gt;/settings&lt;/code&gt;, and critically, no &lt;code&gt;default.tsx&lt;/code&gt; file to serve as a fallback for exactly this situation, Next.js has no content to render for that slot on that route, and without a fallback, it treats this as a genuine 404 for the entire page, not just an empty modal slot.&lt;/p&gt;
&lt;h3 id=&quot;why-this-specific-failure-mode-is-so-confusing&quot;&gt;Why This Specific Failure Mode Is So Confusing&lt;/h3&gt;
&lt;p&gt;The intercepting modal pattern itself works perfectly, clicking a photo from the grid correctly shows the modal, refreshing correctly shows the full page, exactly the behavior covered in the earlier post. The &lt;code&gt;/settings&lt;/code&gt; page, something that looks completely unrelated to any of that photo and modal logic, is what actually breaks, with a full 404, and nothing about that page's own code gives any indication why. The actual cause lives entirely in the parallel route configuration at the layout level, several files away from the page that's actually failing, which makes this a genuinely disorienting bug to trace the first time.&lt;/p&gt;
&lt;h3 id=&quot;the-fix-a-default-tsx-for-every-parallel-slot&quot;&gt;The Fix: A default.tsx for Every Parallel Slot&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/@modal/default.tsx
export default function Default() {
  return null; // this slot simply renders nothing when there's no active modal
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;default.tsx&lt;/code&gt; tells Next.js exactly what to render for this slot whenever the current route doesn't match anything more specific within it. Returning &lt;code&gt;null&lt;/code&gt; here is completely correct and intentional, for any route that isn't specifically showing a modal, &lt;code&gt;@modal&lt;/code&gt; should render nothing, and &lt;code&gt;default.tsx&lt;/code&gt; is what makes that an explicit, defined behavior instead of an unhandled gap that produces a full 404.&lt;/p&gt;
&lt;h3 id=&quot;why-this-is-easy-to-miss-when-first-setting-up-parallel-routes&quot;&gt;Why This Is Easy to Miss When First Setting Up Parallel Routes&lt;/h3&gt;
&lt;p&gt;Most tutorials and examples for intercepting routes focus entirely on the specific route being intercepted, since that's the actual feature being demonstrated, and &lt;code&gt;default.tsx&lt;/code&gt; is easy to treat as an afterthought or skip in a quick example, since the demo itself often only ever navigates between the two or three routes that were actually set up correctly. The gap only becomes visible once someone navigates to a genuinely unrelated route that was never part of the original example, exactly the kind of route a real, full application has plenty of.&lt;/p&gt;
&lt;h3 id=&quot;the-broader-rule&quot;&gt;The Broader Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Every parallel route slot needs a &lt;code&gt;default.tsx&lt;/code&gt;, without exception, the moment that slot exists anywhere in a layout that also renders routes the slot has no specific content for.&lt;/strong&gt; This isn't an edge case fallback for rare situations, it's the default, expected behavior for the overwhelming majority of routes in any real application, since a slot built for one specific interaction, a modal, a sidebar variant, is inherently the exception, not the rule, across a site's full set of routes.&lt;/p&gt;
&lt;h3 id=&quot;checking-your-own-parallel-routes&quot;&gt;Checking Your Own Parallel Routes&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find app -type d -name &quot;@*&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For every parallel route slot this turns up, confirm a &lt;code&gt;default.tsx&lt;/code&gt; exists directly inside it. If it doesn't, any route within that layout's scope that isn't specifically handled by that slot is at genuine risk of a full 404, not a minor rendering gap, the moment someone actually navigates there.&lt;/p&gt;
&lt;p&gt;I set up a &lt;code&gt;default.tsx&lt;/code&gt; as the very first file whenever I add a parallel route slot to any project now, client work included, for exactly this reason, it costs one trivial file and prevents a genuinely confusing, hard-to-trace production bug. If you're exploring more Next.js patterns like this, I write about them regularly at &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pixelanas.com/blog&quot;&gt;pixelanas.com/blog&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you're using parallel routes anywhere, run the check above right now and confirm every slot has its fallback file. If one doesn't, you've got a real, ready-to-happen 404 waiting on some route you haven't tested yet. Drop what you find in the comments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28790/forgetting-default-tsx-in-next-js-parallel-route-can-404-your-entire-page-not-just-one-slot</guid>
<pubDate>Sun, 27 Sep 2026 09:32:05 +0000</pubDate>
</item>
<item>
<title>Why Your useEffect Runs Twice in Development (And Why You Shouldn't &quot;Fix&quot; It)</title>
<link>https://coderlegion.com/28704/why-your-useeffect-runs-twice-in-development-and-why-you-shouldnt-fix-it</link>
<description>&lt;p&gt;This causes a very specific, very common reaction, a developer sees a &lt;code&gt;console.log&lt;/code&gt; inside &lt;code&gt;useEffect&lt;/code&gt; fire twice in the browser console during local development, assumes something's genuinely broken, and starts debugging a bug that doesn't actually exist.&lt;/p&gt;
&lt;h3 id=&quot;what-s-actually-happening&quot;&gt;What's Actually Happening&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;'use client';
import { useEffect } from 'react';

export function Tracker() {
  useEffect(() =&amp;gt; {
    console.log('Effect ran'); // logs twice in development, once in production
    trackPageView();
  }, []);

  return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In development, with React's Strict Mode enabled, which the Next.js App Router enables by default, React deliberately mounts a component, runs its effects, unmounts it, and immediately remounts it again, running the effects a second time. This is not a bug, and it's not accidental, it's a specific, intentional development-only behavior designed to help surface exactly the kind of bug this pattern causes in the first place, effects with real, unintended side effects or missing cleanup.&lt;/p&gt;
&lt;h3 id=&quot;why-react-does-this-on-purpose&quot;&gt;Why React Does This on Purpose&lt;/h3&gt;
&lt;p&gt;React is preparing for a future where components can be safely paused, discarded, and remounted, for features like offline support and certain rendering optimizations, and that capability genuinely requires effects to be safely re-runnable, mount, unmount, mount again, without producing broken or duplicated behavior. The double-invoke in development is a deliberate stress test, if your effect breaks when run twice in a row, mount, unmount, mount, it's revealing a genuine correctness gap, code that assumes it only ever runs once, which was never actually a safe assumption to begin with, even before Strict Mode made it visible.&lt;/p&gt;
&lt;h3 id=&quot;why-the-instinct-to-fix-this-is-usually-wrong&quot;&gt;Why the Instinct to &quot;Fix&quot; This Is Usually Wrong&lt;/h3&gt;
&lt;p&gt;Seeing double logs or double network calls in development and reaching for a way to suppress it, disabling Strict Mode entirely, or adding a fragile &lt;code&gt;useRef&lt;/code&gt; guard specifically to prevent the second invocation, treats the symptom as the problem. The actual problem, if there is one, is that the effect wasn't written to safely handle being cleaned up and re-run, which is a real gap worth fixing properly, not hiding by preventing Strict Mode from ever revealing it again.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// ❌ A common &quot;fix&quot; that just hides the signal instead of addressing what it revealed
const hasRun = useRef(false);

useEffect(() =&amp;gt; {
  if (hasRun.current) return;
  hasRun.current = true;
  trackPageView();
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern specifically defeats the entire point of Strict Mode's double-invoke check, and worse, it can behave inconsistently across React versions and concurrent rendering scenarios in ways that are genuinely harder to reason about than just writing the effect correctly in the first place.&lt;/p&gt;
&lt;h3 id=&quot;the-actual-fix-write-effects-that-are-safe-to-run-twice&quot;&gt;The Actual Fix: Write Effects That Are Safe to Run Twice&lt;/h3&gt;
&lt;p&gt;For something like an analytics call, the real question worth asking is whether firing it twice in development actually matters, and for most analytics providers, it doesn't, in production, without Strict Mode's double-invoke, it only fires once anyway. The development-only double log is not the same as double-counting real production analytics data, a distinction worth confirming directly rather than assuming.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&amp;gt; {
  // In production, this fires once. In development, Strict Mode fires it
  // twice, which is expected and does not reflect production behavior.
  trackPageView();
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For an effect that sets up something genuinely stateful, a subscription, an interval, an event listener, the actual fix is a real, correct cleanup function, which is precisely what Strict Mode's double-invoke is designed to verify you've written:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&amp;gt; {
  const interval = setInterval(() =&amp;gt; {
    checkForUpdates();
  }, 5000);

  return () =&amp;gt; clearInterval(interval); // correctly cleans up, safe to run twice
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With a correct cleanup function, the double-invoke in development runs cleanly, set up, tear down, set up again, exactly the pattern Strict Mode exists to confirm your code actually handles safely. If removing the cleanup function causes something to visibly break or duplicate in development, that's Strict Mode doing its job, not React misbehaving.&lt;/p&gt;
&lt;h3 id=&quot;where-this-actually-matters-for-real-bugs-not-just-console-noise&quot;&gt;Where This Actually Matters for Real Bugs, Not Just Console Noise&lt;/h3&gt;
&lt;p&gt;This connects directly to the stale fetch race condition covered in an earlier post, an effect fetching data without a cleanup function that ignores stale responses is exactly the kind of effect that also breaks under Strict Mode's double-invoke in development, showing a flash of duplicated or incorrect state before settling, which is a genuine, useful early warning for a bug that would otherwise only surface in production under real network timing variance.&lt;/p&gt;
&lt;h3 id=&quot;the-actual-rule-1&quot;&gt;The Actual Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;A &lt;code&gt;useEffect&lt;/code&gt; that produces visibly different or broken behavior when Strict Mode runs it twice in development almost always has a real gap, missing or incorrect cleanup, not properly guarding against being re-invoked, that would eventually cause a genuine bug in production too, just under different, harder-to-reproduce conditions.&lt;/strong&gt; The fix is writing the effect to genuinely handle mount, unmount, remount correctly, not suppressing the specific mechanism designed to reveal that it doesn't.&lt;/p&gt;
&lt;p&gt;I keep Strict Mode enabled across every project I build, client work and the templates at &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pixelanas.com&quot;&gt;pixelanas.com&lt;/a&gt; alike, specifically because catching this category of bug in development, as annoying as the double logs are at first, is far cheaper than debugging the same underlying issue once it surfaces as an intermittent, hard-to-reproduce production bug instead.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you've disabled Strict Mode, or added a ref-based guard specifically to suppress double-invoked effects, worth reconsidering whether that's hiding a real, fixable gap rather than actually solving anything. Drop your own experience with this in the comments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28704/why-your-useeffect-runs-twice-in-development-and-why-you-shouldnt-fix-it</guid>
<pubDate>Sat, 26 Sep 2026 09:20:46 +0000</pubDate>
</item>
<item>
<title>🔥 30 Days of Frontend — Day 2: React Reconciliation</title>
<link>https://coderlegion.com/28635/30-days-of-frontend-day-2-react-reconciliation</link>
<description>&lt;p&gt;Why doesn't React update the entire UI after every state change?&lt;/p&gt;
&lt;p&gt;Because React uses &lt;strong&gt;reconciliation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When a component renders again, React produces a new element tree.&lt;/p&gt;
&lt;p&gt;React then determines what changed compared with the previous result and prepares the necessary updates.&lt;/p&gt;
&lt;p&gt;Think of it as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Previous tree + New tree
          ↓
    Reconciliation
          ↓
   Required updates
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;-a-simple-example&quot;&gt;🧠 A Simple Example&lt;/h3&gt;
&lt;p&gt;Imagine we have a list of todos:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;function TodoList({ todos }) {
  return (
    &amp;lt;ul&amp;gt;
      {todos.map((todo) =&amp;gt; (
        &amp;lt;TodoItem key={todo.id} todo={todo} /&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Initially, our list looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[A, B, C]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then it changes to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[A, B, D]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because we're using stable keys, React can reason about the identity of each item:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A → same item
B → same item
C → removed
D → new item
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part isn't just that the data changed.&lt;/p&gt;
&lt;p&gt;It's that React can understand &lt;strong&gt;which elements represent the same identity across renders&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;-why-do-keys-matter-&quot;&gt;🔑 Why Do Keys Matter?&lt;/h3&gt;
&lt;p&gt;Consider this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;items.map((item) =&amp;gt; (
  &amp;lt;Item key={Math.random()} /&amp;gt;
))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every render produces a different key.&lt;/p&gt;
&lt;p&gt;That makes it difficult for React to identify the item as the same item from the previous render.&lt;/p&gt;
&lt;p&gt;Instead, use a stable identifier:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;items.map((item) =&amp;gt; (
  &amp;lt;Item key={item.id} /&amp;gt;
))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now React has a stable way to understand:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;This is the same item as before.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This becomes especially important when components contain local state.&lt;/p&gt;
&lt;p&gt;For example, if each &lt;code&gt;Item&lt;/code&gt; contains an input field, unstable keys can cause component instances to be recreated and lead to unexpected state behavior.&lt;/p&gt;
&lt;h3 id=&quot;-a-useful-mental-model&quot;&gt;🧩 A Useful Mental Model&lt;/h3&gt;
&lt;p&gt;When debugging list rendering, don't only ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Why did this component render?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Also ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Does React know this is the same component as before?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That question can help explain many confusing behaviors involving:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;List keys&lt;/li&gt;
&lt;li&gt;Component identity&lt;/li&gt;
&lt;li&gt;Local state&lt;/li&gt;
&lt;li&gt;Re-renders&lt;/li&gt;
&lt;li&gt;Elements being added or removed&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-where-does-reconciliation-happen-&quot;&gt;⚙️ Where Does Reconciliation Happen?&lt;/h3&gt;
&lt;p&gt;Reconciliation happens during React's &lt;strong&gt;render phase&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;React determines what work needs to be done first. The resulting changes are then applied during the &lt;strong&gt;commit phase&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A simplified mental model is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;State Update
     ↓
Render
     ↓
Reconciliation
     ↓
Determine required changes
     ↓
Commit
     ↓
DOM updates
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's important to remember that rendering does not mean React immediately changes the DOM.&lt;/p&gt;
&lt;p&gt;The render phase is where React figures out what the next UI should look like.&lt;/p&gt;
&lt;p&gt;The commit phase is where React applies the necessary changes to the DOM.&lt;/p&gt;
&lt;h3 id=&quot;-the-takeaway&quot;&gt;🎯 The Takeaway&lt;/h3&gt;
&lt;p&gt;Reconciliation is one of the core ideas behind how React updates the UI efficiently.&lt;/p&gt;
&lt;p&gt;Once you understand reconciliation, concepts like keys, component identity, and rendering behavior become much easier to reason about.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/28635/30-days-of-frontend-day-2-react-reconciliation</guid>
<pubDate>Fri, 25 Sep 2026 17:17:09 +0000</pubDate>
</item>
<item>
<title>Your useEffect Data Fetch Might Show Stale Data If a User Navigates Quickly</title>
<link>https://coderlegion.com/28607/your-useeffect-data-fetch-might-show-stale-data-if-a-user-navigates-quickly</link>
<description>&lt;p&gt;This is a genuinely common race condition, and it's specifically the kind that only shows up under real usage conditions, someone clicking between items quickly, a slow network, exactly the situations casual development testing on a fast local connection rarely reproduces.&lt;/p&gt;
&lt;h3 id=&quot;the-setup-that-looks-completely-standard&quot;&gt;The Setup That Looks Completely Standard&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;'use client';
import { useState, useEffect } from 'react';

export function UserDetail({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);

  useEffect(() =&amp;gt; {
    fetch(`/api/users/${userId}`)
      .then((res) =&amp;gt; res.json())
      .then((data) =&amp;gt; setUser(data));
  }, [userId]);

  return &amp;lt;div&amp;gt;{user ? user.name : 'Loading...'}&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is close to the most common way to fetch data based on a changing prop in a Client Component, and it works completely correctly the vast majority of the time. It also has a real race condition that only shows up under a specific, genuinely common sequence of events.&lt;/p&gt;
&lt;h3 id=&quot;the-exact-sequence-that-triggers-the-bug&quot;&gt;The Exact Sequence That Triggers the Bug&lt;/h3&gt;
&lt;p&gt;A user clicks on user A in a list, &lt;code&gt;userId&lt;/code&gt; becomes A's ID, the effect fires, a request for A's data starts. Before that request resolves, network latency, a slow connection, the user clicks user B instead, &lt;code&gt;userId&lt;/code&gt; becomes B's ID, the effect fires again, a second, separate request for B's data starts. Now two requests are in flight simultaneously.&lt;/p&gt;
&lt;p&gt;If B's request happens to resolve faster than A's, perfectly plausible depending on server load, caching, or just network variance, B's data renders correctly first. Then A's slower, now-stale request finally resolves, and its &lt;code&gt;.then()&lt;/code&gt; callback runs &lt;code&gt;setUser(data)&lt;/code&gt; with A's data, overwriting B's correct, current data with A's outdated, no-longer-relevant response. The component now displays user A's name and information while &lt;code&gt;userId&lt;/code&gt; itself is actually set to B, a genuine, visible mismatch between what's shown and what's actually selected.&lt;/p&gt;
&lt;h3 id=&quot;why-this-feels-random-rather-than-reliably-broken&quot;&gt;Why This Feels Random Rather Than Reliably Broken&lt;/h3&gt;
&lt;p&gt;This only manifests when the specific timing lines up, a slower first request resolving after a faster second one. On a fast, consistent local connection during development, requests often resolve close enough to their sending order that this exact interleaving rarely happens, which is exactly why it's easy to ship without ever seeing it locally, and exactly why it tends to surface first as a confusing, hard-to-reproduce bug report from a real user on a real, more variable connection.&lt;/p&gt;
&lt;h3 id=&quot;the-actual-fix-ignore-stale-responses-explicitly&quot;&gt;The Actual Fix: Ignore Stale Responses Explicitly&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;'use client';
import { useState, useEffect } from 'react';

export function UserDetail({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);

  useEffect(() =&amp;gt; {
    let ignore = false;

    fetch(`/api/users/${userId}`)
      .then((res) =&amp;gt; res.json())
      .then((data) =&amp;gt; {
        if (!ignore) {
          setUser(data);
        }
      });

    return () =&amp;gt; {
      ignore = true; // marks this specific effect's request as stale on cleanup
    };
  }, [userId]);

  return &amp;lt;div&amp;gt;{user ? user.name : 'Loading...'}&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;ignore&lt;/code&gt; flag, scoped locally to each individual effect invocation, gets set to &lt;code&gt;true&lt;/code&gt; in the cleanup function, which React runs automatically the moment &lt;code&gt;userId&lt;/code&gt; changes again, before the next effect invocation starts. When A's slow request finally resolves, its specific closure's &lt;code&gt;ignore&lt;/code&gt; flag is already &lt;code&gt;true&lt;/code&gt;, since the effect was cleaned up the moment the user clicked B, and the stale &lt;code&gt;setUser(data)&lt;/code&gt; call for A's outdated data simply never happens. Only B's request, whose &lt;code&gt;ignore&lt;/code&gt; flag remains &lt;code&gt;false&lt;/code&gt; because its effect was never cleaned up, actually updates state.&lt;/p&gt;
&lt;h3 id=&quot;an-alternative-using-abortcontroller&quot;&gt;An Alternative Using AbortController&lt;/h3&gt;
&lt;p&gt;For fetch specifically, &lt;code&gt;AbortController&lt;/code&gt; provides a more complete version of the same fix, actually canceling the in-flight request rather than just ignoring its eventual result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&amp;gt; {
  const controller = new AbortController();

  fetch(`/api/users/${userId}`, { signal: controller.signal })
    .then((res) =&amp;gt; res.json())
    .then((data) =&amp;gt; setUser(data))
    .catch((err) =&amp;gt; {
      if (err.name !== 'AbortError') {
        console.error(err); // genuinely handle real errors, ignore expected aborts
      }
    });

  return () =&amp;gt; {
    controller.abort();
  };
}, [userId]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This has a real advantage over the simple ignore flag, the actual network request gets canceled, not just its result discarded, which saves real bandwidth and server load for a request whose result was never going to be used anyway, particularly valuable for anything more expensive than a small JSON response.&lt;/p&gt;
&lt;h3 id=&quot;why-this-matters-more-for-search-and-filtering-specifically&quot;&gt;Why This Matters More for Search and Filtering Specifically&lt;/h3&gt;
&lt;p&gt;This exact pattern shows up constantly in search-as-you-type and rapid filtering interactions, exactly the kind of interface covered in an earlier post on search and filtering patterns. A user typing quickly triggers a new request on every keystroke, and without this protection, a slower response to an earlier, now-outdated keystroke can overwrite the correct results for what the user is actually searching for right now, displaying results for a query they've already moved past.&lt;/p&gt;
&lt;h3 id=&quot;the-actual-rule-2&quot;&gt;The Actual Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Any effect that fetches data based on a value that can change again before the fetch resolves needs a way to distinguish a stale, no-longer-relevant response from a current one.&lt;/strong&gt; A simple &lt;code&gt;ignore&lt;/code&gt; flag set in the cleanup function handles this cheaply for most cases. &lt;code&gt;AbortController&lt;/code&gt; handles it more completely, actually canceling the wasted request rather than just discarding its result, and is worth the small amount of extra code for anything with real cost behind each request, a genuinely expensive query, meaningful bandwidth, a rate-limited endpoint.&lt;/p&gt;
&lt;p&gt;I handle this exact pattern, mostly with &lt;code&gt;AbortController&lt;/code&gt; for anything backed by a real database query, across the dashboards and templates I build at &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pixelanas.com&quot;&gt;pixelanas.com&lt;/a&gt;, since it's exactly the kind of subtle bug that looks fine in every casual test and only shows up once real users start clicking around at real speed.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you've got a &lt;code&gt;useEffect&lt;/code&gt; fetching data based on a changing prop with no stale-response handling, go test it specifically by clicking or navigating quickly between a few items in a row. If you see a flash of the wrong data settling in, that's this exact race condition. Drop what you find in the comments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28607/your-useeffect-data-fetch-might-show-stale-data-if-a-user-navigates-quickly</guid>
<pubDate>Fri, 25 Sep 2026 13:18:13 +0000</pubDate>
</item>
<item>
<title>The Secret to Clean React Code: Mastering Component Composition</title>
<link>https://coderlegion.com/28593/the-secret-to-clean-react-code-mastering-component-composition</link>
<description>&lt;p&gt;If you have ever navigated a modern web application like Netflix or Spotify, you have experienced the magic of modular user interfaces. Under the hood, these interfaces are built using components—small, self-contained pieces of code that govern how a specific button, card, or menu looks and acts. However, as applications grow, keeping these components organized and easy to update becomes a massive challenge. This is where component composition comes into play.&lt;/p&gt;
&lt;p&gt;Component composition is a fundamental software design pattern where you build complex user interfaces by combining small, focused, and self-contained parts. Instead of creating a single, massive block of code that tries to handle every layout and interactive detail, you nest simpler components inside one another. In modern web frameworks like React, this means designing elements to be flexible containers that can accept and display any content passed directly into them.&lt;/p&gt;
&lt;p&gt;To understand this concept without writing a single line of code, think of a custom picture frame. The frame itself is a structured component. It has a physical border, a glass front, and a backing stand. Crucially, the frame manufacturer does not need to know what you plan to display inside it. You can insert a family photo, a watercolor painting, a motivational quote, or even a pressed leaf. The frame's only job is to provide the outer border and support whatever you place inside it. The frame &quot;composes&quot; the final product by holding whatever content you decide to supply.&lt;/p&gt;
&lt;p&gt;In everyday software engineering, developers often fall into a trap called &quot;prop drilling.&quot; &quot;Props&quot;—short for properties—are the configuration parameters passed from a parent component down to its children. When you have a deeply nested user interface, you might find yourself passing a user's login details through five intermediate layout elements that have absolutely no use for that information, just so a tiny avatar button at the very bottom can display the user's profile image.&lt;/p&gt;
&lt;p&gt;This practice makes software highly brittle. If you ever need to change the format of that user data, you have to edit and test all five intermediary files. Component composition prevents this by letting engineers build the nested element at a higher level and pass the completed element directly to its destination container. This prevents engineers from spending hours fixing broken data pipelines, minimizes merge conflicts in collaborative teams, and ensures that a simple design change in one part of the app does not accidentally crash another.&lt;/p&gt;
&lt;p&gt;Here is a simple example in React (a popular user interface library) demonstrating how to compose clean components using the special &lt;code&gt;children&lt;/code&gt; property, which acts as a slot for whatever content we want to place inside:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React from 'react';

// This structural container is completely decoupled from the data inside it
function CardContainer({ children }) {
  return (
    &amp;lt;div style={{ border: '1px solid #ccc', padding: '16px', borderRadius: '8px', margin: '10px' }}&amp;gt;
      {children}
    &amp;lt;/div&amp;gt;
  );
}

// We can reuse the exact same container for entirely different features
function App() {
  return (
    &amp;lt;main&amp;gt;
      {/* Use case 1: User Profile */}
      &amp;lt;CardContainer&amp;gt;
        &amp;lt;h3&amp;gt;User Profile&amp;lt;/h3&amp;gt;
        &amp;lt;p&amp;gt;Name: Alex Mercer&amp;lt;/p&amp;gt;
        &amp;lt;button&amp;gt;Edit Profile&amp;lt;/button&amp;gt;
      &amp;lt;/CardContainer&amp;gt;

      {/* Use case 2: Shopping Cart Item */}
      &amp;lt;CardContainer&amp;gt;
        &amp;lt;h3&amp;gt;Shopping Cart&amp;lt;/h3&amp;gt;
        &amp;lt;p&amp;gt;Item: Mechanical Keyboard&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;Price: $120&amp;lt;/p&amp;gt;
      &amp;lt;/CardContainer&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The major takeaway of component composition is that it shifts your mental model from creating all-in-one components to creating cooperative systems. By building layout shells that delegate their internal content to the parent that uses them, you create an adaptive, resilient codebase that can evolve effortlessly as your product requirements change.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/stop-building-monolithic-ui-guide-to.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28593/the-secret-to-clean-react-code-mastering-component-composition</guid>
<pubDate>Fri, 25 Sep 2026 09:06:38 +0000</pubDate>
</item>
<item>
<title>Building MetaMask Wallet Ownership Verification in MyZubster</title>
<link>https://coderlegion.com/28567/building-metamask-wallet-ownership-verification-in-myzubster</link>
<description>&lt;p&gt;Building MetaMask Wallet Ownership Verification in MyZubster&lt;/p&gt;
&lt;p&gt;I’ve started implementing MetaMask / EVM wallet ownership verification inside MyZubster.&lt;/p&gt;
&lt;p&gt;At first, this might sound like a simple “Connect Wallet” feature.&lt;/p&gt;
&lt;p&gt;But I wanted to avoid one of the most common mistakes in Web3 UX:&lt;/p&gt;
&lt;p&gt;treating “wallet connected” as if it automatically meant “identity verified”, “payment authorized”, or “transaction completed”.&lt;/p&gt;
&lt;p&gt;For MyZubster, those states need to stay separate.&lt;/p&gt;
&lt;p&gt;The architecture I’m building follows this rule:&lt;/p&gt;
&lt;p&gt;CONNECTED&lt;br&gt;
!=&lt;br&gt;
VERIFIED&lt;br&gt;
!=&lt;br&gt;
SIGNED&lt;br&gt;
!=&lt;br&gt;
PAID&lt;br&gt;
!=&lt;br&gt;
SETTLED&lt;/p&gt;
&lt;p&gt;That distinction is now becoming part of the product.&lt;/p&gt;
&lt;p&gt;What we already had&lt;/p&gt;
&lt;p&gt;MyZubster already had several EVM-related pieces in the codebase:&lt;/p&gt;
&lt;p&gt;ethers&lt;br&gt;
Ethereum Sepolia verification&lt;br&gt;
Base Sepolia anchoring&lt;br&gt;
Marketplace ETH support&lt;br&gt;
NFT experiments&lt;br&gt;
blockchain evidence flows&lt;/p&gt;
&lt;p&gt;What was missing was the user-side ownership layer.&lt;/p&gt;
&lt;p&gt;In other words:&lt;/p&gt;
&lt;p&gt;How can a logged-in MyZubster user prove that they actually control a specific Ethereum wallet?&lt;/p&gt;
&lt;p&gt;Without giving MyZubster the private key.&lt;/p&gt;
&lt;p&gt;Without sharing a seed phrase.&lt;/p&gt;
&lt;p&gt;Without sending ETH.&lt;/p&gt;
&lt;p&gt;Without paying gas.&lt;/p&gt;
&lt;p&gt;That became the first milestone.&lt;/p&gt;
&lt;p&gt;The new flow&lt;/p&gt;
&lt;p&gt;The flow now looks like this:&lt;/p&gt;
&lt;p&gt;Authenticated MyZubster user&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Connect MetaMask&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;eth_requestAccounts&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read wallet address&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read chain ID&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Request challenge from MyZubster&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server creates short-lived nonce&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;User signs message in MetaMask&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;personal_sign&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Signature sent to backend&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Backend recovers signer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Address matches?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WALLET_VERIFIED&lt;/p&gt;
&lt;p&gt;The important part is that verification happens server-side.&lt;/p&gt;
&lt;p&gt;The frontend cannot simply tell MyZubster:&lt;/p&gt;
&lt;p&gt;{&lt;br&gt;
  &quot;walletVerified&quot;: true&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;and be trusted.&lt;/p&gt;
&lt;p&gt;New API&lt;/p&gt;
&lt;p&gt;I added four authenticated endpoints:&lt;/p&gt;
&lt;p&gt;GET    /api/wallet/me&lt;br&gt;
POST   /api/wallet/challenge&lt;br&gt;
POST   /api/wallet/verify&lt;br&gt;
DELETE /api/wallet/disconnect&lt;/p&gt;
&lt;p&gt;Their responsibility is intentionally narrow.&lt;/p&gt;
&lt;p&gt;/challenge creates the ownership proof request.&lt;/p&gt;
&lt;p&gt;/verify validates the signature.&lt;/p&gt;
&lt;p&gt;/me returns the current wallet relationship.&lt;/p&gt;
&lt;p&gt;/disconnect removes the verified association from the MyZubster account.&lt;/p&gt;
&lt;p&gt;The challenge&lt;/p&gt;
&lt;p&gt;The challenge includes context such as:&lt;/p&gt;
&lt;p&gt;MyZubster account&lt;br&gt;
wallet address&lt;br&gt;
chain ID&lt;br&gt;
domain&lt;br&gt;
application URI&lt;br&gt;
random nonce&lt;br&gt;
issued time&lt;br&gt;
expiration time&lt;br&gt;
intended action&lt;/p&gt;
&lt;p&gt;The intended action is currently:&lt;/p&gt;
&lt;p&gt;LINK_WALLET&lt;/p&gt;
&lt;p&gt;A simplified message looks like:&lt;/p&gt;
&lt;p&gt;www.myzubster.com wants you to link your Ethereum account to MyZubster:&lt;/p&gt;
&lt;p&gt;0x...&lt;/p&gt;
&lt;p&gt;Sign this message to prove wallet control.&lt;br&gt;
This is not a payment and does not spend ETH.&lt;/p&gt;
&lt;p&gt;URI: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com&quot;&gt;https://www.myzubster.com&lt;/a&gt;&lt;br&gt;
Version: 1&lt;br&gt;
Chain ID: 1&lt;br&gt;
Nonce: ...&lt;br&gt;
Issued At: ...&lt;br&gt;
Expiration Time: ...&lt;br&gt;
Request ID: LINK_WALLET:user-id&lt;/p&gt;
&lt;p&gt;That “not a payment” line is deliberate.&lt;/p&gt;
&lt;p&gt;I don’t want a wallet signature to ever be confused with a transaction.&lt;/p&gt;
&lt;p&gt;Why use a nonce?&lt;/p&gt;
&lt;p&gt;Because the signature must not be reusable forever.&lt;/p&gt;
&lt;p&gt;Each challenge is:&lt;/p&gt;
&lt;p&gt;random&lt;br&gt;
short-lived&lt;br&gt;
account-bound&lt;br&gt;
wallet-bound&lt;br&gt;
action-bound&lt;/p&gt;
&lt;p&gt;The backend checks:&lt;/p&gt;
&lt;p&gt;challenge exists&lt;br&gt;
↓&lt;br&gt;
challenge is not expired&lt;br&gt;
↓&lt;br&gt;
wallet matches&lt;br&gt;
↓&lt;br&gt;
message matches&lt;br&gt;
↓&lt;br&gt;
signature is valid&lt;br&gt;
↓&lt;br&gt;
recovered signer matches wallet&lt;/p&gt;
&lt;p&gt;Only then does the wallet become verified.&lt;/p&gt;
&lt;p&gt;Signature verification&lt;/p&gt;
&lt;p&gt;The backend uses ethers to recover the signer:&lt;/p&gt;
&lt;p&gt;const recovered = verifyMessage(message, signature);&lt;/p&gt;
&lt;p&gt;Then the recovered address is normalized and compared with the expected wallet.&lt;/p&gt;
&lt;p&gt;If another wallet signs the challenge, verification fails.&lt;/p&gt;
&lt;p&gt;What gets stored&lt;/p&gt;
&lt;p&gt;MyZubster never stores:&lt;/p&gt;
&lt;p&gt;private key&lt;br&gt;
seed phrase&lt;br&gt;
mnemonic&lt;br&gt;
MetaMask password&lt;br&gt;
wallet secret&lt;/p&gt;
&lt;p&gt;Only wallet relationship metadata is persisted.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;p&gt;evmWallet: {&lt;br&gt;
  walletType: 'EVM',&lt;br&gt;
  provider: 'metamask',&lt;br&gt;
  address: '0x...',&lt;br&gt;
  chainId: 1,&lt;br&gt;
  status: 'WALLET_VERIFIED',&lt;br&gt;
  verifiedAt: Date,&lt;br&gt;
  lastVerifiedAt: Date&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;Temporary challenge information is also stored server-side until verification or expiration.&lt;/p&gt;
&lt;p&gt;Wallet state&lt;/p&gt;
&lt;p&gt;I’m avoiding a single boolean like:&lt;/p&gt;
&lt;p&gt;walletConnected = true&lt;/p&gt;
&lt;p&gt;Instead, MyZubster has explicit states:&lt;/p&gt;
&lt;p&gt;WALLET_NOT_CONNECTED&lt;br&gt;
WALLET_CHALLENGE_PENDING&lt;br&gt;
WALLET_VERIFIED&lt;br&gt;
WALLET_DISCONNECTED&lt;/p&gt;
&lt;p&gt;That becomes useful both for the UI and for Zorgax.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;MetaMask connected&lt;/p&gt;
&lt;p&gt;is not the same as:&lt;/p&gt;
&lt;p&gt;wallet ownership verified&lt;/p&gt;
&lt;p&gt;and neither means:&lt;/p&gt;
&lt;p&gt;payment completed&lt;br&gt;
Frontend&lt;/p&gt;
&lt;p&gt;I extended the existing WalletHubPanel rather than creating a completely separate wallet system.&lt;/p&gt;
&lt;p&gt;The user now gets a:&lt;/p&gt;
&lt;p&gt;Connect MetaMask&lt;/p&gt;
&lt;p&gt;action.&lt;/p&gt;
&lt;p&gt;The frontend uses:&lt;/p&gt;
&lt;p&gt;eth_requestAccounts&lt;/p&gt;
&lt;p&gt;then:&lt;/p&gt;
&lt;p&gt;eth_chainId&lt;/p&gt;
&lt;p&gt;and finally:&lt;/p&gt;
&lt;p&gt;personal_sign&lt;/p&gt;
&lt;p&gt;The signature is then sent back to the backend for verification.&lt;/p&gt;
&lt;p&gt;The UI explicitly explains that the signature:&lt;/p&gt;
&lt;p&gt;does not transfer ETH&lt;br&gt;
does not cost gas&lt;br&gt;
does not authorize a payment&lt;br&gt;
Preventing duplicate wallet ownership claims&lt;/p&gt;
&lt;p&gt;Another issue is what happens if two MyZubster accounts try to link the same verified wallet.&lt;/p&gt;
&lt;p&gt;The current behavior is:&lt;/p&gt;
&lt;p&gt;Account A&lt;br&gt;
→ verifies 0xABC&lt;/p&gt;
&lt;p&gt;Account B&lt;br&gt;
→ tries to verify 0xABC&lt;/p&gt;
&lt;p&gt;Result:&lt;br&gt;
WALLET_ALREADY_LINKED&lt;/p&gt;
&lt;p&gt;This does not mean that MyZubster treats a wallet as a legal identity.&lt;/p&gt;
&lt;p&gt;It simply prevents contradictory wallet-to-account relationships inside the application.&lt;/p&gt;
&lt;p&gt;What changes in MyZubster&lt;/p&gt;
&lt;p&gt;This creates a new trust layer.&lt;/p&gt;
&lt;p&gt;MyZubster can already work with:&lt;/p&gt;
&lt;p&gt;account identity&lt;br&gt;
GitHub identity&lt;br&gt;
community profile&lt;br&gt;
Marketplace identity&lt;br&gt;
Zorgax context&lt;/p&gt;
&lt;p&gt;Now it can also say:&lt;/p&gt;
&lt;p&gt;this account proved control&lt;br&gt;
of this EVM wallet&lt;br&gt;
at this time&lt;/p&gt;
&lt;p&gt;That is a reusable building block.&lt;/p&gt;
&lt;p&gt;The next milestone: signed Marketplace requests&lt;/p&gt;
&lt;p&gt;This is where the wallet integration becomes much more useful.&lt;/p&gt;
&lt;p&gt;The next step is to let a verified wallet sign a canonical Marketplace request.&lt;/p&gt;
&lt;p&gt;Something like:&lt;/p&gt;
&lt;p&gt;{&lt;br&gt;
  &quot;schema&quot;: &quot;myzubster.marketplace-request.v1&quot;,&lt;br&gt;
  &quot;intent&quot;: &quot;MARKETPLACE_REQUEST&quot;,&lt;br&gt;
  &quot;listingId&quot;: &quot;listing-123&quot;,&lt;br&gt;
  &quot;quantity&quot;: 1,&lt;br&gt;
  &quot;buyerAccountReference&quot;: &quot;user-42&quot;,&lt;br&gt;
  &quot;walletAddress&quot;: &quot;0x...&quot;,&lt;br&gt;
  &quot;nonce&quot;: &quot;...&quot;,&lt;br&gt;
  &quot;issuedAt&quot;: &quot;...&quot;,&lt;br&gt;
  &quot;expiresAt&quot;: &quot;...&quot;&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;The buyer signs it off-chain.&lt;/p&gt;
&lt;p&gt;The server verifies it.&lt;/p&gt;
&lt;p&gt;Then MyZubster can create:&lt;/p&gt;
&lt;p&gt;MarketplaceOrder: REQUESTED&lt;/p&gt;
&lt;p&gt;with cryptographic evidence attached.&lt;/p&gt;
&lt;p&gt;No gas.&lt;/p&gt;
&lt;p&gt;No blockchain transaction.&lt;/p&gt;
&lt;p&gt;No payment.&lt;/p&gt;
&lt;p&gt;Just signed intent.&lt;/p&gt;
&lt;p&gt;Why not put every action on-chain?&lt;/p&gt;
&lt;p&gt;Because not every interaction needs blockchain finality.&lt;/p&gt;
&lt;p&gt;If every Marketplace request required a transaction, the UX would become unnecessarily heavy.&lt;/p&gt;
&lt;p&gt;Instead, the architecture I’m aiming for is:&lt;/p&gt;
&lt;p&gt;Marketplace request&lt;br&gt;
↓&lt;br&gt;
off-chain signature&lt;br&gt;
↓&lt;br&gt;
seller acceptance&lt;br&gt;
↓&lt;br&gt;
exchange lifecycle&lt;br&gt;
↓&lt;br&gt;
structured evidence&lt;br&gt;
↓&lt;br&gt;
optional blockchain anchor&lt;/p&gt;
&lt;p&gt;Blockchain should be used where permanence adds value.&lt;/p&gt;
&lt;p&gt;Not everywhere by default.&lt;/p&gt;
&lt;p&gt;User wallet vs infrastructure wallet&lt;/p&gt;
&lt;p&gt;This is also an important boundary.&lt;/p&gt;
&lt;p&gt;MyZubster already has server-side blockchain infrastructure for evidence anchoring.&lt;/p&gt;
&lt;p&gt;That wallet must stay completely separate from user wallets.&lt;/p&gt;
&lt;p&gt;User MetaMask&lt;br&gt;
!=&lt;br&gt;
MyZubster infrastructure wallet&lt;/p&gt;
&lt;p&gt;User-controlled wallet actions remain user-controlled.&lt;/p&gt;
&lt;p&gt;Server-side evidence operations remain isolated.&lt;/p&gt;
&lt;p&gt;How Zorgax can use this&lt;/p&gt;
&lt;p&gt;Once the wallet state is explicit, Zorgax can explain it correctly.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;User:&lt;br&gt;
Is my wallet verified?&lt;/p&gt;
&lt;p&gt;Zorgax:&lt;br&gt;
Yes. Your MetaMask wallet ownership has been verified.&lt;/p&gt;
&lt;p&gt;But if the user asks:&lt;/p&gt;
&lt;p&gt;Did I pay?&lt;/p&gt;
&lt;p&gt;Zorgax should answer:&lt;/p&gt;
&lt;p&gt;No confirmed payment evidence exists.&lt;br&gt;
Your wallet only signed the request.&lt;/p&gt;
&lt;p&gt;That distinction is essential.&lt;/p&gt;
&lt;p&gt;Security tests&lt;/p&gt;
&lt;p&gt;I’ve added tests covering:&lt;/p&gt;
&lt;p&gt;valid signer&lt;br&gt;
wrong signer&lt;br&gt;
expired challenge&lt;br&gt;
non-payment message semantics&lt;br&gt;
authenticated API wiring&lt;br&gt;
MetaMask provider integration&lt;/p&gt;
&lt;p&gt;The goal is not just to make the feature work.&lt;/p&gt;
&lt;p&gt;The goal is to make it hard to misunderstand.&lt;/p&gt;
&lt;p&gt;Current status&lt;/p&gt;
&lt;p&gt;The first implementation is now open as:&lt;/p&gt;
&lt;p&gt;PR #1385 — MetaMask wallet ownership linking&lt;/p&gt;
&lt;p&gt;The PR adds:&lt;/p&gt;
&lt;p&gt;wallet model&lt;br&gt;
challenge service&lt;br&gt;
signature verification&lt;br&gt;
authenticated API&lt;br&gt;
MetaMask UI&lt;br&gt;
disconnect flow&lt;br&gt;
security tests&lt;/p&gt;
&lt;p&gt;This is not yet an ETH payment system.&lt;/p&gt;
&lt;p&gt;It is the identity and ownership layer that payments or signed Marketplace actions can build on later.&lt;/p&gt;
&lt;p&gt;Why this matters for MyZubster&lt;/p&gt;
&lt;p&gt;The bigger change is architectural.&lt;/p&gt;
&lt;p&gt;MyZubster is gradually moving toward a model where different kinds of evidence are handled by different layers:&lt;/p&gt;
&lt;p&gt;MyZubster account&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;wallet control&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;signed intent&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Marketplace state&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;payment evidence&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;optional blockchain evidence&lt;/p&gt;
&lt;p&gt;instead of:&lt;/p&gt;
&lt;p&gt;connect wallet&lt;br&gt;
↓&lt;br&gt;
everything becomes blockchain&lt;/p&gt;
&lt;p&gt;For me, that is the more useful direction.&lt;/p&gt;
&lt;p&gt;MetaMask should not just be a button.&lt;/p&gt;
&lt;p&gt;It should become a cryptographic tool that adds proof exactly where proof is useful.&lt;/p&gt;
&lt;p&gt;Project: MyZubster&lt;br&gt;
Stack: Node.js, Express, MongoDB, React, ethers, MetaMask&lt;br&gt;
Current milestone: EVM wallet ownership verification&lt;br&gt;
Next milestone: signed Marketplace requests without gas&lt;/p&gt;
&lt;p&gt;Repository:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Implementation PR:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/1385&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster/pull/1385&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28567/building-metamask-wallet-ownership-verification-in-myzubster</guid>
<pubDate>Fri, 25 Sep 2026 04:23:07 +0000</pubDate>
</item>
<item>
<title>AI Can Generate More UI. Who Keeps It Consistent?</title>
<link>https://coderlegion.com/28523/ai-can-generate-more-ui-who-keeps-it-consistent</link>
<description>&lt;p&gt;AI makes UI faster to produce — and easier to fragment. Why Vellira is building cross-platform components and developer tooling to keep growing UI systems coherent as humans and coding agents move faster.&lt;/p&gt;
&lt;p&gt;AI can now generate UI faster than many teams can review it.&lt;/p&gt;
&lt;p&gt;A developer can describe a component, ask a coding model to implement it, and get a plausible result in minutes. The same model can refactor nearby code, write tests, update types, and produce several variations before a traditional implementation would have reached its first review.&lt;/p&gt;
&lt;p&gt;That is a real improvement.&lt;/p&gt;
&lt;p&gt;But it also moves the bottleneck.&lt;/p&gt;
&lt;p&gt;The hard question is becoming less:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How quickly can we create another component?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And more:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How do we keep the whole UI system coherent when humans and AI can change it this quickly?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is one of the problems we are building Vellira around.&lt;/p&gt;
&lt;h3 id=&quot;generation-is-getting-cheaper-fragmentation-is-not-&quot;&gt;Generation is getting cheaper. Fragmentation is not.&lt;/h3&gt;
&lt;p&gt;Imagine an AI-generated component that looks good, compiles, and works in a local preview.&lt;/p&gt;
&lt;p&gt;That still leaves a surprising number of questions unanswered.&lt;/p&gt;
&lt;p&gt;Does it duplicate something the system already has? Does its API fit the surrounding components? Does it use the shared design foundations? Is the same product capability represented correctly on React Native? Are accessibility expectations covered? Did the documentation and examples move with the implementation? Will the next contributor understand how this component is supposed to evolve?&lt;/p&gt;
&lt;p&gt;A locally correct component can still make the system around it less consistent.&lt;/p&gt;
&lt;p&gt;That distinction matters more as generation gets faster.&lt;/p&gt;
&lt;p&gt;When UI changes slowly, teams can compensate for weak system boundaries with review, documentation, and memory. Someone remembers which component should be reused. Someone catches the one-off visual value. Someone notices that Web changed but React Native did not. Someone remembers to update Storybook.&lt;/p&gt;
&lt;p&gt;As the rate of change increases, that becomes harder to sustain.&lt;/p&gt;
&lt;p&gt;More UI output creates more opportunities for small differences to accumulate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;APIs drift between similar components;&lt;/li&gt;
&lt;li&gt;accessibility behavior becomes inconsistent;&lt;/li&gt;
&lt;li&gt;local values bypass shared tokens;&lt;/li&gt;
&lt;li&gt;documentation falls behind implementation;&lt;/li&gt;
&lt;li&gt;Web and React Native diverge unintentionally;&lt;/li&gt;
&lt;li&gt;similar capabilities get implemented more than once;&lt;/li&gt;
&lt;li&gt;tests encode different expectations for comparable behavior;&lt;/li&gt;
&lt;li&gt;package and public surfaces become harder to keep aligned.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these problems were invented by AI.&lt;/p&gt;
&lt;p&gt;AI simply increases the rate at which they can appear.&lt;/p&gt;
&lt;p&gt;If a team can create five or ten times more UI changes, the system around those changes has to become stronger too.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9517244816750394364&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;a-component-library-is-necessary-it-is-no-longer-sufficient-&quot;&gt;A component library is necessary. It is no longer sufficient.&lt;/h3&gt;
&lt;p&gt;Traditional design systems are often described as collections of reusable components, tokens, guidelines, and documentation.&lt;/p&gt;
&lt;p&gt;Those foundations still matter. Vellira has them too.&lt;/p&gt;
&lt;p&gt;But a catalog can only tell you so much.&lt;/p&gt;
&lt;p&gt;It can show that &lt;code&gt;Button&lt;/code&gt;, &lt;code&gt;Select&lt;/code&gt;, or &lt;code&gt;Tabs&lt;/code&gt; exists. It cannot, by itself, keep every future change aligned with the same expectations around APIs, accessibility, platforms, documentation, testing, and quality.&lt;/p&gt;
&lt;p&gt;This becomes especially visible when UI work is shared between developers and coding agents.&lt;/p&gt;
&lt;p&gt;A coding model can generate a perfectly reasonable component that does not quite belong to the system around it.&lt;/p&gt;
&lt;p&gt;It may introduce a slightly different API. It may recreate a capability that already exists. It may look correct on Web while ignoring the native side of the project. It may use values that are visually close to existing tokens without actually using the shared token system.&lt;/p&gt;
&lt;p&gt;The code is not necessarily bad.&lt;/p&gt;
&lt;p&gt;The surrounding system is simply asking a larger question than &quot;does this component work?&quot;&lt;/p&gt;
&lt;p&gt;It is asking:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Does this change still belong to the same product and engineering system?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is the layer we are increasingly interested in with Vellira.&lt;/p&gt;
&lt;h3 id=&quot;what-vellira-is-becoming&quot;&gt;What Vellira is becoming&lt;/h3&gt;
&lt;p&gt;Vellira started as a cross-platform design system for React and React Native.&lt;/p&gt;
&lt;p&gt;The public project still has that foundation: reusable components, shared tokens, documentation, Storybook, and cross-platform implementation work.&lt;/p&gt;
&lt;p&gt;But building the component set exposed a second problem very quickly.&lt;/p&gt;
&lt;p&gt;Creating components is only part of the work. Keeping all of them aligned as the system grows requires repeatable workflows and tooling around the components themselves.&lt;/p&gt;
&lt;p&gt;Today the public Vellira project already combines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reusable React and React Native components;&lt;/li&gt;
&lt;li&gt;shared design tokens and cross-platform foundations;&lt;/li&gt;
&lt;li&gt;Storybook and documentation for discovery and usage;&lt;/li&gt;
&lt;li&gt;repeatable component-generation workflows;&lt;/li&gt;
&lt;li&gt;automated component quality checks;&lt;/li&gt;
&lt;li&gt;accessibility-focused validation and testing;&lt;/li&gt;
&lt;li&gt;metadata-backed tooling that gives the project structured information about its own components.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of those features alone is the point.&lt;/p&gt;
&lt;p&gt;The interesting part is what they allow us to work toward together: a UI system that becomes easier to extend without becoming easier to fragment.&lt;/p&gt;
&lt;p&gt;We do not want adding a component to mean only &quot;these files compile.&quot;&lt;/p&gt;
&lt;p&gt;We want the surrounding project to help keep that component connected to the same design foundations, the same platform expectations, the same documentation, and the same engineering standards as the rest of Vellira.&lt;/p&gt;
&lt;p&gt;That is useful when a developer writes the code manually.&lt;/p&gt;
&lt;p&gt;It becomes even more useful when an implementation can appear in seconds.&lt;/p&gt;
&lt;h3 id=&quot;vellira-is-not-trying-to-out-code-the-coding-model&quot;&gt;Vellira is not trying to out-code the coding model&lt;/h3&gt;
&lt;p&gt;This distinction is important.&lt;/p&gt;
&lt;p&gt;Coding models are getting better quickly. They should be able to write more implementation code, understand larger codebases, make broader changes, and handle more routine engineering work over time.&lt;/p&gt;
&lt;p&gt;That is good for Vellira.&lt;/p&gt;
&lt;p&gt;We are not building the project around the assumption that AI will remain bad at writing components.&lt;/p&gt;
&lt;p&gt;We are building around the opposite assumption: AI will get very good at producing and modifying UI.&lt;/p&gt;
&lt;p&gt;If that happens, the surrounding system becomes more important, not less.&lt;/p&gt;
&lt;p&gt;A model can generate a new component.&lt;/p&gt;
&lt;p&gt;The project still needs to keep APIs coherent.&lt;/p&gt;
&lt;p&gt;A model can change ten components in one pass.&lt;/p&gt;
&lt;p&gt;The project still needs to keep accessibility, documentation, public surfaces, tokens, and platform behavior from drifting apart.&lt;/p&gt;
&lt;p&gt;A model can become better at understanding context.&lt;/p&gt;
&lt;p&gt;The repository still benefits from making its important expectations explicit and repeatable instead of depending on every author to remember them perfectly.&lt;/p&gt;
&lt;p&gt;So the goal is not to slow AI-assisted development down.&lt;/p&gt;
&lt;p&gt;The goal is to make that speed safer to absorb.&lt;/p&gt;
&lt;h3 id=&quot;the-author-matters-less-than-the-result&quot;&gt;The author matters less than the result&lt;/h3&gt;
&lt;p&gt;There is a temptation to treat AI-generated code as a completely separate category that needs a special design-system workflow.&lt;/p&gt;
&lt;p&gt;I do not think that is the most useful distinction.&lt;/p&gt;
&lt;p&gt;Once code enters a repository, the important question is not who typed it.&lt;/p&gt;
&lt;p&gt;The important question is whether it fits the project.&lt;/p&gt;
&lt;p&gt;A human can introduce an inconsistent API.&lt;/p&gt;
&lt;p&gt;A coding agent can follow the existing conventions perfectly.&lt;/p&gt;
&lt;p&gt;A developer can forget documentation.&lt;/p&gt;
&lt;p&gt;An automated workflow can keep it current.&lt;/p&gt;
&lt;p&gt;The author is not a reliable measure of system quality. The resulting state of the project is.&lt;/p&gt;
&lt;p&gt;That is why Vellira is investing in tooling around the component lifecycle rather than relying only on the intelligence or discipline of whichever author produced the latest change.&lt;/p&gt;
&lt;p&gt;Humans and AI can both move faster when the project around them carries more of the consistency burden.&lt;/p&gt;
&lt;h3 id=&quot;cross-platform-ui-raises-the-stakes&quot;&gt;Cross-platform UI raises the stakes&lt;/h3&gt;
&lt;p&gt;Vellira targets both React and React Native.&lt;/p&gt;
&lt;p&gt;That makes this problem even more visible because &quot;the same component&quot; does not always mean &quot;the same source code.&quot;&lt;/p&gt;
&lt;p&gt;Web and native platforms have different primitives, interaction models, accessibility APIs, and constraints. Some behavior should be shared. Some behavior needs to diverge intentionally.&lt;/p&gt;
&lt;p&gt;A useful cross-platform design system therefore needs more than copied implementations.&lt;/p&gt;
&lt;p&gt;It needs to keep one product language coherent while still respecting each platform.&lt;/p&gt;
&lt;p&gt;Faster code generation does not remove that challenge.&lt;/p&gt;
&lt;p&gt;It can amplify it.&lt;/p&gt;
&lt;p&gt;An AI tool can create two implementations very quickly, but speed alone cannot tell a team whether those implementations still represent one coherent component system.&lt;/p&gt;
&lt;p&gt;That is why the tooling around Vellira's components matters as much as the component count itself.&lt;/p&gt;
&lt;h3 id=&quot;the-bottleneck-is-moving-from-production-to-coordination&quot;&gt;The bottleneck is moving from production to coordination&lt;/h3&gt;
&lt;p&gt;This is the part of AI-assisted frontend development that I find most interesting.&lt;/p&gt;
&lt;p&gt;Better coding models reduce the cost of producing code.&lt;/p&gt;
&lt;p&gt;When producing UI is expensive, teams naturally make fewer changes. When producing UI becomes cheap, experimentation increases, more contributors can work in parallel, and automated agents can make changes of their own.&lt;/p&gt;
&lt;p&gt;The limiting resource starts moving away from typing speed.&lt;/p&gt;
&lt;p&gt;It moves toward coordination, consistency, review, and maintenance.&lt;/p&gt;
&lt;p&gt;That creates a different kind of leverage for developer tooling.&lt;/p&gt;
&lt;p&gt;If ten changes can be produced in the time it previously took to produce one, tooling that helps those ten changes remain part of the same system becomes more valuable.&lt;/p&gt;
&lt;p&gt;This is the bet behind the direction Vellira is taking:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;AI can generate more UI. Vellira is being built to help keep that UI coherent.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The better generation becomes, the more important the system around generation becomes.&lt;/p&gt;
&lt;h3 id=&quot;where-vellira-is-today&quot;&gt;Where Vellira is today&lt;/h3&gt;
&lt;p&gt;We are still building the foundation, and I want to be precise about that.&lt;/p&gt;
&lt;p&gt;The immediate work is concrete: expand the cross-platform component set, strengthen component workflows, improve quality and accessibility checks, and make the public project easier to adopt and use.&lt;/p&gt;
&lt;p&gt;We are proving these ideas inside Vellira itself before making broader claims about what the tooling can understand or manage outside the project.&lt;/p&gt;
&lt;p&gt;That constraint is intentional.&lt;/p&gt;
&lt;p&gt;It is easy to describe an ambitious future for developer tooling. It is more useful to earn that future by making the current system work reliably first.&lt;/p&gt;
&lt;p&gt;But the product direction is already clear.&lt;/p&gt;
&lt;p&gt;Frontend teams are going to work with more generated code, more automation, and more AI-assisted changes — not less.&lt;/p&gt;
&lt;p&gt;The answer should not be to slow those tools down.&lt;/p&gt;
&lt;p&gt;It should be to make the UI system around them stronger.&lt;/p&gt;
&lt;p&gt;If you are building React or React Native interfaces and thinking about how AI changes design-system workflows, follow Vellira as we build this in public:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.vellira.dev&quot;&gt;Explore the Vellira documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://storybook.vellira.dev&quot;&gt;Browse the component system in Storybook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/vellira-dev/vellira&quot;&gt;Follow Vellira on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI can help us build UI faster.&lt;/p&gt;
&lt;p&gt;Vellira is being built so that faster does not have to mean more fragmented.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28523/ai-can-generate-more-ui-who-keeps-it-consistent</guid>
<pubDate>Thu, 24 Sep 2026 16:57:01 +0000</pubDate>
</item>
<item>
<title>This is how I added an in-browser auto captions feature to my YouTube Shorts converter web application using Whisper AI and ffmpeg.wasm</title>
<link>https://coderlegion.com/28514/added-browser-captions-feature-youtube-shorts-converter-application-using-whisper-ffmpeg</link>
<description>&lt;p&gt;A few weeks ago I launched Convert to Shorts — a free browser-based tool that converts horizontal videos to YouTube Shorts format (9:16) without uploading anything to a server. I wrote about the ffmpeg.wasm + Vite setup in a previous article.&lt;/p&gt;
&lt;p&gt;The most requested feature after launch was auto captions. Captions significantly boost Shorts engagement since most people watch without sound, and manually typing captions is tedious.&lt;/p&gt;
&lt;p&gt;The challenge: how do you add free auto captions to a privacy-first tool that never uploads your video to a server?&lt;/p&gt;
&lt;p&gt;The answer: run Whisper AI in the browser.&lt;/p&gt;
&lt;p&gt;The stack - &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Transformers.js (&lt;code&gt;@xenova/transformers&lt;/code&gt;) — Hugging Face's JavaScript port of the Transformers library, runs ONNX models in the browser via WebAssembly&lt;/li&gt;
&lt;li&gt;Whisper tiny — OpenAI's speech recognition model, 75MB, surprisingly accurate for clear speech&lt;/li&gt;
&lt;li&gt;Web Audio API — for extracting and resampling audio from the video file&lt;/li&gt;
&lt;li&gt;ffmpeg.wasm — for burning captions into the video&lt;/li&gt;
&lt;li&gt;ASS subtitles — the subtitle format libass (inside ffmpeg.wasm) understands.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Audio extraction&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Whisper expects mono 16kHz audio as a Float32Array. The Web Audio API handles this cleanly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function extractAudio(
  file: File,
  trimStart: number,
  trimEnd: number
): Promise&amp;lt;Float32Array&amp;gt; {
  const arrayBuffer = await file.arrayBuffer();
  const audioContext = new AudioContext({ sampleRate: 16000 });
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);

  const sampleRate = audioContext.sampleRate;
  const startSample = Math.floor(trimStart * sampleRate);
  const endSample = Math.floor(trimEnd * sampleRate);

  // Mix down to mono, slice to trim range
  const channelData = audioBuffer.getChannelData(0);
  const trimmed = channelData.slice(startSample, endSample);
  await audioContext.close();
  return trimmed;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Creating the &lt;code&gt;AudioContext&lt;/code&gt; at 16kHz means the browser automatically resamples from whatever the source rate is (usually 44.1kHz or 48kHz). No manual resampling needed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Running Whisper&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Transformers.js makes this surprisingly straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const { pipeline, env } = await import(&quot;@xenova/transformers&quot;);

env.useBrowserCache = true;

const transcriber = await pipeline(
  &quot;automatic-speech-recognition&quot;,
  &quot;Xenova/whisper-tiny&quot;,
  {
    progress_callback: (p) =&amp;gt; {
      if (p.status === &quot;downloading&quot;) {
        setModelProgress(Math.round((p.loaded / p.total) * 100));
      }
    },
  }
);

const result = await transcriber(audioFloat32Array, {
  return_timestamps: true,
  chunk_length_s: 30,
  stride_length_s: 5,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;return_timestamps: true&lt;/code&gt; gives you back an array of segments with &lt;code&gt;text&lt;/code&gt; and &lt;code&gt;timestamp: [start, end]&lt;/code&gt; — exactly what you need for timed captions.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;env.useBrowserCache = true&lt;/code&gt; means the model downloads once and is stored in IndexedDB. Every subsequent use loads from cache — no 75MB download each time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Burning captions with ffmpeg.wasm&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is where it got interesting. My first instinct was to use ffmpeg's &lt;code&gt;drawtext&lt;/code&gt; filter with an &lt;code&gt;enable='between(t,start,end)'&lt;/code&gt; expression for each caption segment — one filter per caption, chained with commas.&lt;/p&gt;
&lt;p&gt;This failed in multiple ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The comma inside &lt;code&gt;between(t,start,end)&lt;/code&gt; was interpreted as a filter separator&lt;/li&gt;
&lt;li&gt;Escaping with &lt;code&gt;\\&lt;/code&gt;, didn't work in ffmpeg.wasm's argument parsing&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;gte(t,start)*lte(t,end)&lt;/code&gt; instead of between also failed at the filter chain level&lt;/li&gt;
&lt;li&gt;Chaining 20+ &lt;code&gt;drawtext&lt;/code&gt; filters caused ffmpeg.wasm to abort&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reliable solution was ASS subtitles. ASS (Advanced SubStation Alpha) is a subtitle format that ffmpeg's built-in &lt;code&gt;ass&lt;/code&gt; filter handles natively via libass:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function buildAssSubtitles(segments, color, size) {
  function toAssTime(seconds) {
    const h = Math.floor(seconds / 3600);
    const m = Math.floor((seconds % 3600) / 60);
    const s = Math.floor(seconds % 60);
    const cs = Math.round((seconds % 1) * 100);
    return `${h}:${String(m).padStart(2,&quot;0&quot;)}:${String(s).padStart(2,&quot;0&quot;)}.${String(cs).padStart(2,&quot;0&quot;)}`;
  }

  const header = `[Script Info]
ScriptType: v4.00+
PlayResX: 1080
PlayResY: 1920
WrapStyle: 1
ScaledBorderAndShadow: yes

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, ...
Style: Default,Roboto Bold,75,&amp;amp;H00FFFFFF,...

[Events]
Format: Layer, Start, End, Style, Text
`;

  const events = segments
    .map(seg =&amp;gt;
      `Dialogue: 0,${toAssTime(seg.start)},${toAssTime(seg.end)},Default,${seg.text}`
    )
    .join(&quot;\n&quot;);

  return header + events;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4: The font problem&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;libass needs a font to render subtitles. In a normal environment it uses system fonts. In ffmpeg.wasm's WebAssembly sandbox there are no system fonts.&lt;/p&gt;
&lt;p&gt;Three approaches I tried:&lt;/p&gt;
&lt;p&gt;Approach 1: Embed font as Base64 in the ASS file&lt;br&gt;
The ASS format supports a &lt;code&gt;[Fonts]&lt;/code&gt; section with Base64-encoded font data split into 80-character lines. This failed with a libass assertion error in &lt;code&gt;ass.c&lt;/code&gt; about Base64 padding — the ffmpeg.wasm build of libass appears to have a bug in its font decoder.&lt;/p&gt;
&lt;p&gt;Approach 2: Write font to ffmpeg.wasm virtual filesystem and use &lt;code&gt;fontsdir&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;ass=captions.ass:fontsdir=/fonts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Write the font file to &lt;code&gt;/fonts/Roboto-Bold.ttf&lt;/code&gt; in the virtual filesystem before running ffmpeg. This worked.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;await ffmpeg.createDir(&quot;/fonts&quot;);
const fontResponse = await fetch(&quot;/Roboto-Bold.ttf&quot;);
const fontBuffer = await fontResponse.arrayBuffer();
await ffmpeg.writeFile(&quot;/fonts/Roboto-Bold.ttf&quot;, new Uint8Array(fontBuffer));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key insight: ffmpeg.wasm has a full virtual filesystem (Emscripten's FS). You can create directories and write files to it just like a real filesystem, and ffmpeg commands can reference those paths.&lt;/p&gt;
&lt;p&gt;As a result the full caption pipeline added roughly 10-30 seconds to the export time for a 30-60 second clip. The Whisper tiny model is surprisingly accurate for clear English speech. Multiple languages work out of the box since Whisper was trained on multilingual data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I learned&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Transformers.js is genuinely production-ready. The API is clean, browser caching just works, and the ONNX runtime handles the WebAssembly execution reliably.&lt;/p&gt;
&lt;p&gt;ASS subtitles are more robust than &lt;code&gt;drawtext&lt;/code&gt; filter chains. If you're burning timed text into video with ffmpeg.wasm, reach for the &lt;code&gt;ass&lt;/code&gt; filter before trying to chain multiple drawtext filters.&lt;/p&gt;
&lt;p&gt;ffmpeg.wasm's virtual filesystem is powerful. You're not limited to just reading and writing video files — you can create directory structures, write fonts, write subtitle files, and reference them all from ffmpeg commands exactly as you would on a real filesystem.&lt;/p&gt;
&lt;p&gt;libass has quirks in the WASM build. The embedded font Base64 decoding in the WASM build of libass appears broken. Use fontsdir instead.&lt;/p&gt;
&lt;p&gt;Try it&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://converttoshorts.com/&quot;&gt;converttoshorts.com&lt;/a&gt; — free, no account, no upload. Auto captions are in the export panel.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28514/added-browser-captions-feature-youtube-shorts-converter-application-using-whisper-ffmpeg</guid>
<pubDate>Thu, 24 Sep 2026 16:02:33 +0000</pubDate>
</item>
<item>
<title>Why I stopped using online image compressors and built a CLI instead</title>
<link>https://coderlegion.com/28438/why-i-stopped-using-online-image-compressors-and-built-a-cli-instead</link>
<description>&lt;p&gt;Four years of optimizing React and Next.js projects taught me one thing: unoptimized images are everywhere, and nobody wants to fix them.&lt;/p&gt;
&lt;p&gt;Every project has the same pattern. Heavy PNG and JPG files are sitting inside &lt;code&gt;/public&lt;/code&gt;, there is no consistent image pipeline, and some of those files have no business being that large in a production codebase.&lt;/p&gt;
&lt;p&gt;This is especially common in small and mid-sized projects. There is no CDN transformation layer or dedicated asset pipeline. Images get added while the product is moving quickly, and the cleanup becomes a task for “later.”&lt;/p&gt;
&lt;p&gt;Later, of course, never comes.&lt;/p&gt;
&lt;p&gt;Then, at 1am, while refactoring an extremely vibe-coded Next.js project, I found myself doing the cleanup manually again.&lt;/p&gt;
&lt;p&gt;Find an image. Upload it to an online compressor. Hit the free limit. Open another tool. Convert a few more. Download everything. Replace the original files. Hunt through the codebase for every import and &lt;code&gt;src&lt;/code&gt; path. Hope I did not miss one.&lt;/p&gt;
&lt;p&gt;And I finally thought: I am a developer. Why am I doing this by hand?&lt;/p&gt;
&lt;p&gt;So I built &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/pixcrush&quot;&gt;pixcrush&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx pixcrush .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One command to convert the images, compress them, and update their matching code references automatically.&lt;/p&gt;
&lt;h3 id=&quot;-but-doesn-t-next-js-already-optimize-images-&quot;&gt;“But doesn’t Next.js already optimize images?”&lt;/h3&gt;
&lt;p&gt;Yes, and if your application uses &lt;code&gt;next/image&lt;/code&gt; consistently, you should absolutely take advantage of it.&lt;/p&gt;
&lt;p&gt;The Next.js &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; component can resize images for different devices, lazy-load them, and serve modern formats such as WebP. Files inside &lt;code&gt;/public&lt;/code&gt; can be referenced from the root URL, while statically imported images also give Next.js access to their intrinsic dimensions. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/getting-started/images&quot;&gt;official Next.js image documentation&lt;/a&gt; explains these runtime optimizations in detail.&lt;/p&gt;
&lt;p&gt;But that solves a different layer of the problem.&lt;/p&gt;
&lt;p&gt;I wanted to clean up the source assets themselves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Replace heavy PNG and JPG files with smaller WebP files when conversion is worthwhile.&lt;/li&gt;
&lt;li&gt;Update existing imports and string-based image paths across the repository.&lt;/li&gt;
&lt;li&gt;Identify images that are no longer referenced.&lt;/li&gt;
&lt;li&gt;Make the migration reviewable before touching any files.&lt;/li&gt;
&lt;li&gt;Keep the process useful for projects that do not consistently use &lt;code&gt;next/image&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This was not about replacing the Next.js image pipeline. It was about making a tedious codebase migration safe and repeatable.&lt;/p&gt;
&lt;h3 id=&quot;why-a-simple-format-conversion-becomes-a-code-migration&quot;&gt;Why a simple format conversion becomes a code migration&lt;/h3&gt;
&lt;p&gt;Consider a small project with images referenced in a few different ways:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import hero from './assets/hero.png';

export function HomePage() {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;Image src={hero} alt=&quot;&quot; /&amp;gt;
      &amp;lt;img src=&quot;/images/team.jpg&quot; alt=&quot;Our team&quot; /&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Converting &lt;code&gt;hero.png&lt;/code&gt; and &lt;code&gt;team.jpg&lt;/code&gt; is only half of the job. The code now points to files that no longer represent the desired output.&lt;/p&gt;
&lt;p&gt;After the migration, the references need to become:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import hero from './assets/hero.webp';

export function HomePage() {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;Image src={hero} alt=&quot;&quot; /&amp;gt;
      &amp;lt;img src=&quot;/images/team.webp&quot; alt=&quot;Our team&quot; /&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now multiply that across &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.jsx&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.tsx&lt;/code&gt;, &lt;code&gt;.html&lt;/code&gt;, and &lt;code&gt;.json&lt;/code&gt; files, plus nested apps inside a Turborepo.&lt;/p&gt;
&lt;p&gt;That is no longer an image-conversion task. It is a codemod with image processing attached.&lt;/p&gt;
&lt;h3 id=&quot;the-workflow-i-wanted&quot;&gt;The workflow I wanted&lt;/h3&gt;
&lt;p&gt;I wanted the tool to perform five clear phases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Scan the repository for supported images and source files.&lt;/li&gt;
&lt;li&gt;Determine which images are actually referenced.&lt;/li&gt;
&lt;li&gt;Convert used PNG and JPG files to WebP.&lt;/li&gt;
&lt;li&gt;Update only the references connected to successful conversions.&lt;/li&gt;
&lt;li&gt;Report what changed, what was skipped, and what needs human review.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The default command is intentionally small:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx pixcrush .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a first pass on an unfamiliar project, I prefer starting with a dry run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx pixcrush . --dry-run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This previews the migration without writing files.&lt;/p&gt;
&lt;h3 id=&quot;step-1-find-the-images-and-their-references&quot;&gt;Step 1: Find the images and their references&lt;/h3&gt;
&lt;p&gt;pixcrush scans for PNG, JPG, and JPEG files while ignoring directories such as &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;.next&lt;/code&gt;, &lt;code&gt;dist&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt;, and &lt;code&gt;.git&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It separately collects the source files that may contain references:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;.js  .jsx  .ts  .tsx  .html  .htm  .json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finding image files is easy. Determining whether they are used is the interesting part.&lt;/p&gt;
&lt;p&gt;For JavaScript and TypeScript, pixcrush parses the source with Babel using the JSX and TypeScript plugins. Babel’s parser produces an abstract syntax tree, which makes it possible to inspect actual string literals instead of blindly replacing every &lt;code&gt;.png&lt;/code&gt; substring. Babel documents its JSX and TypeScript parsing support in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://babeljs.io/docs/babel-parser/&quot;&gt;&lt;code&gt;@babel/parser&lt;/code&gt; reference&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The tracker resolves several common patterns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import logo from './logo.png';

&amp;lt;Image src=&quot;/images/hero.jpg&quot; alt=&quot;&quot; /&amp;gt;

const imagePath = '@/assets/product.png';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also handles image-bearing values in HTML and JSON, including &lt;code&gt;srcset&lt;/code&gt;-style strings and query parameters.&lt;/p&gt;
&lt;p&gt;For absolute paths such as &lt;code&gt;/images/hero.jpg&lt;/code&gt;, pixcrush reconciles the reference with matching &lt;code&gt;public&lt;/code&gt; directories. This matters in monorepos, where the real file may be located at something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;apps/web/public/images/hero.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Images without a resolved reference are reported as unused instead of being converted automatically.&lt;/p&gt;
&lt;h3 id=&quot;step-2-convert-in-memory-and-keep-the-original-when-webp-is-larger&quot;&gt;Step 2: Convert in memory, and keep the original when WebP is larger&lt;/h3&gt;
&lt;p&gt;The actual image conversion uses &lt;a rel=&quot;nofollow&quot; href=&quot;https://sharp.pixelplumbing.com/&quot;&gt;Sharp&lt;/a&gt;, which supports WebP output and exposes a configurable quality setting.&lt;/p&gt;
&lt;p&gt;The important part is not just calling &lt;code&gt;.webp()&lt;/code&gt;. pixcrush creates the WebP data in memory first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const webpBuffer = await sharp(imagePath)
  .webp({ quality })
  .toBuffer();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sharp’s &lt;a rel=&quot;nofollow&quot; href=&quot;https://sharp.pixelplumbing.com/api-output/&quot;&gt;&lt;code&gt;toBuffer()&lt;/code&gt; documentation&lt;/a&gt; confirms that WebP output can be generated directly as a buffer. That lets pixcrush compare the proposed output with the original before writing anything.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;if (webpBuffer.length &amp;gt;= originalSize) {
  // Skip this conversion.
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WebP is often smaller, but “modern format” does not automatically mean “smaller file” for every source image. If the generated WebP would be the same size or larger, pixcrush skips it.&lt;/p&gt;
&lt;p&gt;More importantly, skipped images are not added to the rewrite map. Their code references remain unchanged.&lt;/p&gt;
&lt;p&gt;The default WebP quality is &lt;code&gt;80&lt;/code&gt;, and it can be adjusted:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx pixcrush . --quality 90
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step-3-rewrite-code-without-reformatting-the-entire-file&quot;&gt;Step 3: Rewrite code without reformatting the entire file&lt;/h3&gt;
&lt;p&gt;The riskiest part of the migration is not creating a &lt;code&gt;.webp&lt;/code&gt; file. It is changing the code safely.&lt;/p&gt;
&lt;p&gt;A global find-and-replace is tempting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;.png → .webp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is also far too broad. It can modify unrelated text, references to images that were skipped, or paths that do not resolve to a file pixcrush converted.&lt;/p&gt;
&lt;p&gt;Instead, pixcrush builds a map containing only successful conversions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/absolute/path/hero.png → /absolute/path/hero.webp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It parses each JavaScript or TypeScript source file, visits its string literals, resolves candidate image paths, and creates targeted text replacements only for matches in that conversion map.&lt;/p&gt;
&lt;p&gt;The replacements are applied from the end of the file toward the beginning. This prevents an earlier edit from shifting the offsets required by a later edit.&lt;/p&gt;
&lt;p&gt;There is another subtle benefit: pixcrush does not regenerate the entire source file from the AST. It replaces only the extension inside the matched string. Existing formatting, comments, quote styles, and surrounding code remain untouched.&lt;/p&gt;
&lt;p&gt;Query strings are preserved too:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/images/hero.png?v=2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/images/hero.webp?v=2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-safety-rules-matter-more-than-the-happy-path&quot;&gt;The safety rules matter more than the happy path&lt;/h3&gt;
&lt;p&gt;Code-modifying tools should be conservative. I would rather have pixcrush warn about one path than confidently break a project.&lt;/p&gt;
&lt;p&gt;These are the guardrails I added:&lt;/p&gt;
&lt;h4 id=&quot;dry-runs-do-not-write-files&quot;&gt;Dry runs do not write files&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx pixcrush . --dry-run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This runs the analysis, conversion calculation, and reference-matching flow without writing the WebP files or modified source.&lt;/p&gt;
&lt;h4 id=&quot;dynamic-paths-are-reported-not-guessed&quot;&gt;Dynamic paths are reported, not guessed&lt;/h4&gt;
&lt;p&gt;pixcrush can understand a static value:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;img src=&quot;/products/shoe.png&quot; alt=&quot;&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It cannot safely resolve every runtime value:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;img src={`/products/${product.slug}.png`} alt=&quot;&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dynamic template paths are warned about so they can be reviewed manually.&lt;/p&gt;
&lt;h4 id=&quot;parse-failures-block-destructive-cleanup&quot;&gt;Parse failures block destructive cleanup&lt;/h4&gt;
&lt;p&gt;If a source file cannot be parsed, pixcrush records the failure. When &lt;code&gt;--delete-originals&lt;/code&gt; is enabled, original and unused images are deleted only if both the analysis and rewrite phases completed without parse failures.&lt;/p&gt;
&lt;h4 id=&quot;framework-metadata-images-are-excluded&quot;&gt;Framework metadata images are excluded&lt;/h4&gt;
&lt;p&gt;Files such as favicons, Apple touch icons, Open Graph images, Twitter images, and common PWA manifest icons may be discovered through framework conventions rather than ordinary source references.&lt;/p&gt;
&lt;p&gt;pixcrush excludes these patterns from the migration instead of incorrectly treating them as orphans.&lt;/p&gt;
&lt;h4 id=&quot;external-and-inline-images-are-left-alone&quot;&gt;External and inline images are left alone&lt;/h4&gt;
&lt;p&gt;HTTP URLs and &lt;code&gt;data:&lt;/code&gt; values are not local assets, so the tracker ignores them.&lt;/p&gt;
&lt;h3 id=&quot;running-the-full-migration&quot;&gt;Running the full migration&lt;/h3&gt;
&lt;p&gt;My preferred workflow is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Preview the migration
npx pixcrush . --dry-run

# 2. Review the reported conversions, warnings, and unused files

# 3. Run the migration
npx pixcrush .

# 4. Run the application and its tests

# 5. Remove converted originals only when ready
npx pixcrush . --delete-originals
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final summary includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Images converted&lt;/li&gt;
&lt;li&gt;Space saved&lt;/li&gt;
&lt;li&gt;Source files updated&lt;/li&gt;
&lt;li&gt;Unused images detected&lt;/li&gt;
&lt;li&gt;Dynamic references or parsing issues requiring review&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I still recommend running this on a clean Git branch. A dry run reduces surprises; Git gives you the final, exact review surface.&lt;/p&gt;
&lt;h3 id=&quot;what-pixcrush-does-not-handle-yet&quot;&gt;What pixcrush does not handle yet&lt;/h3&gt;
&lt;p&gt;The current limitations are intentional and worth stating clearly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CSS files are not parsed, so references inside &lt;code&gt;url(...)&lt;/code&gt; are not rewritten. An image referenced only from CSS may also appear in the unused-image report; review that list carefully before using &lt;code&gt;--delete-originals&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Dynamic paths are detected but not automatically converted.&lt;/li&gt;
&lt;li&gt;The image inputs are currently PNG, JPG, and JPEG.&lt;/li&gt;
&lt;li&gt;Framework-convention metadata images are excluded for safety.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a project relies heavily on CSS backgrounds or dynamically generated filenames, expect some manual work after the automated pass.&lt;/p&gt;
&lt;p&gt;“Automated” should not mean “pretend every edge case is safe.”&lt;/p&gt;
&lt;h3 id=&quot;what-changed-for-me&quot;&gt;What changed for me&lt;/h3&gt;
&lt;p&gt;The biggest improvement is not that Sharp can produce a WebP file. The biggest improvement is that the full cleanup is now one reviewable operation.&lt;/p&gt;
&lt;p&gt;The old workflow looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;find → upload → hit a limit → try another tool → download
→ replace files → search the codebase → update paths → repeat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The new workflow is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx pixcrush . --dry-run
npx pixcrush .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the kind of automation I like: not a new platform, dashboard, subscription, or upload queue. Just a local tool that removes a chore and then gets out of the way.&lt;/p&gt;
&lt;h3 id=&quot;try-it-1&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;pixcrush is open source and runs locally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx pixcrush .
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/pixcrush&quot;&gt;npm package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/karishma-dev/pixcrush&quot;&gt;GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you try it on a real React, Next.js, or Turborepo project, I would especially love feedback on path-resolution edge cases and file types worth supporting next.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Connect with me ✌🏻:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LinkedIn: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/karishma-garg-/&quot;&gt;Karishma garg&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Twitter: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/_karishma10&quot;&gt;_karishma10&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/karishma-dev&quot;&gt;karishma-dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Portfolio: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.karishma.dev/&quot;&gt;www.karishma.dev&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28438/why-i-stopped-using-online-image-compressors-and-built-a-cli-instead</guid>
<pubDate>Thu, 24 Sep 2026 07:44:12 +0000</pubDate>
</item>
<item>
<title>@seosiri/developer-ui-kit — a zero-dependency React licensing guard for SaaS dashboards</title>
<link>https://coderlegion.com/28404/seosiri-developer-ui-kit-a-zero-dependency-react-licensing-guard-for-saas-dashboards</link>
<description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;code&gt;@seosiri/developer-ui-kit&lt;/code&gt; (v1.0.1, MIT, 0 dependencies) is a framework-agnostic React component library with a built-in Flexible Licensing Guard for gating PRO&lt;em&gt;/ENT&lt;/em&gt; features in SaaS dashboards. Works with Vite, Next.js, Electron, and Atlassian Forge out of the box.&lt;/p&gt;
&lt;h4 id=&quot;why-this-exists&quot;&gt;Why this exists&lt;/h4&gt;
&lt;p&gt;Every SaaS dashboard eventually needs to answer: &lt;em&gt;is this user allowed to see this feature?&lt;/em&gt; Usually that turns into a one-off component, duplicated across every internal tool a team ships. This package makes that component reusable and auditable instead of reinvented per project.&lt;/p&gt;
&lt;h4 id=&quot;how-it-works&quot;&gt;How it works&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;UniversalSaaSWidget&lt;/code&gt; component takes a license token, checks its prefix (&lt;code&gt;PRO_&lt;/code&gt; or &lt;code&gt;ENT_&lt;/code&gt;) either locally or against a backend gateway, and renders the corresponding tier — with a real-time telemetry state (verifying / verified / error) so the UI never sits in an ambiguous loading state.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @seosiri/developer-ui-kit
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { UniversalSaaSWidget } from '@seosiri/developer-ui-kit';

&amp;lt;UniversalSaaSWidget
  productName=&quot;SEOSiri Analytics Panel&quot;
  productVersion=&quot;1.0.1&quot;
  accentColor=&quot;#0284c7&quot;
  theme=&quot;dark&quot;
  onProTask={() =&amp;gt; alert('Executing secure enterprise pipeline task...')}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;what-s-actually-notable-here&quot;&gt;What's actually notable here&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;0 dependencies&lt;/strong&gt; — verifiable on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/@seosiri/developer-ui-kit&quot;&gt;npm page&lt;/a&gt; itself, not a marketing claim.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Framework-agnostic by construction&lt;/strong&gt; — no CSS provider, no context wrapper; it's pure React with encapsulated inline styles, which is why it compiles across Vite/Next.js/Electron/Forge without extra config.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MIT licensed&lt;/strong&gt;, source on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/SEOSiri-Official/developer-ui-kit&quot;&gt;GitHub&lt;/a&gt; — open core, with the licensing guard controlling &lt;em&gt;feature access&lt;/em&gt;, not &lt;em&gt;code access&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;worth-discussing&quot;&gt;Worth discussing&lt;/h4&gt;
&lt;p&gt;If you're doing something similar — gating features by license tier in a React dashboard — how are you currently handling it? Custom hook, context provider, or something like this baked into a component? Curious what other teams have converged on.&lt;/p&gt;
&lt;p&gt;Full technical breakdown (with architecture diagram and a build-vs-buy comparison table): &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.seosiri.com/2026/09/developer-ui-kit.html&quot;&gt;https://www.seosiri.com/2026/09/developer-ui-kit.html&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28404/seosiri-developer-ui-kit-a-zero-dependency-react-licensing-guard-for-saas-dashboards</guid>
<pubDate>Thu, 24 Sep 2026 03:38:24 +0000</pubDate>
</item>
<item>
<title>🔥 30 Days of Frontend - Day 1</title>
<link>https://coderlegion.com/28384/30-days-of-frontend-day-1</link>
<description>&lt;h3 id=&quot;what-actually-happens-when-state-changes-in-react-&quot;&gt;What Actually Happens When State Changes in React?&lt;/h3&gt;
&lt;p&gt;When state changes, many developers think:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;setState() → React updates the DOM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's a useful simplified model, but there's more happening in between.&lt;/p&gt;
&lt;p&gt;A better mental model is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;State / Props change
        ↓
React schedules a render
        ↓
Component runs again
        ↓
React reconciles the new tree
        ↓
React commits the required DOM changes
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;1-state-or-props-change&quot;&gt;1. State or props change&lt;/h4&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const [count, setCount] = useState(0);

setCount(count + 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The state update tells React that the component needs to be rendered again.&lt;/p&gt;
&lt;h4 id=&quot;2-the-component-runs-again&quot;&gt;2. The component runs again&lt;/h4&gt;
&lt;p&gt;React calls the component function again using the new state:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function Counter() {
  const [count, setCount] = useState(0);

  return &amp;lt;button&amp;gt;{count}&amp;lt;/button&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is what we commonly call a &lt;strong&gt;re-render&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;3-react-creates-the-new-element-tree&quot;&gt;3. React creates the new element tree&lt;/h4&gt;
&lt;p&gt;The component produces a new React element tree based on the current state and props.&lt;/p&gt;
&lt;p&gt;React now has:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Previous tree
      ↓
New tree
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It needs to determine what actually changed.&lt;/p&gt;
&lt;h4 id=&quot;4-react-reconciles-the-trees&quot;&gt;4. React reconciles the trees&lt;/h4&gt;
&lt;p&gt;React compares the new result with the previous one.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Before: &amp;lt;button&amp;gt;0&amp;lt;/button&amp;gt;
After:  &amp;lt;button&amp;gt;1&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React doesn't need to recreate the entire DOM.&lt;/p&gt;
&lt;p&gt;It determines the necessary update.&lt;/p&gt;
&lt;h4 id=&quot;5-react-commits-the-changes&quot;&gt;5. React commits the changes&lt;/h4&gt;
&lt;p&gt;After React determines what needs to change, it commits those changes to the actual DOM.&lt;/p&gt;
&lt;p&gt;So remember:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;A re-render does NOT mean the entire DOM is recreated.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This distinction is important when thinking about React performance.&lt;/p&gt;
&lt;p&gt;A component rendering again isn't automatically a performance problem.&lt;/p&gt;
&lt;p&gt;Instead, ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Did this render produce unnecessary work?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's where tools such as &lt;strong&gt;React DevTools Profiler&lt;/strong&gt; become useful. They can help you identify expensive or unnecessary rendering before you start adding optimizations.&lt;/p&gt;
&lt;h3 id=&quot;the-mental-model&quot;&gt;The mental model&lt;/h3&gt;
&lt;p&gt;Keep this simple flow in mind:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Update
  ↓
Render
  ↓
Reconcile
  ↓
Commit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Understanding this flow makes React performance optimization much easier to reason about.&lt;/p&gt;
&lt;p&gt;Before reaching for &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, or &lt;code&gt;React.memo&lt;/code&gt;, understand what is actually causing the work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Profile first. Optimize second.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/28384/30-days-of-frontend-day-1</guid>
<pubDate>Wed, 23 Sep 2026 20:38:13 +0000</pubDate>
</item>
<item>
<title>Optimizing User Idle Detection in React</title>
<link>https://coderlegion.com/28208/optimizing-user-idle-detection-in-react</link>
<description>&lt;h4 id=&quot;the-challenge-of-responsive-idle-tracking&quot;&gt;The Challenge of Responsive Idle Tracking&lt;/h4&gt;
&lt;p&gt;Tracking user inactivity is essential for modern web applications—whether it's auto-saving drafts, timing out sessions, or pausing resource-heavy animations. However, naïve implementations of activity listeners often lead to performance bottlenecks due to excessive event firing.&lt;/p&gt;
&lt;p&gt;In the latest update to &lt;code&gt;react-hook-lab&lt;/code&gt;, I have refined the &lt;code&gt;useIdle&lt;/code&gt; hook to implement dynamic throttling, ensuring that your state updates remain performant even during high-frequency user interactions.&lt;/p&gt;
&lt;h4 id=&quot;what-changed--2&quot;&gt;What Changed?&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;useIdle&lt;/code&gt; hook now calculates its throttle threshold dynamically based on your defined timeout. By capping the throttle at half the duration of your timeout, we ensure the hook stays responsive while preventing unnecessary render cycles.&lt;/p&gt;
&lt;h4 id=&quot;usage-example&quot;&gt;Usage Example&lt;/h4&gt;
&lt;p&gt;Basic usage to track inactivity after 5 seconds:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { useIdle } from 'react-hook-lab';

const App = () =&amp;gt; {
  const isIdle = useIdle(5000);

  return &amp;lt;div&amp;gt;User is {isIdle ? 'Idle' : 'Active'}&amp;lt;/div&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For scenarios with shorter timeouts, the logic automatically adjusts the throttle to maintain accuracy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Automatically scales throttling for a 1s timeout
const isQuickIdle = useIdle(1000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;resources-1&quot;&gt;Resources&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Saurav-TB-Pandey/react-hook-lab&quot;&gt;GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;NPM Package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/pandeysaurav/&quot;&gt;LinkedIn Profile&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/better-performance-enhancing-idle.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28208/optimizing-user-idle-detection-in-react</guid>
<pubDate>Tue, 22 Sep 2026 17:38:46 +0000</pubDate>
</item>
<item>
<title>Why Your Next.js Modal Route Works Perfectly Until Someone Refreshes the Page</title>
<link>https://coderlegion.com/28121/why-your-next-js-modal-route-works-perfectly-until-someone-refreshes-the-page</link>
<description>&lt;p&gt;This one isn't really a bug, it's a genuinely common point of confusion about a feature working exactly as designed, and understanding why it behaves this way changes how you'd actually build around it, rather than fighting it.&lt;/p&gt;
&lt;h3 id=&quot;the-pattern-a-photo-grid-that-opens-items-as-a-modal&quot;&gt;The Pattern: A Photo Grid That Opens Items as a Modal&lt;/h3&gt;
&lt;p&gt;Intercepting routes are what makes an Instagram-style interaction possible in the App Router, click a photo in a grid, it opens as a modal overlay on top of the current page, without a full navigation, while the URL still updates to something shareable and bookmarkable.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app/
├── @modal/
│   ├── default.tsx
│   └── (.)photo/
│       └── [id]/
│           └── page.tsx    // renders as a modal when intercepted
├── photo/
│   └── [id]/
│       └── page.tsx        // the actual full page
├── layout.tsx
└── page.tsx                 // the grid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Click a photo from the grid, and the &lt;code&gt;(.)photo/[id]&lt;/code&gt; route intercepts the navigation, rendering the modal version in the &lt;code&gt;@modal&lt;/code&gt; slot on top of the grid, without a full page transition. The URL correctly updates to &lt;code&gt;/photo/123&lt;/code&gt;. It feels seamless, and it's a genuinely well-built pattern for exactly this kind of interaction.&lt;/p&gt;
&lt;h3 id=&quot;where-the-confusion-starts&quot;&gt;Where the Confusion Starts&lt;/h3&gt;
&lt;p&gt;Someone building this, testing by clicking through the grid, sees exactly the intended modal behavior. Then they refresh the page while the modal is open, or share that &lt;code&gt;/photo/123&lt;/code&gt; URL with someone else, or that someone else pastes it directly into a new browser tab, and instead of the modal, they get the full, standalone &lt;code&gt;photo/[id]/page.tsx&lt;/code&gt; page, no grid behind it, no modal chrome, just the plain page rendering on its own.&lt;/p&gt;
&lt;p&gt;The natural first reaction is that something's broken, the modal &quot;isn't working&quot; on refresh. It's actually working exactly as designed, just not in the way that first reaction assumes.&lt;/p&gt;
&lt;h3 id=&quot;why-this-is-correct-not-broken&quot;&gt;Why This Is Correct, Not Broken&lt;/h3&gt;
&lt;p&gt;Interception is specifically a client-side navigation behavior. It only intercepts a navigation that happens through Next.js's client-side router, clicking a &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt;, calling &lt;code&gt;router.push()&lt;/code&gt;, moving from one already-loaded page to another within the same app session. A hard refresh, a direct URL visit, or someone opening that link in a completely fresh browser tab isn't a client-side navigation at all, it's a fresh, full server request for that specific URL, with no prior page state to intercept from, no grid already rendered behind it to interrupt. Next.js correctly falls back to rendering the actual underlying route, the full page, because that's genuinely the only thing that makes sense in a context where there's no previous client-side navigation to interrupt in the first place.&lt;/p&gt;
&lt;p&gt;This is why the folder structure includes both the intercepted route (&lt;code&gt;(.)photo/[id]&lt;/code&gt;, the modal version) and the actual route (&lt;code&gt;photo/[id]&lt;/code&gt;, the full page) as separate, real routes. The full page isn't a fallback or an error state, it's the intended, correct experience for anyone arriving at that URL directly, someone who shared or bookmarked the link, a search engine crawling it, anyone without an existing client-side session to interrupt.&lt;/p&gt;
&lt;h3 id=&quot;the-actual-design-implication&quot;&gt;The Actual Design Implication&lt;/h3&gt;
&lt;p&gt;This isn't something to work around, it's something to design for deliberately. The full, non-modal version of the route needs to be a genuinely complete, standalone page on its own merits, not a stripped-down fallback that only half-works, since a real share of your actual traffic to that URL, direct visits, refreshes, shared links, search engines, will land there specifically, not on the modal version.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/photo/[id]/page.tsx
// This needs to be a genuinely complete page, not an afterthought,
// since direct visits, refreshes, and shared links all land here specifically
export default async function PhotoPage({ params }) {
  const { id } = await params;
  const photo = await getPhoto(id);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;BackToGridLink /&amp;gt; {/* since there's no grid rendered behind this version */}
      &amp;lt;PhotoDisplay photo={photo} /&amp;gt;
      &amp;lt;PhotoMetadata photo={photo} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Including something like a clear way back to the grid, since this version genuinely doesn't have the grid rendered behind it the way the modal does, treats this as the real, complete experience it needs to be for a meaningful share of actual visitors, rather than assuming everyone always arrives via the modal path.&lt;/p&gt;
&lt;h3 id=&quot;why-this-actually-matters-for-seo-too&quot;&gt;Why This Actually Matters for SEO Too&lt;/h3&gt;
&lt;p&gt;This split has a genuine upside worth being deliberate about. Since the full page is a real, standalone route, it's exactly what search engines actually crawl and index, a real, complete, server-rendered page for that specific photo, with its own metadata, its own content, rather than something only reachable through a client-side modal interaction a crawler would never trigger. Building the full page version to be genuinely complete isn't just handling an edge case gracefully, it's the version doing real SEO work for that specific piece of content.&lt;/p&gt;
&lt;h3 id=&quot;the-actual-rule-3&quot;&gt;The Actual Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Intercepting routes are a client-side navigation enhancement layered on top of real, independently functional routes, not a replacement for them.&lt;/strong&gt; The full, non-intercepted version of the route needs to be built as a genuinely complete experience on its own, since refreshes, direct visits, shared links, and search engine crawlers all land there specifically, not on the modal version, and treating it as an afterthought means a real share of your actual traffic gets an incomplete experience.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you're building or have built an intercepting route pattern, worth checking whether the full, non-modal version genuinely stands on its own, or was built assuming everyone always arrives through the modal. Drop your experience with this pattern in the comments, curious how many people hit this exact confusion the first time before understanding why it's actually correct.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28121/why-your-next-js-modal-route-works-perfectly-until-someone-refreshes-the-page</guid>
<pubDate>Tue, 22 Sep 2026 08:14:06 +0000</pubDate>
</item>
<item>
<title>Building an Interactive Knowledge Graph with React: The MyZubster MONERO Implementation</title>
<link>https://coderlegion.com/27922/building-an-interactive-knowledge-graph-with-react-the-myzubster-monero-implementation</link>
<description>&lt;h2 id=&quot;building-an-interactive-knowledge-graph-with-react-the-myzubster-monero-implementation&quot;&gt;Building an Interactive Knowledge Graph with React: The MyZubster MONERO Implementation&lt;/h2&gt;
&lt;p&gt;We recently added an interactive Knowledge Graph to MyZubster, starting with the &lt;code&gt;MONERO / ART-001&lt;/code&gt; knowledge node.&lt;/p&gt;
&lt;p&gt;The objective was straightforward: move beyond isolated documentation pages and create a navigable network connecting articles, concepts, sources, and related topics.&lt;/p&gt;
&lt;p&gt;You can explore the first node here:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://myzubster-knowledge.vercel.app/conoscenze?domain=MONERO&amp;amp;id=ART-001&quot;&gt;https://myzubster-knowledge.vercel.app/conoscenze?domain=MONERO&amp;amp;id=ART-001&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The problem&lt;/h3&gt;
&lt;p&gt;Traditional documentation usually follows a hierarchical structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Category
└── Article
    └── Content
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works for reading individual pages, but it does not clearly reveal how knowledge is connected.&lt;/p&gt;
&lt;p&gt;An article about Monero may be related to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;privacy;&lt;/li&gt;
&lt;li&gt;open-source development;&lt;/li&gt;
&lt;li&gt;transaction mechanisms;&lt;/li&gt;
&lt;li&gt;official documentation;&lt;/li&gt;
&lt;li&gt;other MyZubster articles.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We wanted the interface to expose these relationships directly.&lt;/p&gt;
&lt;p&gt;The resulting model looks more like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ART-001
├── related → ART-002
├── related → ART-003
├── source → monero-docs
├── concept → Privacy
└── concept → Open Source
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;stable-knowledge-urls&quot;&gt;Stable knowledge URLs&lt;/h3&gt;
&lt;p&gt;Each article is identified by two parameters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;domain + id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;domain = MONERO
id = ART-001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The public URL becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/conoscenze?domain=MONERO&amp;amp;id=ART-001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach gives every knowledge node a stable and shareable address.&lt;/p&gt;
&lt;p&gt;The same frontend can later support additional domains:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;MONERO / ART-001
KEFIR / ART-001
PERMACULTURE / ART-001
SOUND-SYSTEM / ART-001
CYBERSECURITY / ART-001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The user interface does not need a separate implementation for every domain.&lt;/p&gt;
&lt;h3 id=&quot;current-knowledge-nodes&quot;&gt;Current knowledge nodes&lt;/h3&gt;
&lt;p&gt;The initial MONERO graph includes three articles:&lt;/p&gt;
&lt;h4 id=&quot;art-001-introduction-to-monero&quot;&gt;ART-001 — Introduction to Monero&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://myzubster-knowledge.vercel.app/conoscenze?domain=MONERO&amp;amp;id=ART-001&quot;&gt;https://myzubster-knowledge.vercel.app/conoscenze?domain=MONERO&amp;amp;id=ART-001&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is the root node. It connects the introductory article to its sources, concepts, and related articles.&lt;/p&gt;
&lt;h4 id=&quot;art-002-privacy-in-monero&quot;&gt;ART-002 — Privacy in Monero&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://myzubster-knowledge.vercel.app/conoscenze?domain=MONERO&amp;amp;id=ART-002&quot;&gt;https://myzubster-knowledge.vercel.app/conoscenze?domain=MONERO&amp;amp;id=ART-002&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This node introduces the privacy-related branch of the graph.&lt;/p&gt;
&lt;h4 id=&quot;art-003-monero-transactions&quot;&gt;ART-003 — Monero Transactions&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://myzubster-knowledge.vercel.app/conoscenze?domain=MONERO&amp;amp;id=ART-003&quot;&gt;https://myzubster-knowledge.vercel.app/conoscenze?domain=MONERO&amp;amp;id=ART-003&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This node provides the initial connection to transaction-related knowledge.&lt;/p&gt;
&lt;h3 id=&quot;the-react-implementation&quot;&gt;The React implementation&lt;/h3&gt;
&lt;p&gt;MyZubster currently uses React 18 with Create React App.&lt;/p&gt;
&lt;p&gt;The Knowledge Graph feature introduced:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;frontend/src/data/knowledge.js
frontend/src/pages/KnowledgeGraphPage.js
frontend/src/pages/KnowledgeGraphPage.css
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main application route was also updated in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;frontend/src/App.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both of these paths open the graph:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/conoscenze
/knowledge
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-data-model&quot;&gt;The data model&lt;/h3&gt;
&lt;p&gt;The current implementation stores structured knowledge records in the frontend.&lt;/p&gt;
&lt;p&gt;A simplified article looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;'MONERO:ART-001': {
  id: 'ART-001',
  domain: 'MONERO',
  title: 'Introduction to Monero',
  summary: 'Fundamental concepts of Monero, privacy, and connected documentation.',

  tags: [
    'monero',
    'privacy',
    'open-source'
  ],

  relations: [
    {
      id: 'ART-002',
      label: 'Privacy'
    },
    {
      id: 'ART-003',
      label: 'Transactions'
    }
  ],

  sources: [
    {
      id: 'SRC-001',
      title: 'monero-docs',
      url: 'https://www.getmonero.org/resources/user-guides/'
    }
  ],

  concepts: [
    {
      id: 'CON-PRIVACY',
      title: 'Privacy'
    },
    {
      id: 'CON-OSS',
      title: 'Open Source'
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The lookup key combines the domain and article ID:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const article = knowledgeArticles[`${domain}:${id}`];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This avoids collisions between articles belonging to different domains.&lt;/p&gt;
&lt;h3 id=&quot;reading-the-url&quot;&gt;Reading the URL&lt;/h3&gt;
&lt;p&gt;The page reads the selected article from the query string:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const params = new URLSearchParams(window.location.search);

const domain = (
  params.get('domain') || 'MONERO'
).toUpperCase();

const id = (
  params.get('id') || 'ART-001'
).toUpperCase();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If no parameters are supplied, the interface opens &lt;code&gt;MONERO / ART-001&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If the requested record does not exist, the page returns a clear article-not-found state.&lt;/p&gt;
&lt;h3 id=&quot;generating-graph-nodes&quot;&gt;Generating graph nodes&lt;/h3&gt;
&lt;p&gt;The page converts the selected knowledge record into visual nodes.&lt;/p&gt;
&lt;p&gt;The root article becomes the central node:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  key: `a:${article.id}`,
  id: article.id,
  type: 'article',
  title: article.title,
  description: article.summary
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Related articles, sources, and concepts are then added around it.&lt;/p&gt;
&lt;p&gt;Different prefixes prevent identifier collisions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;a:ART-001
s:SRC-001
c:CON-PRIVACY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is important because an article, source, and concept could theoretically use similar identifiers.&lt;/p&gt;
&lt;h3 id=&quot;graph-interaction&quot;&gt;Graph interaction&lt;/h3&gt;
&lt;p&gt;Users can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;select a node;&lt;/li&gt;
&lt;li&gt;inspect its title, type, identifier, and description;&lt;/li&gt;
&lt;li&gt;filter nodes by type;&lt;/li&gt;
&lt;li&gt;open related articles;&lt;/li&gt;
&lt;li&gt;open external sources;&lt;/li&gt;
&lt;li&gt;share the current article URL.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The available filters are:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;All
Articles
Sources
Concepts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A related article is opened by updating the URL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;window.location.assign(
  `/conoscenze?domain=${domain}&amp;amp;id=${node.id}`
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This reloads the same React page with another knowledge record.&lt;/p&gt;
&lt;h3 id=&quot;how-to-explore-the-graph&quot;&gt;How to explore the graph&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Open the root node:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://myzubster-knowledge.vercel.app/conoscenze?domain=MONERO&amp;amp;id=ART-001&quot;&gt;https://myzubster-knowledge.vercel.app/conoscenze?domain=MONERO&amp;amp;id=ART-001&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select one of the surrounding nodes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Read its metadata in the information panel.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use the filters to isolate articles, sources, or concepts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select a related article and choose “Open article.”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copy the browser URL to share that exact knowledge node.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;how-developers-can-add-an-article&quot;&gt;How developers can add an article&lt;/h3&gt;
&lt;p&gt;Open:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;frontend/src/data/knowledge.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add a new record:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;'MONERO:ART-004': {
  id: 'ART-004',
  domain: 'MONERO',
  title: 'New Monero Article',
  summary: 'A concise explanation of the subject.',

  tags: [
    'monero',
    'documentation'
  ],

  relations: [
    {
      id: 'ART-001',
      label: 'Introduction'
    }
  ],

  sources: [
    {
      id: 'SRC-002',
      title: 'Source title',
      url: 'https://example.org/source'
    }
  ],

  concepts: [
    {
      id: 'CON-EXAMPLE',
      title: 'Example concept'
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The article will then be available at:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/conoscenze?domain=MONERO&amp;amp;id=ART-004
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make it discoverable from another node, add a reciprocal or relevant relationship to an existing article.&lt;/p&gt;
&lt;h3 id=&quot;build-and-verification&quot;&gt;Build and verification&lt;/h3&gt;
&lt;p&gt;After modifying the knowledge data, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd frontend
npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The production build should complete without compilation errors.&lt;/p&gt;
&lt;p&gt;Then test the new route:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/conoscenze?domain=MONERO&amp;amp;id=ART-004
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before submitting the change, verify that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the article ID is unique within its domain;&lt;/li&gt;
&lt;li&gt;every related article exists;&lt;/li&gt;
&lt;li&gt;source URLs are correct;&lt;/li&gt;
&lt;li&gt;titles and summaries are clear;&lt;/li&gt;
&lt;li&gt;the mobile layout remains usable;&lt;/li&gt;
&lt;li&gt;unsupported claims are not presented as verified facts.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;contribution-guidelines&quot;&gt;Contribution guidelines&lt;/h3&gt;
&lt;p&gt;A useful knowledge contribution should include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable article ID;&lt;/li&gt;
&lt;li&gt;the correct domain;&lt;/li&gt;
&lt;li&gt;a descriptive title;&lt;/li&gt;
&lt;li&gt;a concise summary;&lt;/li&gt;
&lt;li&gt;relevant tags;&lt;/li&gt;
&lt;li&gt;relationships with existing nodes;&lt;/li&gt;
&lt;li&gt;reliable sources;&lt;/li&gt;
&lt;li&gt;clearly named concepts;&lt;/li&gt;
&lt;li&gt;limitations or uncertainty where appropriate.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A graph relationship indicates that two subjects are connected. It does not automatically prove a claim or imply scientific validation, partnership, or endorsement.&lt;/p&gt;
&lt;p&gt;Source provenance and review status remain essential.&lt;/p&gt;
&lt;h3 id=&quot;deployment-workflow&quot;&gt;Deployment workflow&lt;/h3&gt;
&lt;p&gt;The feature was developed on a dedicated Git branch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;feature/knowledge-graph-monero
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The frontend production build was verified before the branch was rebased onto the latest &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The final implementation was merged into &lt;code&gt;main&lt;/code&gt; and pushed to the public repository.&lt;/p&gt;
&lt;p&gt;Source repository:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Implementation commit:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/commit/43c7d966&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster/commit/43c7d966&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;next-steps&quot;&gt;Next steps&lt;/h3&gt;
&lt;p&gt;The current version uses frontend data to establish the interaction model.&lt;/p&gt;
&lt;p&gt;A future version can move the knowledge records into a dedicated API or versioned content repository.&lt;/p&gt;
&lt;p&gt;Potential improvements include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;server-side schema validation;&lt;/li&gt;
&lt;li&gt;automatic backlinks;&lt;/li&gt;
&lt;li&gt;multi-level graph expansion;&lt;/li&gt;
&lt;li&gt;full-text search;&lt;/li&gt;
&lt;li&gt;contributor attribution;&lt;/li&gt;
&lt;li&gt;review and publication states;&lt;/li&gt;
&lt;li&gt;source provenance;&lt;/li&gt;
&lt;li&gt;revision history;&lt;/li&gt;
&lt;li&gt;API-driven graph generation;&lt;/li&gt;
&lt;li&gt;integration with Zorgax;&lt;/li&gt;
&lt;li&gt;support for additional knowledge domains.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The long-term objective is not simply to collect articles. It is to create a community knowledge network where people can explore how information, evidence, concepts, and practical experience relate to one another.&lt;/p&gt;
&lt;p&gt;Try the first node:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://myzubster-knowledge.vercel.app/conoscenze?domain=MONERO&amp;amp;id=ART-001&quot;&gt;https://myzubster-knowledge.vercel.app/conoscenze?domain=MONERO&amp;amp;id=ART-001&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Repository:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;react-javascript-knowledgegraph-opensource-webdevelopment-monero-knowledgemanagement-myzubster&quot;&gt;React #JavaScript #KnowledgeGraph #OpenSource #WebDevelopment #Monero #KnowledgeManagement #MyZubster&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27922/building-an-interactive-knowledge-graph-with-react-the-myzubster-monero-implementation</guid>
<pubDate>Mon, 21 Sep 2026 10:19:43 +0000</pubDate>
</item>
<item>
<title>⚛️ Controlled vs Uncontrolled Components in React – A Deep Dive</title>
<link>https://coderlegion.com/27766/controlled-vs-uncontrolled-components-in-react-a-deep-dive</link>
<description>&lt;p&gt;When building forms in React, you’ll inevitably come across &lt;strong&gt;controlled&lt;/strong&gt; and &lt;strong&gt;uncontrolled&lt;/strong&gt; components.&lt;br&gt;
At first glance, they look similar — both accept user input and manage form data.&lt;br&gt;
But how they manage that data under the hood can significantly impact &lt;strong&gt;performance, maintainability and UX&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In this blog, let’s break down the difference, see them in action, explore real-world issues (like cursor position and undo/redo bugs), and understand when to use each.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-what-are-controlled-components-&quot;&gt;🧠 What Are Controlled Components?&lt;/h3&gt;
&lt;p&gt;In &lt;strong&gt;controlled components&lt;/strong&gt;, React state is the &lt;strong&gt;single source of truth&lt;/strong&gt;.&lt;br&gt;
That means every keystroke or change updates the component’s state and that state determines what’s rendered.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { useState } from &quot;react&quot;;

function ControlledInput() {
  const [name, setName] = useState(&quot;&quot;);

  return (
    &amp;lt;input
      value={name}
      onChange={(e) =&amp;gt; setName(e.target.value)}
      placeholder=&quot;Type your name&quot;
    /&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, the &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; value is fully controlled by React via the &lt;code&gt;name&lt;/code&gt; state.&lt;br&gt;
Each keystroke triggers &lt;code&gt;setName()&lt;/code&gt;, re-rendering the component with the updated value.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Easy to validate or transform input on change.&lt;/li&gt;
&lt;li&gt;Centralized state makes debugging and unit testing easier.&lt;/li&gt;
&lt;li&gt;Integrates seamlessly with complex UIs or form libraries (Formik, React Hook Form).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frequent re-renders can impact performance in large forms.&lt;/li&gt;
&lt;li&gt;Need to carefully manage performance and input lag.&lt;/li&gt;
&lt;li&gt;Cursor position bugs can occur with advanced state management.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-what-are-uncontrolled-components-&quot;&gt;🧩 What Are Uncontrolled Components?&lt;/h3&gt;
&lt;p&gt;In &lt;strong&gt;uncontrolled components&lt;/strong&gt;, the DOM maintains its own state — React just &lt;strong&gt;references&lt;/strong&gt; it.&lt;br&gt;
You don’t handle every keystroke; instead, you access the value when needed.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { useRef } from &quot;react&quot;;

function UncontrolledInput() {
  const inputRef = useRef();

  const handleSubmit = () =&amp;gt; {
    alert(`Input value: ${inputRef.current.value}`);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;input ref={inputRef} placeholder=&quot;Type your name&quot; /&amp;gt;
      &amp;lt;button onClick={handleSubmit}&amp;gt;Submit&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ &lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Better performance for simple forms.&lt;/li&gt;
&lt;li&gt;Minimal React re-renders.&lt;/li&gt;
&lt;li&gt;Perfect when you just need the final value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Harder to validate or modify values dynamically.&lt;/li&gt;
&lt;li&gt;Not ideal when input data must always reflect application state.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-controlled-vs-uncontrolled-key-differences&quot;&gt;⚔️ Controlled vs Uncontrolled: Key Differences&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Feature         &lt;/th&gt;&lt;th&gt;Controlled                 &lt;/th&gt;&lt;th&gt;Uncontrolled&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Data Source&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;React State                &lt;/td&gt;&lt;td&gt;DOM (via refs)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;May re-render often        &lt;/td&gt;&lt;td&gt;Faster for simple inputs&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Validation&lt;/strong&gt;  &lt;/td&gt;&lt;td&gt;Easy to handle dynamically &lt;/td&gt;&lt;td&gt;Requires manual check&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Use Case&lt;/strong&gt;    &lt;/td&gt;&lt;td&gt;Complex, validated forms   &lt;/td&gt;&lt;td&gt;Simple or performance-critical inputs&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-valtio-amp-controlled-inputs-why-cursor-jumps-happen&quot;&gt;⚡ Valtio &amp;amp; Controlled Inputs: Why Cursor Jumps Happen&lt;/h3&gt;
&lt;p&gt;When you use a state library like Valtio in a React controlled input, you might encounter this issue: you edit text in the middle of the input, but the cursor jumps to the end. This frustrates users and breaks the typing experience.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🔍 What’s going on?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;By default, Valtio batches updates before triggering re-renders. The documentation states:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“By default, state mutations are batched before triggering re-render. Sometimes, we want to disable the batching.”&lt;br&gt;
Because of this batching behavior, when you type and update the proxy state, multiple changes may be queued and then re-rendered in one go. During that delay, the browser loses context of the input cursor position, so after render the input’s value is applied and the cursor jumps to the end.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;✅ The fix: { sync: true }&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Valtio provides the option to disable batching for specific use-cases like inputs:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const snap = useSnapshot(state, { sync: true });&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;sync: true&lt;/code&gt;, updates go through immediately rather than being deferred/batched. This keeps the input cursor where the user expects it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Trade-off:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;sync: true&lt;/code&gt; disables batching optimizations, which can reduce render efficiency in large/complex components. Therefore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;sync: true&lt;/code&gt; only in input fields where cursor behaviour matters.&lt;/li&gt;
&lt;li&gt;Avoid it for general state where performance is more important than instantaneous update.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-undo-redo-cmd-z-cmd-shift-z-in-controlled-components&quot;&gt;🔁 Undo/Redo (Cmd+Z / Cmd+Shift+Z) in Controlled Components&lt;/h3&gt;
&lt;p&gt;Another subtle but important issue:&lt;br&gt;
Undo (&lt;code&gt;Cmd+Z&lt;/code&gt;) and Redo (&lt;code&gt;Cmd+Shift+Z&lt;/code&gt;) &lt;strong&gt;might not work&lt;/strong&gt; as expected in controlled inputs.&lt;/p&gt;
&lt;p&gt;Why?&lt;br&gt;
Because React replaces the entire value on every keystroke rather than letting the browser handle native input history.&lt;/p&gt;
&lt;p&gt;This means the browser sometimes &lt;strong&gt;loses the input undo stack&lt;/strong&gt;, especially if re-renders happen between updates.&lt;/p&gt;
&lt;p&gt;🧪 &lt;strong&gt;What to do:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Test undo/redo behavior in all input fields before handing over to QA.&lt;/li&gt;
&lt;li&gt;If using a state library or custom hooks, check whether it maintains the input history properly.&lt;/li&gt;
&lt;li&gt;In cases where it fails and browser-native undo is critical (e.g., long text editors), consider using &lt;strong&gt;uncontrolled inputs&lt;/strong&gt; or controlled wrappers that debounce updates.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-best-practices-for-react-forms&quot;&gt;🧰 Best Practices for React Forms&lt;/h3&gt;
&lt;p&gt;✅ Use &lt;strong&gt;controlled inputs&lt;/strong&gt; when you need validation, dynamic control or shared state.&lt;br&gt;
✅ Use &lt;strong&gt;uncontrolled inputs&lt;/strong&gt; when performance and simplicity matter.&lt;br&gt;
✅ For state libraries like &lt;strong&gt;Valtio&lt;/strong&gt;, test cursor and undo behaviors thoroughly.&lt;br&gt;
✅ Avoid unnecessary re-renders – use &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt; or split components.&lt;br&gt;
✅ Always test form UX — typing, cursor and undo/redo should feel &lt;strong&gt;native and fluid&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-final-thoughts&quot;&gt;🎯 Final Thoughts&lt;/h3&gt;
&lt;p&gt;Controlled vs Uncontrolled Components seem like a small React concept, but they can deeply impact user experience, especially in &lt;strong&gt;complex form-heavy applications&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;💡 Understanding these nuances (like cursor handling, undo/redo, and batching) helps you ship &lt;strong&gt;smoother, bug-free UIs&lt;/strong&gt; that feel professional and reliable.&lt;/p&gt;
&lt;p&gt;🚀 Whether you use plain React or libraries like Valtio, always test the real typing experience — that’s where users feel the difference.&lt;/p&gt;
&lt;p&gt;💬 Have you ever faced cursor or undo issues in your React forms? Drop your experience or workaround below — let’s learn together! 👇&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27766/controlled-vs-uncontrolled-components-in-react-a-deep-dive</guid>
<pubDate>Sat, 19 Sep 2026 19:08:29 +0000</pubDate>
</item>
<item>
<title>How to Build a Full-Stack AI Tools Directory App (Next.js 14 + Supabase)</title>
<link>https://coderlegion.com/27762/how-to-build-a-full-stack-ai-tools-directory-app-next-js-14-supabase</link>
<description>&lt;p&gt;Looking to build a production-ready web application or launch a SaaS directory? In this comprehensive guide, we build a scalable AI Tools Directory from scratch using a modern tech stack.&lt;/p&gt;
&lt;p&gt;Here is what you will learn in this step-by-step guide:&lt;/p&gt;
&lt;p&gt;🏗️ Tech Stack &amp;amp; Setup: Setting up Next.js 14 (App Router), Tailwind CSS, and TypeScript from scratch.&lt;/p&gt;
&lt;p&gt;🗄️ Database Architecture: Designing PostgreSQL schemas in Supabase with proper indexing and Row Level Security (RLS) policies.&lt;/p&gt;
&lt;p&gt;⚡ API Routes &amp;amp; Filtering: Creating dynamic Next.js API endpoints that support real-time search, category filters, and pagination.&lt;/p&gt;
&lt;p&gt;🎨 Modern UI Components: Building responsive, interactive UI components with Tailwind CSS and Lucide React.&lt;/p&gt;
&lt;p&gt;🔍 SEO Optimization: Implementing dynamic metadata generation and Structured Data (JSON-LD) for tool pages to maximize organic search traffic.&lt;/p&gt;
&lt;p&gt;🚀 Deployment: Deploying the application seamlessly on Vercel with full environment security.&lt;/p&gt;
&lt;p&gt;Check out the full tutorial below and start building your own AI directory today!&lt;/p&gt;
&lt;p&gt;👉 Read the full tutorial here: [https://www.thefluxread.com/2026/09/how-to-build-full-stack-ai-tools.html]&lt;/p&gt;
&lt;h2 id=&quot;nextjs-supabase-webdev-react-javascript-ai-tailwindcss&quot;&gt;nextjs #supabase #webdev #react #javascript #ai #tailwindcss&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27762/how-to-build-a-full-stack-ai-tools-directory-app-next-js-14-supabase</guid>
<pubDate>Sat, 19 Sep 2026 17:02:52 +0000</pubDate>
</item>
<item>
<title>Project LifeOps (Part 5): 24/7 Zero-Cost Cloud Deployment ($0/month), Mobile Optimization, and PWA</title>
<link>https://coderlegion.com/27730/project-lifeops-part-5-24-zero-cost-cloud-deployment-0-month-mobile-optimization-and-pwa</link>
<description>&lt;p&gt;We have arrived at the grand finale of our engineering series. Across the four previous installments, we designed the &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part1_arquitectura_backend/&quot;&gt;database architecture and FastAPI backend&lt;/a&gt;, crafted the &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/&quot;&gt;Glassmorphism Dark Mode React interface&lt;/a&gt;, built the &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/&quot;&gt;interactive fitness, reading, cinema, and Kanban modules&lt;/a&gt;, and developed the &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part4_informes_word_excel/&quot;&gt;in-memory Word and Excel reporting engine&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Yet in software development, a sobering pattern exists: &lt;strong&gt;countless extraordinary personal projects wither and die on &lt;code&gt;localhost:3000&lt;/code&gt; or &lt;code&gt;localhost:8000&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The obstacle is almost always the same: production cloud deployment is often perceived as a labyrinth of daunting server configurations, mounting database subscription fees, or the burden of maintaining live virtual machines. And when aiming to bring web apps to smartphones, developers frequently encounter costly gatekeepers: Apple Developer fees ($99/year), Google Play fees, opaque store review cycles, and massive hybrid frameworks.&lt;/p&gt;
&lt;p&gt;In this final installment, we demonstrate a far more elegant, sustainable path:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Deploying the entire full-stack architecture 24/7 at exactly $0.00/month&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seamlessly handling free cloud container sleep cycles (&lt;em&gt;cold-starts&lt;/em&gt;) on the client side&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Engineering an ultra-ergonomic touch experience optimized for modern smartphones (Android / Samsung Galaxy and iPhone)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transforming the application into an installable Progressive Web App (PWA) added directly to home screens&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;[!TIP]&lt;br&gt;
&lt;strong&gt;Test the live app&lt;/strong&gt;: You can explore the final production release of LifeOps at &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/&quot;&gt;https://datalaria.com/apps/lifeops/&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-complete-lifeops-series-roadmap&quot;&gt;🗺️ Complete LifeOps Series Roadmap&lt;/h4&gt;
&lt;p&gt;With this post, the full 5-part blueprint is complete:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 1&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part1_arquitectura_backend/&quot;&gt;Personal Operating System Architecture and FastAPI + Supabase Backend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 2&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/&quot;&gt;React Frontend with Glassmorphism, 360° Dashboard, and Design System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 3&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/&quot;&gt;Core Interactive Modules: Fitness, Library, Cinema, and Professional Kanban Board&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 4&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part4_informes_word_excel/&quot;&gt;Executive Word (.docx) Reporting Engine and Multi-Sheet Excel (.xlsx) Export&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 5 (This article)&lt;/strong&gt;: 24/7 Zero-Cost Cloud Deployment ($0/month), Mobile UX, and PWA&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;1-production-cloud-topology-0-month-&quot;&gt;1. Production Cloud Topology ($0/month) 🌐☁️&lt;/h4&gt;
&lt;p&gt;To guarantee that LifeOps remains online 24/7 without recurring cloud invoices, we strategically leverage the most generous free tiers available:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│                       CLIENT DEVICE                         │
│           Web Browser / Installed Home Screen PWA           │
│            [https://datalaria.com/apps/lifeops/](https://datalaria.com/apps/lifeops/)              │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│               FRONTEND: Netlify Edge CDN ($0/month)         │
│   • Vite build with Rollup Code Splitting (vendor-*)        │
│   • Instant global edge delivery via Gzip / Brotli          │
│   • Transparent proxy rewrite under Datalaria domain        │
└──────────────────────────────┬──────────────────────────────┘
                               │
            JWT Bearer Tokens  │  HTTPS REST Calls
                               ▼
┌─────────────────────────────────────────────────────────────┐
│               BACKEND: Render.com Web Service ($0/month)    │
│   • FastAPI + Uvicorn container (Python 3.11 / 3.13)        │
│   • Endpoint: [https://lifeops-api.onrender.com](https://lifeops-api.onrender.com)              │
│   • Anti-DoS Rate Limiting (slowapi) + Strict CORS          │
│   • Auto-spins down after 15 minutes of inactivity          │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│              DATABASE: Supabase Cloud ($0/month)            │
│   • Managed PostgreSQL 15+ database instance                │
│   • Isolated `lifeops` schema with Row Level Security (RLS) │
│   • Cryptographic JWT session validation                    │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;1-1-vite-bundle-optimization-vite-config-js-&quot;&gt;1.1. Vite Bundle Optimization (&lt;code&gt;vite.config.js&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;To guarantee sub-second initial loads across 4G and 5G mobile networks, we configured Rollup manual chunking:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  base: './', // Enables serving the SPA from any nested route (/apps/lifeops/)
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('react') || id.includes('react-dom') || id.includes('react-router-dom')) {
              return 'vendor-react';
            }
            if (id.includes('lucide-react')) {
              return 'vendor-icons';
            }
            if (id.includes('@supabase')) {
              return 'vendor-supabase';
            }
            return 'vendor-libs';
          }
        },
      },
    },
    chunkSizeWarningLimit: 600,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By segmenting &lt;code&gt;vendor-react&lt;/code&gt; and &lt;code&gt;@supabase&lt;/code&gt;, user browsers permanently cache stable external dependencies. When application features update, visitors download only a lightweight diff of new business logic.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-conquering-cold-starts-client-side-resilience-&quot;&gt;2. Conquering Cold-Starts: Client-Side Resilience 🛡️⏱️&lt;/h4&gt;
&lt;p&gt;The only operational tradeoff on Render's free tier is container hibernation after 15 minutes of zero traffic.&lt;/p&gt;
&lt;p&gt;When an end-user opens LifeOps after hours of downtime, the first HTTP request requires 25 to 45 seconds while Render allocates a container and initializes Uvicorn. Without proactive client-side handling, users face gateway timeouts (&lt;em&gt;HTTP 504&lt;/em&gt;) or frozen interfaces.&lt;/p&gt;
&lt;p&gt;In LifeOps, we turned this limitation into an &lt;strong&gt;informative, guided user experience&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// lifeops-app/src/components/Dashboard/SystemHealth.jsx
const checkHealth = async (isAutoRetry = false) =&amp;gt; {
  setLoading(true);
  if (!isAutoRetry) setError(null);

  try {
    const data = await api.getHealth();
    setHealth(data);
    setError(null);
  } catch (err) {
    setError(err.message || 'Connecting to API...');
    
    // Auto-retry loop up to 3 times with 6-second delay while Render awakens
    if (retryCount &amp;lt; 3) {
      setTimeout(() =&amp;gt; {
        setRetryCount((prev) =&amp;gt; prev + 1);
      }, 6000);
    }
  } finally {
    setLoading(false);
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;what-happens-behind-the-scenes-&quot;&gt;What happens behind the scenes?&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;The initial health check dispatches a wake-up ping to the backend.&lt;/li&gt;
&lt;li&gt;The UI renders a friendly, transparent notification: &lt;em&gt;&quot;Render instance waking up (Cold start)... retrying automatically&quot;&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The frontend retries in the background every 6 seconds without interrupting client navigation.&lt;/li&gt;
&lt;li&gt;As soon as FastAPI responds with &lt;code&gt;HTTP 200&lt;/code&gt;, the badge shifts to green (&lt;em&gt;&quot;FastAPI Online • Supabase Connected&quot;&lt;/em&gt;), fetching data without requiring a manual page refresh.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/es/posts/app-lifeops_part5_deploy_mobile_pwa/lifeops_cloud_infrastructure_en.png&quot; alt=&quot;Account settings and cloud infrastructure panel connected to Render and Supabase&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-thumb-accessible-mobile-ergonomics-android-amp-ios-&quot;&gt;3. Thumb-Accessible Mobile Ergonomics (Android &amp;amp; iOS) 📱👍&lt;/h4&gt;
&lt;p&gt;Over 70% of daily interactions with a personal operating system occur on smartphones: logging a workout right after a run, cataloging a book during a commute, or reviewing Kanban deliverables from the couch.&lt;/p&gt;
&lt;p&gt;On a 6.5-inch device held in one hand, forcing the user's thumb to reach all the way to the top-left corner for a hamburger menu is poor ergonomic design.&lt;/p&gt;
&lt;p&gt;We anchored the mobile user experience on &lt;strong&gt;three ergonomic pillars&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│ 📱 SMARTPHONE VIEWPORT (&amp;lt;= 768px)                           │
├─────────────────────────────────────────────────────────────┤
│  [Top Header]: LifeOps Logo       [Locale ES/EN] [User Icon]│
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ 🪟 FLOATING MODAL (React Portal)                      │  │
│  │ ───────────────────────────────────────────────────── │  │
│  │ [=== Drag Handle: slide down to dismiss ===]          │  │
│  │                                                       │  │
│  │  Title: Log Workout Session                           │  │
│  │  • 16px font inputs (prevents iOS Safari auto-zoom)   │  │
│  │  • Smooth touch scroll (touch-action: pan-y)          │  │
│  │                                                       │  │
│  │ ───────────────────────────────────────────────────── │  │
│  │ [📌 STICKY FOOTER ACTIONS: [ Cancel ] [ Save Record ]]│  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│ 📌 FIXED BOTTOM NAVIGATION BAR (Height: 64px)               │
│ [ 📊 Dashboard ] [ 🏃 Personal ] [ 💼 Tasks ] [ ⚙️ Settings ]│
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;3-1-fixed-bottom-navigation-bar-bottomnav-jsx-&quot;&gt;3.1. Fixed Bottom Navigation Bar (&lt;code&gt;BottomNav.jsx&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;Stationed permanently along the screen footer (&lt;code&gt;position: fixed; bottom: 0; z-index: 1000;&lt;/code&gt;), it houses 5 prominent touch targets (48x48px touch bounding boxes following Apple and Google accessibility standards), allowing instantaneous section transitions with a thumb tap.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/es/posts/app-lifeops_part5_deploy_mobile_pwa/lifeops_mobile_bottom_nav_en.png&quot; alt=&quot;Smartphone mobile viewport featuring the thumb-accessible fixed bottom navigation bar&quot;&gt;&lt;/p&gt;
&lt;h5 id=&quot;3-2-off-canvas-sliding-drawer&quot;&gt;3.2. Off-Canvas Sliding Drawer&lt;/h5&gt;
&lt;p&gt;The primary navigation sidebar (&lt;code&gt;Sidebar.jsx&lt;/code&gt;) switches behavior based on screen real estate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On Desktop (&amp;gt; 768px)&lt;/strong&gt;: Expands to 260px or collapses to a slim 72px icon bar with floating tooltips, persisted in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On Mobile (&amp;lt;= 768px)&lt;/strong&gt;: Becomes an Off-Canvas drawer sliding in over a frosted backdrop (&lt;code&gt;backdrop-filter: blur(8px)&lt;/code&gt;), auto-dismissing on link navigation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/es/posts/app-lifeops_part5_deploy_mobile_pwa/lifeops_mobile_drawer_en.png&quot; alt=&quot;Off-canvas mobile sliding drawer over frosted glass backdrop with quick links and system status&quot;&gt;&lt;/p&gt;
&lt;h5 id=&quot;3-3-viewport-anchored-modals-with-react-createportal-modal-jsx-&quot;&gt;3.3. Viewport-Anchored Modals with &lt;code&gt;React.createPortal&lt;/code&gt; (&lt;code&gt;Modal.jsx&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;Form modals in mobile CSS are notorious for layout bugs: getting clipped by ancestor containers, misaligning behind virtual keyboards, or burying save buttons beneath the fold.&lt;/p&gt;
&lt;p&gt;In LifeOps, we engineered a clean architectural solution:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// Rendered directly into document.body via React Portal
export function Modal({ isOpen, onClose, title, children }) {
  ...
  if (!isOpen) return null;

  return createPortal(
    &amp;lt;div className=&quot;modal-backdrop&quot; onClick={onClose}&amp;gt;
      &amp;lt;div className=&quot;modal-content&quot; onClick={(e) =&amp;gt; e.stopPropagation()}&amp;gt;
        {/* Touch drag handle for swipe-to-close */}
        &amp;lt;div className=&quot;modal-drag-handle-area&quot; onTouchStart={handleTouchStart} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd}&amp;gt;
          &amp;lt;div className=&quot;modal-drag-pill&quot; /&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div className=&quot;modal-header&quot;&amp;gt;...&amp;lt;/div&amp;gt;
        &amp;lt;div className=&quot;modal-body&quot;&amp;gt;{children}&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;,
    document.body
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Precise Mobile Viewport Anchoring (Modal.css) */
@media (max-width: 768px) {
  .modal-backdrop {
    padding: 12px 10px calc(64px + 12px) 10px; /* Terminates exactly above the bottom nav */
    height: 100dvh; /* Dynamic Viewport Height handles mobile address bars */
  }

  .modal-body {
    touch-action: pan-y !important; /* Smooth touch gesture scrolling */
  }

  .form-group input, .form-group select {
    font-size: 16px !important; /* Prevents unwanted iOS Safari viewport auto-zooming */
  }

  /* Action buttons remain permanently docked in view */
  .form-actions {
    position: sticky !important;
    bottom: 0 !important;
    background: #111827 !important;
    z-index: 30;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;createPortal&lt;/code&gt;, modals break free from ancestor stacking contexts. And thanks to &lt;code&gt;calc(64px + 12px)&lt;/code&gt; and sticky footers, the &lt;strong&gt;Cancel and Save buttons remain permanently anchored in thumb view&lt;/strong&gt;, regardless of form length.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/es/posts/app-lifeops_part5_deploy_mobile_pwa/lifeops_mobile_modal_en.png&quot; alt=&quot;Touch-friendly mobile modal rendered via React Portal with swipe drag handle and sticky footer actions&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;4-progressive-web-app-pwa-transformation-&quot;&gt;4. Progressive Web App (PWA) Transformation 📲✨&lt;/h4&gt;
&lt;p&gt;For an application to feel native on a mobile device, it must install without store middlemen.&lt;/p&gt;
&lt;p&gt;We configured modern meta tags and web app standards in &lt;code&gt;index.html&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover&quot; /&amp;gt;
&amp;lt;meta name=&quot;theme-color&quot; content=&quot;#111827&quot; /&amp;gt;
&amp;lt;meta name=&quot;apple-mobile-web-app-capable&quot; content=&quot;yes&quot; /&amp;gt;
&amp;lt;meta name=&quot;apple-mobile-web-app-status-bar-style&quot; content=&quot;black-translucent&quot; /&amp;gt;
&amp;lt;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;favicon.svg&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;key-benefits-as-an-installed-pwa-&quot;&gt;Key Benefits as an Installed PWA:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Home Screen Presence&lt;/strong&gt;: High-resolution branded icon sitting alongside native apps on Android and iOS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fullscreen Standalone Mode&lt;/strong&gt;: Browser URL bars and navigation chrome disappear, reclaiming 15% of vertical screen space.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status Bar Theming&lt;/strong&gt;: Device battery and clock bars harmonize seamlessly with the &lt;code&gt;#111827&lt;/code&gt; glassmorphic palette via &lt;code&gt;theme-color&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instant Updates&lt;/strong&gt;: New builds deploy live over Git without waiting days for third-party store approvals.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;5-retrospective-lessons-learned-from-the-lifeops-blueprint-&quot;&gt;5. Retrospective: Lessons Learned from the LifeOps Blueprint 🎓💡&lt;/h4&gt;
&lt;p&gt;Architecting and engineering LifeOps from the ground up has been one of the most rewarding journeys documented on Datalaria.&lt;/p&gt;
&lt;p&gt;To conclude our technical journey, here are the &lt;strong&gt;three foundational takeaways&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Relational modeling outperforms flat schemas as telemetry matures&lt;/strong&gt;:&lt;br&gt;
Structuring general events in &lt;code&gt;lifeops.activities&lt;/code&gt; with 1-to-1 extension tables (&lt;code&gt;workouts&lt;/code&gt;, &lt;code&gt;books&lt;/code&gt;, &lt;code&gt;films&lt;/code&gt;) and employing &lt;code&gt;JSONB&lt;/code&gt; for progress timelines delivered superior maintainability over sprawling, null-riddled tables.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deliberate design fosters consistent user adherence&lt;/strong&gt;:&lt;br&gt;
Investing in visual polish (Glassmorphism, hardware-accelerated blurs, Outfit typography, and consistent Lucide iconography) transforms daily logging into an enjoyable ritual rather than a chore.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modern serverless tooling enables $0/month production without compromises&lt;/strong&gt;:&lt;br&gt;
The pairing of &lt;strong&gt;FastAPI + Supabase + React (Vite) + Render + Netlify&lt;/strong&gt; provides reliability, speed, and cryptographic security rivaling commercial SaaS platforms charging $15/month per seat.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;series-conclusion-amp-complete-index-&quot;&gt;Series Conclusion &amp;amp; Complete Index 🚀&lt;/h4&gt;
&lt;p&gt;With this fifth installment, the official &lt;strong&gt;LifeOps&lt;/strong&gt; series stands complete. You have access to the complete source code, architectural rationales, and production implementation details step by step.&lt;/p&gt;
&lt;p&gt;I invite you to try the live application, experiment with its modules, and draw inspiration to build your own bespoke systems guided by Datalaria's core motto: &lt;strong&gt;&quot;Learn by building&quot;&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;complete-series-reference-amp-links-&quot;&gt;Complete Series Reference &amp;amp; Links 🔗&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;LifeOps Live Application&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/&quot;&gt;datalaria.com/apps/lifeops&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Interactive OpenAPI Docs&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://lifeops-api.onrender.com/docs&quot;&gt;lifeops-api.onrender.com/docs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;📚 &lt;strong&gt;Part 1&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part1_arquitectura_backend/&quot;&gt;Personal Operating System Architecture and FastAPI + Supabase Backend&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;🎨 &lt;strong&gt;Part 2&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/&quot;&gt;React Frontend with Glassmorphism, 360° Dashboard, and Design System&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;📋 &lt;strong&gt;Part 3&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/&quot;&gt;Core Interactive Modules: Fitness, Library, Cinema, and Professional Kanban Board&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Part 4&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part4_informes_word_excel/&quot;&gt;Executive Word (.docx) Reporting Engine and Multi-Sheet Excel (.xlsx) Export&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;☁️ &lt;strong&gt;Part 5 (This article)&lt;/strong&gt;: 24/7 Zero-Cost Cloud Deployment ($0/month), Mobile UX, and PWA.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thank you for following along throughout this series! If you are building your own personal operating systems or have ideas for enhancements, share your insights in the comments below. 👇&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27730/project-lifeops-part-5-24-zero-cost-cloud-deployment-0-month-mobile-optimization-and-pwa</guid>
<pubDate>Sat, 19 Sep 2026 10:13:09 +0000</pubDate>
</item>
<item>
<title>Taming Async Race Conditions and Unmounted State in React: What's New in react-hook-lab</title>
<link>https://coderlegion.com/27670/taming-async-race-conditions-and-unmounted-state-in-react-whats-new-in-react-hook-lab</link>
<description>&lt;h2 id=&quot;taming-async-race-conditions-and-unmounted-state-in-react-what-s-new-in-react-hook-lab&quot;&gt;Taming Async Race Conditions and Unmounted State in React: What's New in react-hook-lab&lt;/h2&gt;
&lt;p&gt;Every React developer has encountered the dreaded console warning: &lt;em&gt;&quot;Can't perform a React state update on an unmounted component.&quot;&lt;/em&gt; Or worse, subtle race conditions where an earlier slow network response resolves after a newer one and overwrites valid user data.&lt;/p&gt;
&lt;p&gt;In the latest release of &lt;strong&gt;react-hook-lab&lt;/strong&gt;, we focused on hardening our async and browser hooks against edge cases, memory leaks, and unmounted component state updates.&lt;/p&gt;
&lt;p&gt;Here is a breakdown of the notable updates, behavioral fixes, and how you can leverage them in your applications.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-what-changed-across-the-suite-&quot;&gt;1. What Changed Across the Suite?&lt;/h3&gt;
&lt;h4 id=&quot;async-hooks-useasync-useasyncdebounce-usedebounce-&quot;&gt;Async Hooks (&lt;code&gt;useAsync&lt;/code&gt;, &lt;code&gt;useAsyncDebounce&lt;/code&gt;, &lt;code&gt;useDebounce&lt;/code&gt;)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Request Invalidation&lt;/strong&gt;: &lt;code&gt;useAsync&lt;/code&gt; now tracks request sequences using internal identifiers. If dependencies change or a component unmounts while a promise is in flight, the stale response is safely discarded without mutating component state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Controlled Debounce Dependencies&lt;/strong&gt;: &lt;code&gt;useAsyncDebounce&lt;/code&gt; now supports an explicit dependency list alongside internal ref synchronizations to avoid unnecessary re-triggers or missed parameter updates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preserving Whitespace with &lt;code&gt;useDebounce&lt;/code&gt;&lt;/strong&gt;: By default, &lt;code&gt;useDebounce&lt;/code&gt; trims whitespace on strings. You can now pass &lt;code&gt;{ trim: false }&lt;/code&gt; to strictly preserve raw input spacing—critical for live text editors and formatted inputs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;browser-amp-dom-hooks-useclipboard-usecamera-usedownload-usefullscreen-usecookie-&quot;&gt;Browser &amp;amp; DOM Hooks (&lt;code&gt;useClipboard&lt;/code&gt;, &lt;code&gt;useCamera&lt;/code&gt;, &lt;code&gt;useDownload&lt;/code&gt;, &lt;code&gt;useFullscreen&lt;/code&gt;, &lt;code&gt;useCookie&lt;/code&gt;)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unmount-Safe Timers&lt;/strong&gt;: &lt;code&gt;useClipboard&lt;/code&gt; now exposes a manual &lt;code&gt;reset()&lt;/code&gt; helper and safely tears down pending timeout handlers on unmount, preventing lingering timeouts from calling unmounted state setters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Target-Aware Fullscreen&lt;/strong&gt;: &lt;code&gt;useFullscreen&lt;/code&gt; now strictly tracks whether the active fullscreen element matches the specific target element reference rather than any element on the page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Path-Aware Cookie Verification&lt;/strong&gt;: &lt;code&gt;useCookie&lt;/code&gt; now accounts for route scopes before attempting verification checks against client-side cookies.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-practical-examples&quot;&gt;2. Practical Examples&lt;/h3&gt;
&lt;h4 id=&quot;example-1-robust-copy-to-clipboard-with-manual-reset&quot;&gt;Example 1: Robust Copy to Clipboard with Manual Reset&lt;/h4&gt;
&lt;p&gt;The updated &lt;code&gt;useClipboard&lt;/code&gt; hook guarantees that component unmounts will not leave orphan timers behind, and provides an explicit &lt;code&gt;reset&lt;/code&gt; trigger for complex UI states.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React from &quot;react&quot;;
import { useClipboard } from &quot;react-hook-lab&quot;;

export function ApiKeyCard({ apiKey }: { apiKey: string }) {
  const { copied, error, copy, reset } = useClipboard(2500);

  return (
    &amp;lt;div className=&quot;api-key-container&quot;&amp;gt;
      &amp;lt;code&amp;gt;{apiKey}&amp;lt;/code&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; copy(apiKey)} disabled={copied}&amp;gt;
        {copied ? &quot;Copied!&quot; : &quot;Copy Key&quot;}
      &amp;lt;/button&amp;gt;
      {copied &amp;amp;&amp;amp; (
        &amp;lt;button onClick={reset} className=&quot;secondary-btn&quot;&amp;gt;
          Dismiss
        &amp;lt;/button&amp;gt;
      )}
      {error &amp;amp;&amp;amp; &amp;lt;span className=&quot;error-text&quot;&amp;gt;Failed to copy: {error.message}&amp;lt;/span&amp;gt;}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;example-2-debounced-text-input-with-preserved-spaces&quot;&gt;Example 2: Debounced Text Input with Preserved Spaces&lt;/h4&gt;
&lt;p&gt;When debouncing search inputs or markdown editors, trimming trailing spaces prematurely breaks the typing flow. The new &lt;code&gt;trim&lt;/code&gt; configuration in &lt;code&gt;useDebounce&lt;/code&gt; lets you keep input spacing intact:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React, { useState } from &quot;react&quot;;
import { useDebounce } from &quot;react-hook-lab&quot;;

export function RealtimeNotePreview() {
  const [content, setContent] = useState(&quot;&quot;);

  // Preserve active trailing whitespace while the user pauses
  const debouncedContent = useDebounce(content, 400, {
    trim: false,
  });

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;textarea
        value={content}
        onChange={(e) =&amp;gt; setContent(e.target.value)}
        placeholder=&quot;Type markdown notes here...&quot;
      /&amp;gt;
      &amp;lt;div className=&quot;preview&quot;&amp;gt;
        &amp;lt;h3&amp;gt;Parsed Output&amp;lt;/h3&amp;gt;
        &amp;lt;pre&amp;gt;{debouncedContent}&amp;lt;/pre&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-upgrading&quot;&gt;3. Upgrading&lt;/h3&gt;
&lt;p&gt;These updates are backward compatible for the vast majority of consumers while eliminating ghost state updates under heavy re-renders.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install react-hook-lab@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;resources-2&quot;&gt;Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Saurav-TB-Pandey/react-hook-lab&quot;&gt;Saurav-TB-Pandey/react-hook-lab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NPM Package&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Author LinkedIn&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/pandeysaurav/&quot;&gt;Saurav Pandey&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/production-proofing-react-hooks.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27670/taming-async-race-conditions-and-unmounted-state-in-react-whats-new-in-react-hook-lab</guid>
<pubDate>Fri, 18 Sep 2026 20:26:15 +0000</pubDate>
</item>
<item>
<title>Project LifeOps (Part 3): Core Interactive Modules: Fitness, Library, Cinema, and Professional Kanba</title>
<link>https://coderlegion.com/27227/project-lifeops-part-core-interactive-modules-fitness-library-cinema-professional-kanba</link>
<description>&lt;p&gt;In earlier installments, we engineered the foundational backbone of &lt;strong&gt;LifeOps&lt;/strong&gt;: in &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part1_arquitectura_backend/&quot;&gt;Part 1&lt;/a&gt; we designed the polymorphic PostgreSQL schema and modular FastAPI backend, and in &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/&quot;&gt;Part 2&lt;/a&gt; we brought the &lt;em&gt;Glassmorphism Dark Mode&lt;/em&gt; design system and global 360° Dashboard to life.&lt;/p&gt;
&lt;p&gt;Now comes the most defining phase of development: &lt;strong&gt;the day-to-day functional modules&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A personal operating system fails if it forces users into rigid input forms or scatters daily data across disconnected views. The challenge was articulating three completely disparate life areas under unified design and performance standards:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;🏃 &lt;strong&gt;Health &amp;amp; Physical Fitness&lt;/strong&gt;: Workout logging across sports with dynamic metrics (distance volume, calories, pace calculations, and Personal Bests).&lt;/li&gt;
&lt;li&gt;📚 &lt;strong&gt;Intellectual Culture &amp;amp; Media&lt;/strong&gt;: Book tracking with interactive reading progress bars and an entertainment catalog organized by streaming platform.&lt;/li&gt;
&lt;li&gt;💼 &lt;strong&gt;Professional Execution&lt;/strong&gt;: An agile 4-column Kanban board mapped to projects, equipped with a &lt;strong&gt;chronological progress log powered by PostgreSQL JSONB&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;[!TIP]&lt;br&gt;
&lt;strong&gt;Test the live app&lt;/strong&gt;: You can explore the production build of LifeOps directly at &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/&quot;&gt;https://datalaria.com/apps/lifeops/&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-lifeops-series-roadmap&quot;&gt;🗺️ LifeOps Series Roadmap&lt;/h4&gt;
&lt;p&gt;To understand how every architectural layer fits together, this series spans 5 structured installments:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 1&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part1_arquitectura_backend/&quot;&gt;Personal Operating System Architecture and FastAPI + Supabase Backend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 2&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/&quot;&gt;React Frontend with Glassmorphism, 360° Dashboard, and Design System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 3 (This article)&lt;/strong&gt;: Core Interactive Modules: Fitness, Library, Cinema, and Professional Kanban Board&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;Part 4&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part4_informes_word_excel/&quot;&gt;In-Memory Word Dossiers (.docx) &amp;amp; Multi-Sheet Excel Engine (.xlsx)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;Part 5&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part5_deploy_mobile_pwa/&quot;&gt;24/7 Zero-Cost Cloud Deployment ($0/month), Mobile UX, and PWA&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;1-the-ux-dilemma-dual-view-mode-inspiring-cards-vs-synthesized-table-&quot;&gt;1. The UX Dilemma: Dual View Mode (Inspiring Cards vs. Synthesized Table) 🎴📊&lt;/h4&gt;
&lt;p&gt;When building data-intensive dashboards, designers face a classic user experience tradeoff:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Grid / Cards View&lt;/strong&gt;: Visually engaging, spacious, and inspiring. It highlights book covers, movie posters, and workout chips at a glance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Data Table View&lt;/strong&gt;: Irreplaceable when auditing timestamps, comparing heart rates, or scanning through an entire month of logs without endless vertical scrolling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of forcing a single opinionated view, I built a &lt;strong&gt;persistent dual-mode toggle stored in &lt;code&gt;localStorage&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const [viewMode, setViewMode] = useState(() =&amp;gt; {
  return localStorage.getItem('lifeops_view_sport') || 'grid';
});

const handleViewChange = (mode) =&amp;gt; {
  setViewMode(mode);
  localStorage.setItem('lifeops_view_sport', mode);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the header of each module, two unobtrusive buttons with &lt;code&gt;LayoutGrid&lt;/code&gt; and &lt;code&gt;Table&lt;/code&gt; icons toggle the presentation mode instantly. Thanks to React and Vite's build architecture, view switches execute in sub-milliseconds without triggering redundant server roundtrips.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/lifeops_sport_table_en.png&quot; alt=&quot;Dual view mode in LifeOps: Synthesized data table with key metrics, personal bests, and instant sorting&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-fitness-amp-performance-module-sportmodule-jsx-&quot;&gt;2. Fitness &amp;amp; Performance Module (&lt;code&gt;SportModule.jsx&lt;/code&gt;) 🏃💨&lt;/h4&gt;
&lt;p&gt;The fitness module caters to multi-sport athletes practicing running, road cycling, gym strength sessions, and swimming.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/lifeops_sport_performance_en.png&quot; alt=&quot;Fitness and Performance Module featuring aggregate KPI summary widgets and detailed workout cards&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────────────┐
│  🏃 Sport &amp;amp; Fitness                    [ + Log Workout ]       [⊞|≡]   │
├─────────────────┬─────────────────┬─────────────────┬──────────────────┤
│ TOTAL DISTANCE  │ ACTIVE TIME     │ CALORIES        │ PERSONAL BESTS   │
│   142.5 km      │   18.4 hours    │   12,450 kcal   │    4 PBs 🏅      │
├─────────────────┴─────────────────┴─────────────────┴──────────────────┤
│                                                                        │
│ ┌───────────────────────────┐         ┌──────────────────────────────┐ │
│ │ 🏃 RUNNING         🏅 PB  │         │ 🚴 CYCLING                   │ │
│ │ Sunday Long Run           │         │ Mountain Pass Century Ride   │ │
│ │ 2026-09-10                │         │ 2026-09-08                   │ │
│ │ [📍 21.1 km] [⏱️ 1h 48m]  │         │ [📍 65.0 km] [⏱️ 2h 45m]     │ │
│ │ [🔥 1,420 kcal] [❤️ 152bpm]│        │ [🔥 1,890 kcal] [⛰️ +950m]    │ │
│ └───────────────────────────┘         └──────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;2-1-aggregated-high-level-kpis&quot;&gt;2.1. Aggregated High-Level KPIs&lt;/h5&gt;
&lt;p&gt;Before rendering individual activity items, we compute cumulative totals using in-memory array reducers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const totalKm = workouts.reduce((sum, act) =&amp;gt; sum + (act.workout?.distance_km || 0), 0);
const totalMinutes = workouts.reduce((sum, act) =&amp;gt; sum + (act.duration_minutes || 0), 0);
const totalCalories = workouts.reduce((sum, act) =&amp;gt; sum + (act.workout?.calories || 0), 0);
const pbCount = workouts.filter((act) =&amp;gt; act.workout?.personal_best).length;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;2-2-segregated-fastapi-payload&quot;&gt;2.2. Segregated FastAPI Payload&lt;/h5&gt;
&lt;p&gt;When an athlete records an exercise, the client dispatches a structured contract that FastAPI parses into the master table &lt;code&gt;lifeops.activities&lt;/code&gt; and the child table &lt;code&gt;lifeops.workouts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const payload = {
  activity: {
    activity_type: 'sport',
    title: formData.title,
    date: formData.date,
    duration_minutes: parseInt(formData.duration_minutes),
    description: formData.notes,
  },
  workout: {
    workout_type: formData.workout_type, // 'running' | 'cycling' | 'gym'
    distance_km: parseFloat(formData.distance_km) || null,
    calories: parseInt(formData.calories) || null,
    avg_heart_rate: parseInt(formData.avg_heart_rate) || null,
    elevation_m: parseInt(formData.elevation_m) || null,
    personal_best: formData.personal_best,
    notes: formData.notes,
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures shared attributes (dates, general durations, notes) remain globally indexable for the 360° Dashboard, while biometric attributes (elevation gain, heart rate, personal bests) reside in their specialized child table with cascading foreign key guarantees.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-culture-amp-media-tracking-books-amp-cinema-&quot;&gt;3. Culture &amp;amp; Media Tracking: Books &amp;amp; Cinema 📚🎬&lt;/h4&gt;
&lt;p&gt;For cultural tracking, the goal was leaving behind fragmented mobile notes in favor of an enriched visual library.&lt;/p&gt;
&lt;h5 id=&quot;3-1-interactive-reading-progress-booksmodule-jsx-&quot;&gt;3.1. Interactive Reading Progress (&lt;code&gt;BooksModule.jsx&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;On each book card, we calculate page completion percentages in real time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const percentage = b.pages_total &amp;gt; 0 
  ? Math.min(100, Math.round((b.pages_read / b.pages_total) * 100)) 
  : 0;

&amp;lt;div className=&quot;book-progress-wrapper&quot;&amp;gt;
  &amp;lt;div className=&quot;progress-info&quot;&amp;gt;
    &amp;lt;span&amp;gt;{b.pages_read} / {b.pages_total} pages&amp;lt;/span&amp;gt;
    &amp;lt;span className=&quot;percentage-tag&quot;&amp;gt;{percentage}%&amp;lt;/span&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div className=&quot;progress-bar-bg&quot;&amp;gt;
    &amp;lt;div 
      className=&quot;progress-bar-fill&quot; 
      style={{ 
        width: `${percentage}%`,
        background: percentage === 100 ? 'var(--accent-emerald)' : 'var(--grad-primary)'
      }} 
    /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A status filter organizes titles into &lt;code&gt;reading&lt;/code&gt;, &lt;code&gt;completed&lt;/code&gt;, and &lt;code&gt;wishlist&lt;/code&gt;, paired with an interactive 5-star rating system using &lt;code&gt;lucide-react&lt;/code&gt; icons.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/lifeops_books_progress_en.png&quot; alt=&quot;Reading Library module with dynamic progress tracking percentages and status badges&quot;&gt;&lt;/p&gt;
&lt;h5 id=&quot;3-2-streaming-media-catalog-filmsmodule-jsx-&quot;&gt;3.2. Streaming Media Catalog (&lt;code&gt;FilmsModule.jsx&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;The cinema module tracks movies, TV series, and documentaries cataloged by streaming provider (&lt;em&gt;Netflix, HBO Max, Prime Video, Disney+, Cinema&lt;/em&gt;). Each entry logs release year, director, rating, and personal review notes, answering: &lt;em&gt;&quot;What were the best films I watched this past quarter?&quot;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/lifeops_films_catalog_en.png&quot; alt=&quot;Cinema and TV series catalog organized by streaming provider with ratings&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;4-professional-execution-the-kanban-board-kanbanboard-jsx-&quot;&gt;4. Professional Execution: The Kanban Board (&lt;code&gt;KanbanBoard.jsx&lt;/code&gt;) 📋&lt;/h4&gt;
&lt;p&gt;For professional tasks, I implemented a full Kanban board featuring 4 columns reflecting agile task lifecycles:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;📝 &lt;strong&gt;To Do (&lt;code&gt;todo&lt;/code&gt;)&lt;/strong&gt;: Backlog tasks queued for upcoming execution.&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;In Progress (&lt;code&gt;in_progress&lt;/code&gt;)&lt;/strong&gt;: Active work during the current sprint.&lt;/li&gt;
&lt;li&gt;🔍 &lt;strong&gt;In Review (&lt;code&gt;review&lt;/code&gt;)&lt;/strong&gt;: Tasks pending review, testing, or third-party feedback.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Done (&lt;code&gt;done&lt;/code&gt;)&lt;/strong&gt;: Completed deliverables.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/lifeops_kanban_board_en.png&quot; alt=&quot;Professional 4-column Kanban board with project tags, urgent priority chips, and progress badges&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ 📝 TO DO (3)     │ ⚡ IN PROGRESS (2)│ 🔍 IN REVIEW (1) │ ✅ DONE (8)      │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ ┌──────────────┐ │ ┌──────────────┐ │ ┌──────────────┐ │ ┌──────────────┐ │
│ │ Refactor API │ │ │ Frontend UI  │ │ │ E2E Test Run │ │ │ DB Migration │ │
│ │ 🚨 Critical  │ │ │ 🟡 Medium    │ │ │ 🟢 Low       │ │ │ 2026-09-02   │ │
│ │ 📅 Sep 18    │ │ │ 💬 3 notes   │ │ │ 📅 Today     │ │ │ 💬 5 notes   │ │
│ │ [→ Move]     │ │ │ [←]      [→] │ │ │ [←]      [→] │ │ │ [← Move]     │ │
│ └──────────────┘ │ └──────────────┘ │ └──────────────┘ │ └──────────────┘ │
└──────────────────┴──────────────────┴──────────────────┴──────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;4-1-optimistic-ui-updates-for-tactile-fluidity&quot;&gt;4.1. Optimistic UI Updates for Tactile Fluidity&lt;/h5&gt;
&lt;p&gt;When moving a task from &lt;code&gt;in_progress&lt;/code&gt; to &lt;code&gt;review&lt;/code&gt;, waiting for the entire roundtrip (browser → Render API → Supabase → response) introduces an undesirable delay of several hundred milliseconds.&lt;/p&gt;
&lt;p&gt;To deliver an instantaneous tactile feel, we execute an &lt;strong&gt;optimistic state update&lt;/strong&gt; in React:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const handleMoveTask = async (taskId, currentStatus, direction) =&amp;gt; {
  const statusOrder = ['todo', 'in_progress', 'review', 'done'];
  const currentIndex = statusOrder.indexOf(currentStatus);
  const targetIndex = direction === 'next' ? currentIndex + 1 : currentIndex - 1;

  if (targetIndex &amp;lt; 0 || targetIndex &amp;gt;= statusOrder.length) return;
  const targetStatus = statusOrder[targetIndex];

  // 1. Instantaneous optimistic update
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27227/project-lifeops-part-core-interactive-modules-fitness-library-cinema-professional-kanba</guid>
<pubDate>Tue, 15 Sep 2026 06:12:43 +0000</pubDate>
</item>
<item>
<title>What I Learned Integrating AI Into My React App</title>
<link>https://coderlegion.com/27205/what-i-learned-integrating-ai-into-my-react-app</link>
<description>&lt;p&gt;While building Chef Claude, a small React application where users enter the ingredients they have and receive an AI-generated recipe, I started wondering how this would actually work.&lt;/p&gt;
&lt;p&gt;Should I store recipes somewhere in my code? Or use a recipe API?&lt;/p&gt;
&lt;p&gt;Since every user could enter a different combination of ingredients, I couldn't rely on a fixed list of recipes.&lt;/p&gt;
&lt;p&gt;That's when I thought of integrating AI into the app.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-was-building&quot;&gt;What I Was Building&lt;/h3&gt;
&lt;p&gt;The flow of Chef Claude was simple.&lt;/p&gt;
&lt;p&gt;Users add ingredients based on what they have available. Every ingredient is stored in state, and after adding at least four ingredients, the &quot;Get a Recipe&quot; button appears.&lt;/p&gt;
&lt;p&gt;When the user clicks the button, the ingredient list is sent to the AI model. The model uses those ingredients to generate a recipe and sends the response back.&lt;/p&gt;
&lt;p&gt;But the response comes back as Markdown.&lt;/p&gt;
&lt;p&gt;Displaying that raw response directly in the UI didn't look good, so I used &lt;code&gt;react-markdown&lt;/code&gt; to render the Markdown properly in my React app.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ajmx7s8z73vxnss0he7e.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;getting-a-hugging-face-access-token&quot;&gt;Getting a Hugging Face Access Token&lt;/h3&gt;
&lt;p&gt;Before connecting my app to an AI model, I needed a Hugging Face access token.&lt;/p&gt;
&lt;p&gt;Creating one only took a few minutes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a &lt;a rel=&quot;nofollow&quot; href=&quot;https://huggingface.co/join&quot;&gt;Hugging Face account&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Open &lt;a rel=&quot;nofollow&quot; href=&quot;https://huggingface.co/settings/tokens&quot;&gt;&lt;strong&gt;Access Tokens&lt;/strong&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Create a new fine-grained token.&lt;/li&gt;
&lt;li&gt;Enable inference permissions.&lt;/li&gt;
&lt;li&gt;Copy the token.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/9xyxl2629xduuwxuhc27.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setting-up-the-environment-variable&quot;&gt;Setting Up the Environment Variable&lt;/h3&gt;
&lt;p&gt;After getting my Hugging Face token, I needed to use it inside my React application.&lt;/p&gt;
&lt;p&gt;Instead of hardcoding my API key directly into my React code, I stored it in a &lt;code&gt;.env&lt;/code&gt; file to keep my configuration separate from the source code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;VITE_HF_ACCESS_TOKEN=YOUR_API_KEY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I accessed it inside my application using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const token = import.meta.env.VITE_HF_ACCESS_TOKEN;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first, I thought keeping my token in a &lt;code&gt;.env&lt;/code&gt; file meant it was completely safe.&lt;/p&gt;
&lt;p&gt;But after learning how Vite handles environment variables, I realized that wasn't true.&lt;/p&gt;
&lt;p&gt;Since my React application needs the token to make the API request, Vite exposes &lt;code&gt;VITE_&lt;/code&gt; environment variables to the frontend during the build process. That also means the browser has access to them.&lt;/p&gt;
&lt;p&gt;One thing that confused me was this: if my &lt;code&gt;.env&lt;/code&gt; file wasn't part of my application code, then how was the browser still getting my API key?&lt;/p&gt;
&lt;p&gt;After understanding how Vite builds the application, it finally clicked. The &lt;code&gt;.env&lt;/code&gt; file helps keep my configuration separate from the source code, but &lt;code&gt;VITE_&lt;/code&gt; variables are still injected into the frontend because the browser needs them to make the API request.&lt;/p&gt;
&lt;p&gt;That's when I realized my API key wasn't actually secret anymore.&lt;/p&gt;
&lt;p&gt;While researching this, I also understood why backend applications are important. Instead of letting the browser call the Hugging Face API directly, the better approach is to keep the API key on the backend. The frontend sends the user's ingredients to the backend, the backend talks to the AI using the secret API key, and only the generated recipe is returned to the frontend.&lt;/p&gt;
&lt;p&gt;Even though I haven't learned backend development yet, this was the moment I finally understood why developers keep saying that secret API keys should never live in the frontend.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;One thing I learned:&lt;/strong&gt; Keeping &lt;code&gt;.env&lt;/code&gt; out of your source code and keeping an API key secret are two different things. If the frontend needs the key, the browser will eventually have access to it. That's why sensitive API keys should be kept on a backend or serverless function.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;connecting-the-model&quot;&gt;Connecting the Model&lt;/h3&gt;
&lt;p&gt;After setting up the API key, the next step was connecting my React application to an AI model.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { HfInference } from &quot;@huggingface/inference&quot;;

const SYSTEM_PROMPT = `
You are an assistant that receives a list of ingredients that a user has and suggests a recipe they could make with some or all of those ingredients. You don't need to use every ingredient they mention in your recipe. The recipe can include additional ingredients they didn't mention, but try not to include too many extra ingredients. Format your response in markdown to make it easier to render to a web page
`;

const hf = new HfInference(import.meta.env.VITE_HF_ACCESS_TOKEN);

export async function getRecipeFromMistral(ingredientsArr) {
    const ingredientsString = ingredientsArr.join(&quot;, &quot;);

    const response = await hf.chatCompletion({
    // I'm using Qwen 2.5 7B Instruct, but you can replace it with any compatible Hugging Face model.
        model: &quot;Qwen/Qwen2.5-7B-Instruct&quot;,
        messages: [
            { role: &quot;system&quot;, content: SYSTEM_PROMPT },
            {
                role: &quot;user&quot;,
                content: `I have ${ingredientsString}. Please give me a recipe I'd recommend!`,
            },
        ],
    });

    return response.choices[0].message.content;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's break down what each part is doing.&lt;/p&gt;
&lt;h4 id=&quot;hfinference&quot;&gt;&lt;code&gt;HfInference&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Instead of manually sending HTTP requests to the Hugging Face API, the SDK provides a client that makes interacting with AI models much simpler.&lt;/p&gt;
&lt;h4 id=&quot;system-prompt&quot;&gt;&lt;code&gt;SYSTEM_PROMPT&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;The system prompt tells the AI how it should behave.&lt;/p&gt;
&lt;p&gt;In my project, I wanted the AI to act like a recipe assistant. It should take the user's ingredients, suggest a recipe, and return the response in Markdown so I could render it nicely in my UI.&lt;/p&gt;
&lt;p&gt;Without a system prompt, the AI still responds, but it doesn't have clear instructions about the format or the type of answer I expect.&lt;/p&gt;
&lt;h4 id=&quot;ingredientsarr-join-&quot;&gt;&lt;code&gt;ingredientsArr.join(&quot;, &quot;)&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;The ingredients entered by the user are stored as an array.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;[&quot;Rice&quot;, &quot;Egg&quot;, &quot;Onion&quot;, &quot;Tomato&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before sending them to the AI, I converted them into a single string using &lt;code&gt;.join(&quot;, &quot;)&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Rice, Egg, Onion, Tomato
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a natural sentence that becomes part of the user's prompt.&lt;/p&gt;
&lt;h4 id=&quot;chatcompletion-&quot;&gt;&lt;code&gt;chatCompletion()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;This is the function provided by the Hugging Face SDK that sends my prompt to the selected AI model and returns its response.&lt;/p&gt;
&lt;p&gt;In simple words, this is where my React application actually talks to the AI.&lt;/p&gt;
&lt;h4 id=&quot;returning-the-recipe&quot;&gt;Returning the recipe&lt;/h4&gt;
&lt;p&gt;Finally, I return:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;response.choices[0].message.content
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives me the generated recipe, which I later render in my React application using &lt;code&gt;react-markdown&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/5cafuwoee1i1tqb48uj6.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-response-was-markdown&quot;&gt;The Response Was Markdown&lt;/h3&gt;
&lt;p&gt;After connecting the AI model, I thought the hard part was over.&lt;/p&gt;
&lt;p&gt;The user clicked the &lt;strong&gt;&quot;Get a Recipe&quot;&lt;/strong&gt; button, the AI generated a recipe, and everything worked.&lt;/p&gt;
&lt;p&gt;But there was one small problem.&lt;/p&gt;
&lt;p&gt;The response didn't look good in the UI.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ic8sm8itwma28yzmwkos.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;The AI was already returning a well-structured Markdown response, and when I rendered it directly, it looked like plain text instead of a nicely formatted recipe.&lt;/p&gt;
&lt;p&gt;That's when I discovered &lt;code&gt;react-markdown&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Instead of manually formatting the response, I used &lt;code&gt;react-markdown&lt;/code&gt; to render the Markdown as React elements.&lt;/p&gt;
&lt;p&gt;The difference was immediately noticeable. Headings, bullet points, and formatting started looking like a proper recipe instead of a block of text.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/thpoqimmrrzxy3507ug0.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;That small library made the final output look much better without adding much extra code.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;When I started this project, I thought integrating AI simply meant sending a prompt and getting a response.&lt;/p&gt;
&lt;p&gt;By the end, I realized there was much more involved—structuring prompts, handling responses, understanding how environment variables work, and presenting the AI's output in a way that feels polished to the user.&lt;/p&gt;
&lt;p&gt;This project also reminded me that every library solves a specific problem. Instead of building everything from scratch, knowing when to use the right tool is just as important.&lt;/p&gt;
&lt;p&gt;If you've built something similar or have suggestions, I'd love to hear them in the comments.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27205/what-i-learned-integrating-ai-into-my-react-app</guid>
<pubDate>Tue, 15 Sep 2026 00:46:58 +0000</pubDate>
</item>
</channel>
</rss>