<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Janarthanan Soundararajan (Jana)</title>
    <description>The latest articles on DEV Community by Janarthanan Soundararajan (Jana) (@janarthanan_soundararajan).</description>
    <link>https://dev.to/janarthanan_soundararajan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4064706%2Fa88f1e6d-2e06-462a-ae72-7d011147ba2e.png</url>
      <title>DEV Community: Janarthanan Soundararajan (Jana)</title>
      <link>https://dev.to/janarthanan_soundararajan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/janarthanan_soundararajan"/>
    <language>en</language>
    <item>
      <title>What Became Hard to Change as Our React App Grew</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Fri, 09 Oct 2026 12:59:16 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/what-became-hard-to-change-as-our-react-app-grew-3m8b</link>
      <guid>https://dev.to/janarthanan_soundararajan/what-became-hard-to-change-as-our-react-app-grew-3m8b</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjxmb9l1p781rdfnb835a.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjxmb9l1p781rdfnb835a.jpg" alt="cluttered workspace, busy desk" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
A small React app feels friendly. A few screens, a few components, and you know where everything lives.&lt;/p&gt;

&lt;p&gt;Then more features arrive.&lt;/p&gt;

&lt;p&gt;You change one field and discover it has a social life across the entire codebase. A shared form needs another special case. A button disappears because a condition changed somewhere else.&lt;/p&gt;

&lt;p&gt;With vibe coding, it's easy to keep adding features once the screen works. Without checking how the new code fits into the app, repeated rules and extra copies of data can go unnoticed. As the app grows, those small problems become harder to find and fix.&lt;/p&gt;

&lt;p&gt;The app still works. Making even a small change can turn into a treasure hunt.&lt;/p&gt;

&lt;p&gt;Here are a few places where things get harder to change, and what can help keep them manageable.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧩 The shared component that tried to do everything
&lt;/h2&gt;

&lt;p&gt;A shared form starts with a reasonable goal: reuse the same fields across two screens.&lt;/p&gt;

&lt;p&gt;Then one screen needs different validation. Another hides a section. Another submits to a different endpoint.&lt;/p&gt;

&lt;p&gt;Soon, the component has so many true-or-false props that using it feels like filling out a questionnaire.&lt;/p&gt;

&lt;p&gt;Sharing UI pieces that work the same way helps. Sharing a whole form gets harder when each screen needs different behavior.&lt;/p&gt;

&lt;p&gt;Share common inputs and layouts. Let each feature decide its own rules. Build each screen from smaller components, so one shared form doesn't need to handle every case.&lt;/p&gt;

&lt;p&gt;A little duplication can be easier to maintain than a growing collection of special cases.&lt;/p&gt;
&lt;h2&gt;
  
  
  📦 The same data, now available in several flavours
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj416wubxcbetxfx737bu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj416wubxcbetxfx737bu.png" alt="multiple monitors, confused office worker" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, a customer list loads from the API. The selected customer's details are copied into a global store and reused on the details or edit page.&lt;/p&gt;

&lt;p&gt;The idea is to save another fetch because the data is already available.&lt;/p&gt;

&lt;p&gt;After an update, the query cache has the latest details, but the separate copy still has the old ones. Same customer, two different stories.&lt;/p&gt;

&lt;p&gt;Store the selected customer's ID and read their details through the query layer. It can reuse cached data and refresh it when needed, based on its settings. Using an ID doesn't automatically mean another request.&lt;/p&gt;

&lt;p&gt;Keep temporary UI state near the component. A form can also keep its own draft while the user edits. Just decide what happens when they save, cancel, or receive newer data.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔄 The field rename that became a group project
&lt;/h2&gt;

&lt;p&gt;Reading API fields directly inside components feels convenient.&lt;/p&gt;

&lt;p&gt;Until a backend field changes and several screens need edits.&lt;/p&gt;

&lt;p&gt;If several screens turn an API response into the same display data, move that work into one function near the API call. Components can use the result without repeating those steps.&lt;/p&gt;

&lt;p&gt;Do this when it reduces repeated work. Adding a layer that only renames fields can give you more code to maintain.&lt;/p&gt;

&lt;p&gt;TypeScript won't check whether incoming JSON matches your types. You need to validate the response while the app is running if you want that check.&lt;/p&gt;
&lt;h2&gt;
  
  
  📋 The same rule, three different versions
&lt;/h2&gt;

&lt;p&gt;An invoice can be edited only while its status is &lt;code&gt;DRAFT&lt;/code&gt;. That check appears in the table's Edit button, the details page, and the edit form.&lt;/p&gt;

&lt;p&gt;Later, the rule changes: locked invoices cannot be edited, even when they're drafts.&lt;/p&gt;

&lt;p&gt;The table and form get updated. The details page gets missed. It still offers an Edit button for locked invoices. Apparently, it didn't get the memo.&lt;/p&gt;

&lt;p&gt;Keep the shared check in one function inside the invoice feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canEditInvoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DRAFT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLocked&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each screen uses &lt;code&gt;canEditInvoice(invoice)&lt;/code&gt;, so the rule has one place to change and test.&lt;/p&gt;

&lt;p&gt;The backend must check the rule too. Hiding the Edit button doesn't stop someone sending an API request.&lt;/p&gt;

&lt;h2&gt;
  
  
  🕸️ The import that brought its whole family
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnfflt5uvpu73zhacsek7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnfflt5uvpu73zhacsek7.jpg" alt="tangled cables, cable mess" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A feature imports a hook from another feature. That hook imports a store from somewhere else.&lt;/p&gt;

&lt;p&gt;Now a small change comes with a guided tour of the application.&lt;/p&gt;

&lt;p&gt;Keep related code together. Decide which functions and components other features can import. Move code used by several features into shared modules when it makes sense.&lt;/p&gt;

&lt;p&gt;Folders help people find code. Code reviews and rules about allowed imports help keep features from depending on each other's internal code.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧪 The tests that objected to moving furniture
&lt;/h2&gt;

&lt;p&gt;You refactor a component. The screen behaves exactly as before.&lt;/p&gt;

&lt;p&gt;The tests disagree.&lt;/p&gt;

&lt;p&gt;Tests that check internal state or replace most hooks with fake versions can break when you change how a component works.&lt;/p&gt;

&lt;p&gt;Test what the user can do: entering data, submitting a form, and seeing the expected result. Test business rules directly, and use end-to-end tests to check important tasks across the app.&lt;/p&gt;

&lt;p&gt;A refactor should feel safer because the tests exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔧 One smaller fix before the grand rewrite
&lt;/h2&gt;

&lt;p&gt;When changes become difficult, a rewrite can sound tempting. So can a new state library.&lt;/p&gt;

&lt;p&gt;Start with one awkward change instead. Look at the code it depends on. Find the repeated rule or the value that several places try to manage.&lt;/p&gt;

&lt;p&gt;Fix that part, then see whether the next similar change takes less effort.&lt;/p&gt;

&lt;p&gt;What became unexpectedly difficult to change in your React app?&lt;/p&gt;

</description>
      <category>react</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>5 Places Custom React Hooks Make Your Code Better</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Thu, 01 Oct 2026 08:39:48 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/5-places-custom-react-hooks-make-your-code-better-16k6</link>
      <guid>https://dev.to/janarthanan_soundararajan/5-places-custom-react-hooks-make-your-code-better-16k6</guid>
      <description>&lt;p&gt;You add a search box to a page. A timer delays the search until the user stops typing.&lt;/p&gt;

&lt;p&gt;Then another page needs the same behavior. You copy the timer, the effect, and the cleanup.&lt;/p&gt;

&lt;p&gt;A few weeks later, someone changes the delay in one place. Another search box behaves differently. A third forgets to clear its timer.&lt;/p&gt;

&lt;p&gt;The problem isn't that the code is long. It's that the same behavior now has several owners.&lt;/p&gt;

&lt;p&gt;That's where a custom hook helps.&lt;/p&gt;

&lt;p&gt;In this article, we'll build five hooks for situations that show up in real applications:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;Coordinates&lt;/th&gt;
&lt;th&gt;Primary Real-World Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;⏱️ &lt;code&gt;useDebouncedValue&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Input value &amp;amp; timer&lt;/td&gt;
&lt;td&gt;Delaying network searches &amp;amp; heavy validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;🌐 &lt;code&gt;useOnlineStatus&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser connectivity store&lt;/td&gt;
&lt;td&gt;Showing "Offline" alerts &amp;amp; pausing remote sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;📱 &lt;code&gt;useMediaQuery&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Media query matches&lt;/td&gt;
&lt;td&gt;Adapting layout logic &amp;amp; honoring &lt;code&gt;prefers-reduced-motion&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;💾 &lt;code&gt;useLocalStorageState&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React state &amp;amp; storage&lt;/td&gt;
&lt;td&gt;Persisting drafts, filters, and UI preferences safely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;👁️ &lt;code&gt;useInView&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DOM element &amp;amp; &lt;code&gt;IntersectionObserver&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Scroll-triggered animations, lazy widgets, &amp;amp; impression tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We'll also cover when a hook is the wrong abstraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧩 First: what belongs in a custom hook?
&lt;/h2&gt;

&lt;p&gt;A custom hook is a function that uses React hooks to package reusable behavior. Its name starts with &lt;code&gt;use&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useSomething&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// React hooks can be used here.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But adding &lt;code&gt;use&lt;/code&gt; to a function name doesn't make it a useful hook.&lt;/p&gt;

&lt;p&gt;If you're calculating a total, use a regular function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're managing a timer, subscribing to a browser event, or coordinating state and cleanup, a hook may be appropriate.&lt;/p&gt;

&lt;p&gt;A useful hook usually has a clear contract:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Given these inputs, expose this state or behavior, and manage its lifecycle.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delayedSearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDebouncedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component shouldn't need to know how the timer is created or cleaned up.&lt;/p&gt;

&lt;p&gt;One important distinction: &lt;strong&gt;custom hooks share logic, not automatically state.&lt;/strong&gt; Two calls to a hook containing &lt;code&gt;useState&lt;/code&gt; normally have independent state. Hooks subscribing to the same external source can observe the same underlying data. &lt;a href="https://react.dev/learn/reusing-logic-with-custom-hooks" rel="noopener noreferrer"&gt;React's custom hook guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Call hooks at the top level of a component or another hook, not inside conditions, loops, or event handlers.&lt;/p&gt;

&lt;p&gt;The examples below use JavaScript. They are deliberately small, with their limitations explained rather than hidden behind a "production-ready" label.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⏱️ 1. &lt;code&gt;useDebouncedValue&lt;/code&gt;: wait until input settles
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The problem
&lt;/h3&gt;

&lt;p&gt;A user types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;r → re → rea → reac → react
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If every change triggers expensive work, you may perform five operations when the user only wanted the final result. Debouncing waits for a quiet period.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useDebouncedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;debouncedValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDebouncedValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setDebouncedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;debouncedValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;The hook keeps two values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;value&lt;/code&gt;: the latest input.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;debouncedValue&lt;/code&gt;: the last input that stayed unchanged long enough.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the input changes, the effect starts a timer.&lt;/p&gt;

&lt;p&gt;If the input changes again before the delay ends, cleanup cancels the previous timer. A new timer starts for the new value.&lt;/p&gt;

&lt;p&gt;The same cleanup also cancels a pending timer when the component unmounts. Both &lt;code&gt;value&lt;/code&gt; and &lt;code&gt;delay&lt;/code&gt; are dependencies because changing either should restart the waiting period.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;

&lt;p&gt;Keep the input responsive. Debounce the work that follows it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useDebouncedValue&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./useDebouncedValue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductSearch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSearch&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debouncedSearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nf"&gt;useDebouncedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;debouncedSearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;debouncedSearch&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Search products
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="nf"&gt;setSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; products found&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a small local list, immediate filtering may feel better. Debouncing becomes useful when the work is expensive or involves a request.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this hook does not solve
&lt;/h3&gt;

&lt;p&gt;Debouncing doesn't cancel a request that has already started.&lt;/p&gt;

&lt;p&gt;If a request for &lt;code&gt;"rea"&lt;/code&gt; is still running when a request for &lt;code&gt;"react"&lt;/code&gt; begins, the older response could arrive last. For network searches, also handle cancellation or stale responses—often with an &lt;code&gt;AbortController&lt;/code&gt; or a data-fetching library.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;br&gt;
&lt;strong&gt;&lt;code&gt;useDebouncedValue&lt;/code&gt; vs. React's &lt;code&gt;useDeferredValue&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useDeferredValue&lt;/code&gt;&lt;/strong&gt; (built into React 18+) addresses &lt;strong&gt;CPU-bound rendering responsiveness&lt;/strong&gt;. It tells React to keep the input responsive while deferring an expensive list re-render in the background without any fixed delay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useDebouncedValue&lt;/code&gt;&lt;/strong&gt; addresses &lt;strong&gt;I/O, network requests, and timers&lt;/strong&gt;. It enforces a fixed quiet period (e.g. 350ms) before triggering remote API requests, database queries, or server-side validations.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Good uses:&lt;/strong&gt; search requests, delayed previews, validation after typing pauses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt; debouncing the input's own displayed value. That makes typing feel broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌐 2. &lt;code&gt;useOnlineStatus&lt;/code&gt;: subscribe to connection changes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The problem
&lt;/h3&gt;

&lt;p&gt;Several components may need to show a connection warning. You could add &lt;code&gt;online&lt;/code&gt; and &lt;code&gt;offline&lt;/code&gt; listeners inside each component. A hook gives those components one consistent interface.&lt;/p&gt;

&lt;p&gt;Here, the browser is the source of truth. &lt;code&gt;useSyncExternalStore&lt;/code&gt; is designed for subscribing to state outside React. &lt;a href="https://react.dev/reference/react/useSyncExternalStore" rel="noopener noreferrer"&gt;React's external-store reference&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSyncExternalStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onStoreChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;online&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onStoreChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;offline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onStoreChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;online&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;onStoreChange&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;offline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;onStoreChange&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSnapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLine&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getServerSnapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useOnlineStatus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useSyncExternalStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getSnapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getServerSnapshot&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;There are three pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;subscribe&lt;/code&gt; tells React when the browser's status changes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getSnapshot&lt;/code&gt; reads the current value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getServerSnapshot&lt;/code&gt; supplies a value during server rendering and initial hydration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server cannot inspect the user's browser connection. Returning &lt;code&gt;true&lt;/code&gt; is our chosen initial assumption, not a measurement.&lt;/p&gt;

&lt;p&gt;The subscription functions live outside the hook because they don't depend on component props. Their identities remain stable between renders.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useOnlineStatus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./useOnlineStatus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ConnectionNotice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isOnline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useOnlineStatus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isOnline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      You appear to be offline. Recent changes may
      not have synced yet.
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The important limitation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;navigator.onLine&lt;/code&gt; is a hint, not proof that your API is reachable. A device may have a network connection while your server is unavailable. VPNs and other network conditions can also affect the result. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine" rel="noopener noreferrer"&gt;MDN's explanation of &lt;code&gt;navigator.onLine&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use this hook for helpful UI messaging. Don't use it as the only reason to disable a submit button. Try the operation and handle its actual result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good uses:&lt;/strong&gt; connection banners, offline indicators, explaining delayed synchronization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt; treating it as a backend health check.&lt;/p&gt;

&lt;h2&gt;
  
  
  📱 3. &lt;code&gt;useMediaQuery&lt;/code&gt;: use browser preferences in component behavior
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The problem
&lt;/h3&gt;

&lt;p&gt;CSS handles responsive presentation well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.cards&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;900px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.cards&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't need a hook for that.&lt;/p&gt;

&lt;p&gt;But sometimes a browser preference changes JavaScript behavior. For example, you may want to stop a canvas animation when the user prefers reduced motion.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;matchMedia&lt;/code&gt; exposes a media query's current result and change events. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia" rel="noopener noreferrer"&gt;MDN's &lt;code&gt;matchMedia&lt;/code&gt; reference&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useSyncExternalStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useMediaQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;serverFallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mediaQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onStoreChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;mediaQuery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;mediaQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;onStoreChange&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;mediaQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;onStoreChange&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mediaQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getSnapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mediaQuery&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;serverFallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mediaQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;serverFallback&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getServerSnapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;serverFallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;serverFallback&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useSyncExternalStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getSnapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getServerSnapshot&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;The query creates a &lt;code&gt;MediaQueryList&lt;/code&gt;. The hook subscribes to its &lt;code&gt;change&lt;/code&gt; event and exposes its boolean &lt;code&gt;matches&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;If the query changes, the hook creates a new subscription and removes the old one.&lt;/p&gt;

&lt;p&gt;The server fallback matters because a server doesn't know the browser's viewport or preferences. Use the same fallback on the server and client for initial hydration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useMediaQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./useMediaQuery&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Hero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefersReducedMotion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMediaQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(prefers-reduced-motion: reduce)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Build something worth showing.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;prefersReducedMotion&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StaticIllustration&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AnimatedIllustration&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;StaticIllustration&lt;/code&gt; and &lt;code&gt;AnimatedIllustration&lt;/code&gt; represent your application's illustration components.&lt;/p&gt;

&lt;p&gt;Here, &lt;code&gt;true&lt;/code&gt; is a conservative initial fallback: start with the static illustration until the browser's preference is available.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to avoid
&lt;/h3&gt;

&lt;p&gt;Don't replace every CSS media query with React state. Use CSS for spacing, columns, font sizes, and element positioning.&lt;/p&gt;

&lt;p&gt;Use this hook when JavaScript behavior genuinely depends on the query.&lt;/p&gt;

&lt;p&gt;Also remember that conditionally replacing components can discard their local state. Switching between a mobile and desktop form should not accidentally erase the user's input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good uses:&lt;/strong&gt; reduced-motion behavior, enabling an interactive visualization, adapting controls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt; measuring viewport width in React just to change a margin.&lt;/p&gt;

&lt;h2&gt;
  
  
  💾 4. &lt;code&gt;useLocalStorageState&lt;/code&gt;: remember a preference or draft
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The problem
&lt;/h3&gt;

&lt;p&gt;A user selects a preference or writes a draft. Refreshing the page loses it. A storage hook can coordinate React state with browser persistence.&lt;/p&gt;

&lt;p&gt;But this hook needs more care than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading can fail. Writing can fail. Stored JSON can be invalid. And an initial render must not overwrite an existing saved value before it has been read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;This version keeps component-local state and persists changes after loading.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useLocalStorageState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;initialValue&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;initialValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setRecord&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dirty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;readError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caught&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;readError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;caught&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;readError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;setRecord&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;dirty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dirty&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;serialized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serialized&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The value cannot be stored as JSON.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;serialized&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caught&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caught&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setRecord&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
          &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;nextValue&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;nextValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;dirty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;isReady&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why the extra state?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ready&lt;/code&gt; means the initial read has finished. &lt;code&gt;dirty&lt;/code&gt; means the user has changed the value.&lt;/p&gt;

&lt;p&gt;Keeping those separate prevents the default value from immediately replacing a saved value.&lt;/p&gt;

&lt;p&gt;It also means that invalid stored JSON is not silently overwritten during loading. The fallback is shown, an error is exposed, and a subsequent user edit can replace the invalid value.&lt;/p&gt;

&lt;p&gt;The record includes its storage key so a key change cannot accidentally write the previous key's value into the new key.&lt;/p&gt;

&lt;p&gt;Like &lt;code&gt;useState&lt;/code&gt;, this hook treats &lt;code&gt;initialValue&lt;/code&gt; as an initial fallback—not a prop that continuously resets the value.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;useLocalStorageState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./useLocalStorageState&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ArticleDraft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;setDraft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;isReady&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLocalStorageState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;article-draft:v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;""&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Your draft
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;textarea&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isReady&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="nf"&gt;setDraft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isReady&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading saved draft…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          Browser storage is unavailable or the
          saved draft could not be read. You can
          keep editing, but changes may not persist.
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a write fails, React state still updates. The user can keep typing, but the interface warns that persistence may have failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Know the boundaries
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; stores strings, is synchronous, and may be unavailable because of browser policies. It is not a database or a guaranteed backup. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage" rel="noopener noreferrer"&gt;MDN's &lt;code&gt;localStorage&lt;/code&gt; reference&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This implementation also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accepts JSON-compatible values, not arbitrary JavaScript objects.&lt;/li&gt;
&lt;li&gt;Does not validate the shape of parsed data.&lt;/li&gt;
&lt;li&gt;Does not synchronize separate hook instances or browser tabs.&lt;/li&gt;
&lt;li&gt;Does not guarantee the latest edit survives an immediate unmount.&lt;/li&gt;
&lt;li&gt;Does not make sensitive data safe to store.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For object data, validate the parsed structure and plan migrations. A versioned key such as &lt;code&gt;"preferences:v2"&lt;/code&gt; can help, but changing a key is not itself a migration strategy.&lt;/p&gt;

&lt;p&gt;If you need cross-tab updates, subscribe to storage changes. Remember that the browser's &lt;code&gt;storage&lt;/code&gt; event fires in other documents—not the window that performed the write. Same-page synchronization needs an additional mechanism. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event" rel="noopener noreferrer"&gt;MDN's storage event reference&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;br&gt;
&lt;strong&gt;Cross-Tab Synchronization Tip:&lt;/strong&gt;&lt;br&gt;
To sync state between open browser tabs in real-time, add an effect that listens to &lt;code&gt;window.addEventListener("storage", (e) =&amp;gt; { if (e.key === key) { ... } })&lt;/code&gt;. When a user toggles dark mode or updates a preference in Tab A, Tab B updates automatically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Good uses:&lt;/strong&gt; lightweight preferences, dismissed notices, non-sensitive drafts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt; passwords, tokens, large frequently updated datasets, or business-critical persistence.&lt;/p&gt;

&lt;h2&gt;
  
  
  👁️ 5. &lt;code&gt;useInView&lt;/code&gt;: observe visibility without scroll calculations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The problem
&lt;/h3&gt;

&lt;p&gt;You want to start an animation or mount an expensive preview when a section becomes visible.&lt;/p&gt;

&lt;p&gt;One approach is to listen to every scroll event and calculate element positions. Another is to let the browser observe the element.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IntersectionObserver&lt;/code&gt; reports changes in an element's intersection with a viewport or specified root. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API" rel="noopener noreferrer"&gt;MDN's Intersection Observer guide&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;This hook accepts configuration options including an optional &lt;code&gt;triggerOnce&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useInView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;rootMargin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;triggerOnce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setElement&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isInView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsInView&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setIsInView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;IntersectionObserver&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setIsInView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intersectionRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nf"&gt;setIsInView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inView&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inView&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;triggerOnce&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;triggerOnce&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;setElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;isInView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;The returned &lt;strong&gt;callback ref&lt;/strong&gt; (&lt;code&gt;ref: setElement&lt;/code&gt;) tells the hook which element to observe. Using a callback ref is important: unlike a standard &lt;code&gt;useRef()&lt;/code&gt;, whose &lt;code&gt;.current&lt;/code&gt; assignment does not notify React or trigger effects, a callback ref guarantees that React runs your setup effect as soon as the DOM node attaches or detaches.&lt;/p&gt;

&lt;p&gt;When the target element changes, the old observer disconnects and a new one starts.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;active&lt;/code&gt; flag prevents a late callback from an obsolete observer from updating the current state. If &lt;code&gt;triggerOnce&lt;/code&gt; is set to &lt;code&gt;true&lt;/code&gt;, the observer disconnects after its first intersection, keeping the element active (ideal for one-time enter animations).&lt;/p&gt;

&lt;p&gt;For this hook, "in view" means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The element is intersecting, and its visible ratio meets the configured threshold.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That explicit ratio check matters. &lt;code&gt;isIntersecting&lt;/code&gt; alone does not mean the element has reached a threshold such as 25%.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useInView&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./useInView&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DemoSection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isInView&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useInView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;
      &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Try the interactive demo&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isInView&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExpensivePreview&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;The preview loads as you scroll here.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ExpensivePreview&lt;/code&gt; represents your application's interactive preview component.&lt;/p&gt;

&lt;p&gt;The stable wrapper height matters. If mounting and unmounting the preview changes the section's geometry dramatically, visibility can oscillate.&lt;/p&gt;

&lt;p&gt;This example unmounts the preview when it leaves view. If the preview has state that should survive, keep it mounted after the first visit or pause its work instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to avoid
&lt;/h3&gt;

&lt;p&gt;Don't hide essential content permanently if observation is unavailable. This implementation falls back to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For infinite scrolling, visibility is only one signal. You also need guards such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isInView&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;hasNextPage&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isFetching&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Request the next page.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, repeated visibility updates can trigger duplicate requests.&lt;/p&gt;

&lt;p&gt;For ordinary images, native lazy loading may be enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/preview.webp"&lt;/span&gt;
  &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Application preview"&lt;/span&gt;
  &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Good uses:&lt;/strong&gt; optional previews, scroll-triggered animation, pausing offscreen work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt; replacing simpler browser features without a reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing a useful hook is about its contract
&lt;/h2&gt;

&lt;p&gt;These five hooks wrap different things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;What it coordinates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useDebouncedValue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A value and a timer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useOnlineStatus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Browser connection events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useMediaQuery&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A browser query subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useLocalStorageState&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local state and persistence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useInView&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A DOM element and an observer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before extracting a hook, answer these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What does it own?&lt;/li&gt;
&lt;li&gt;What inputs can change?&lt;/li&gt;
&lt;li&gt;What does it return?&lt;/li&gt;
&lt;li&gt;What must be cleaned up?&lt;/li&gt;
&lt;li&gt;What happens if the browser feature fails?&lt;/li&gt;
&lt;li&gt;Are separate callers independent or synchronized?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Naming helps reveal the contract. &lt;code&gt;useInView&lt;/code&gt; tells the caller what it provides. &lt;code&gt;useMountEffect&lt;/code&gt; mostly tells the caller when something runs, while hiding what it does.&lt;/p&gt;

&lt;p&gt;Prefer hooks organized around a behavior rather than wrappers that obscure effect dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hook is not an excuse to add another effect
&lt;/h2&gt;

&lt;p&gt;Some values should simply be calculated during rendering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They don't need another state variable, effect, or custom hook.&lt;/p&gt;

&lt;p&gt;Likewise, behavior caused directly by a user action often belongs in the event handler.&lt;/p&gt;

&lt;p&gt;Effects are most useful when coordinating React with something outside it: a timer, event subscription, observer, or browser storage. &lt;a href="https://react.dev/learn/you-might-not-need-an-effect" rel="noopener noreferrer"&gt;React's guide to avoiding unnecessary effects&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In frameworks with React Server Components, use these hooks within the appropriate client-component boundary. Browser APIs should not be assumed to exist on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  TypeScript definitions
&lt;/h2&gt;

&lt;p&gt;For TypeScript projects, here are the ready-to-use type signatures for all five hooks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;View TypeScript signatures&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. useDebouncedValue&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useDebouncedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 2. useOnlineStatus&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useOnlineStatus&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 3. useMediaQuery&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useMediaQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;serverFallback&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 4. useLocalStorageState&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;LocalStorageStateReturn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isReady&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useLocalStorageState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;initialValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;LocalStorageStateReturn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 5. useInView&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;UseInViewOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;root&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Element&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;Document&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;triggerOnce&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;UseInViewReturn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Element&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isInView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useInView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;UseInViewOptions&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;UseInViewReturn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Before you reuse a hook, test its awkward cases
&lt;/h2&gt;

&lt;p&gt;The happy path is usually easy. Cleanup and failure cases deserve attention.&lt;/p&gt;

&lt;p&gt;For these examples, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debounce:&lt;/strong&gt; rapid changes cancel older timers; unmounting cancels pending work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Online status:&lt;/strong&gt; listeners are removed; request failures are handled independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media query:&lt;/strong&gt; changing the query removes the previous subscription.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; existing values are preserved; invalid JSON and failed writes are surfaced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In view:&lt;/strong&gt; changing the target disconnects the old observer; unavailable APIs have a safe fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need a hook for every repeated expression.&lt;/p&gt;

&lt;p&gt;You need one when several components should rely on the same behavior—and you want that behavior's lifecycle, assumptions, and limitations maintained in one place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What behavior have you copied between React components that would make a good custom hook?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why I Built a Code-Native Visual Studio for My Developer Posts</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Fri, 25 Sep 2026 06:05:33 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/why-i-built-a-code-native-visual-studio-for-my-developer-posts-9f0</link>
      <guid>https://dev.to/janarthanan_soundararajan/why-i-built-a-code-native-visual-studio-for-my-developer-posts-9f0</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FTechAaroorian%2Fyuwbrndr%2Fmain%2Fpublic%2Fdemo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FTechAaroorian%2Fyuwbrndr%2Fmain%2Fpublic%2Fdemo.gif" alt="Yuwbrndr Live Studio Demo" width="599" height="301"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Over the past year, AI image tools have become amazing. Being able to type a prompt and get a detailed picture in seconds feels like magic.&lt;/p&gt;

&lt;p&gt;Like a lot of developers who post on LinkedIn, X, and Dev.to, I started using AI to create header images and diagrams for my technical write-ups. &lt;/p&gt;

&lt;p&gt;But the more I posted, the more I ran into a few small, frustrating problems that made me rethink my workflow:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. That "Typical AI Look"
&lt;/h3&gt;

&lt;p&gt;You probably know what I mean: glowing neon spheres, futuristic 3D glass cubes, and floating holograms. They look cool at first, but they didn’t feel like &lt;em&gt;me&lt;/em&gt;. When I write about system design or web dev, I want visuals that feel real and grounded — like an engineer sketching architecture on an office whiteboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Keeping a Consistent Style
&lt;/h3&gt;

&lt;p&gt;When you publish a series of posts or tutorials, you want them to feel connected. You want the same colors, the same fonts, and the same recognizable card styles every week. With text-to-image prompts, every generation is a fresh roll of the dice. A button that was purple yesterday turns neon blue today.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Small Typos Shouldn't Ruin an Entire Image
&lt;/h3&gt;

&lt;p&gt;This was my biggest headache:&lt;br&gt;&lt;br&gt;
Imagine making a detailed architecture diagram. Everything looks great, but you spot a tiny mistake: &lt;code&gt;proccess&lt;/code&gt; instead of &lt;code&gt;process&lt;/code&gt;, or a port number that says &lt;code&gt;8080&lt;/code&gt; instead of &lt;code&gt;443&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With standard AI image generators, you can’t just click the text and fix it. You have to re-prompt from scratch. That means your whole layout shifts, colors change, and you have to start over.&lt;/p&gt;

&lt;p&gt;With code, fixing a typo takes &lt;strong&gt;2 seconds&lt;/strong&gt;: you change the text in the editor, and the canvas updates in 16 milliseconds.&lt;/p&gt;


&lt;h2&gt;
  
  
  What About AI? (Working With It, Not Fighting It)
&lt;/h2&gt;

&lt;p&gt;To be completely honest: &lt;strong&gt;I still use AI all the time.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;I often ask ChatGPT or Gemini to draft the initial HTML and Tailwind layout for an idea. &lt;/p&gt;

&lt;p&gt;In fact, I built a dedicated &lt;strong&gt;"Prompt AI"&lt;/strong&gt; button right inside Yuwbrndr’s sidebar. It generates a ready-to-use prompt that tells your LLM exactly how to write compatible HTML, Tailwind, and Rough.js code.&lt;/p&gt;

&lt;p&gt;The big difference is &lt;strong&gt;where the control lives&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With text-to-image tools, you are stuck with flat, uneditable pixels.&lt;/li&gt;
&lt;li&gt;With Yuwbrndr, the AI writes the code &lt;em&gt;once&lt;/em&gt;, and from that point on, &lt;strong&gt;you own the canvas&lt;/strong&gt;. You can tweak margins, fix typos, adjust colors, and re-export as many times as you want without burning a single credit.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Enter Yuwbrndr: Code as Your Canvas
&lt;/h2&gt;

&lt;p&gt;These experiences led me to build &lt;a href="https://techaaroorian.github.io/yuwbrndr/" rel="noopener noreferrer"&gt;&lt;strong&gt;Yuwbrndr&lt;/strong&gt;&lt;/a&gt; — a free, browser-native studio where developers design visuals directly with HTML, Tailwind CSS, and Canvas.&lt;/p&gt;

&lt;p&gt;Here are a few ways it changed my day-to-day workflow:&lt;/p&gt;


&lt;h3&gt;
  
  
  1. Multi-Slide Carousels in 1 Click (LinkedIn &amp;amp; Social)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rlirhokzigq8hlg8w46.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rlirhokzigq8hlg8w46.png" alt="Multi-Slide Carousels in 1 Click" width="799" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One of my favorite features in Yuwbrndr is the native &lt;strong&gt;Slide Deck mode&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On LinkedIn, multi-slide document carousels get significantly more attention than static single images because readers swipe through them step-by-step.&lt;/p&gt;

&lt;p&gt;In Yuwbrndr:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Switch from &lt;strong&gt;Single design&lt;/strong&gt; to &lt;strong&gt;Slide deck&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add up to 6 structured slides (Hook, Problem, Code, Solution, Summary).&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Export PDF&lt;/strong&gt;: Yuwbrndr renders every slide at 2x Retina resolution and compiles them into a clean, ready-to-upload PDF.&lt;/li&gt;
&lt;li&gt;Need separate images for an X/Twitter thread? Click &lt;strong&gt;Export ZIP&lt;/strong&gt; to download all slides as numbered PNGs in one click.&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  The Numbers: Why Format Matters
&lt;/h3&gt;

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

&lt;p&gt;Here is what surprised me when I looked into recent publishing data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;According to &lt;a href="https://www.socialinsider.io/blog/linkedin-benchmarks/" rel="noopener noreferrer"&gt;Socialinsider’s analysis of 1.4 million LinkedIn posts&lt;/a&gt;, &lt;strong&gt;multi-slide document carousels achieve a 5.48% average engagement rate&lt;/strong&gt;, compared to &lt;strong&gt;2.14% for single image posts&lt;/strong&gt; — more than &lt;strong&gt;2.5x higher&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Reader dwell time is &lt;strong&gt;3.2x longer&lt;/strong&gt; on carousels because people swipe through instead of scrolling right past.&lt;/li&gt;
&lt;li&gt;Over &lt;strong&gt;82% of social feed impressions happen on mobile phones&lt;/strong&gt;, meaning tiny 12px text in complex diagrams gets completely skipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I actually designed this benchmark summary card directly in Yuwbrndr using the preset below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative w-full h-full bg-[#0c0d12] text-slate-100 flex flex-col justify-between p-10 overflow-hidden font-sans select-none border border-slate-800"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Subtle Monotone Technical Grid (No colored glowing blur orbs) --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"absolute inset-0 bg-[linear-gradient(to_right,#ffffff04_1px,transparent_1px),linear-gradient(to_bottom,#ffffff04_1px,transparent_1px)] bg-[size:32px_32px] pointer-events-none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Top Metadata Bar: Editorial Data Header --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative z-10 flex items-center justify-between pb-4 border-b border-slate-800/80 text-xs font-mono"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center gap-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"px-2.5 py-1 rounded bg-slate-800 text-slate-200 font-semibold tracking-wide"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        BENCHMARK REPORT
      &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-400"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Sample: 1,400,000+ LinkedIn Posts&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-500 flex items-center gap-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Source: Socialinsider Study&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;•&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-400"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;2026 Data&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Main Section: Clean Editorial Title &amp;amp; Comparative Vertical Chart --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative z-10 grid grid-cols-12 gap-10 items-end my-auto py-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Left Column: Title &amp;amp; Key Findings (5 cols) --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"col-span-5 space-y-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-3xl font-extrabold text-white tracking-tight leading-tight"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Document Carousels Outperform Single Images
      &lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-400 text-sm leading-relaxed"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Multi-slide technical decks generate &lt;span class="nt"&gt;&amp;lt;strong&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-white font-semibold"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;2.56x higher engagement&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt; than static screenshots because readers actively swipe rather than scroll past.
      &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pt-2 space-y-2 text-xs font-mono text-slate-300"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center gap-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-1.5 h-1.5 rounded-full bg-emerald-400"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Carousel Engagement: &lt;span class="nt"&gt;&amp;lt;strong&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-emerald-400"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;5.48%&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center gap-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-1.5 h-1.5 rounded-full bg-slate-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Single Image Engagement: &lt;span class="nt"&gt;&amp;lt;strong&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-300"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;2.14%&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center gap-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-1.5 h-1.5 rounded-full bg-slate-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Text-Only Engagement: &lt;span class="nt"&gt;&amp;lt;strong&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-400"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;1.92%&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Right Column: Authentic Vertical Bar Chart with Y-Axis (7 cols) --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"col-span-7 bg-[#13151f] border border-slate-800 rounded-xl p-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-center text-xs font-mono text-slate-400 mb-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"uppercase tracking-wider"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Average Engagement Rate by Post Format&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-emerald-400 font-semibold"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+156% Lift&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

      &lt;span class="c"&gt;&amp;lt;!-- Vertical Bar Chart Area --&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-44 flex items-end justify-around gap-6 pt-4 border-b border-slate-700/60 relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Y-Axis Gridlines &amp;amp; Ticks --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"absolute inset-x-0 top-0 border-b border-dashed border-slate-800/80 flex justify-between text-[10px] font-mono text-slate-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;6.0%&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"absolute inset-x-0 top-1/2 border-b border-dashed border-slate-800/80 flex justify-between text-[10px] font-mono text-slate-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;3.0%&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Bar 1: Multi-Slide Carousel (Winner) --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 flex flex-col items-center h-full justify-end group"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-mono text-sm font-bold text-emerald-400 mb-1.5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;5.48%&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full max-w-[84px] h-[91%] bg-emerald-500 rounded-t-md relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"absolute top-1 inset-x-1 h-1 bg-white/20 rounded-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Bar 2: Single Image --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 flex flex-col items-center h-full justify-end group"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-mono text-xs font-semibold text-slate-300 mb-1.5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;2.14%&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full max-w-[84px] h-[36%] bg-slate-600 rounded-t-md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Bar 3: Text Only --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 flex flex-col items-center h-full justify-end group"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-mono text-xs font-semibold text-slate-400 mb-1.5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;1.92%&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full max-w-[84px] h-[32%] bg-slate-700 rounded-t-md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

      &lt;span class="c"&gt;&amp;lt;!-- X-Axis Labels --&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-around gap-6 pt-3 text-center text-xs font-mono"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 font-bold text-white"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Multi-Slide PDF&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 text-slate-400"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Single Image&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 text-slate-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Text Only&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Bottom Data Takeaways: 3 Grounded Metrics --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative z-10 grid grid-cols-3 gap-4 pt-4 border-t border-slate-800/80 text-xs font-mono"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center gap-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-xl font-black text-emerald-400"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;2.56x&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-400 leading-tight"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Higher average engagement vs single image&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center gap-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-xl font-black text-white"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;3.2x&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-400 leading-tight"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Longer reader dwell time per swipe&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center gap-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-xl font-black text-amber-400"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;82%&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-400 leading-tight"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Mobile readers require bold slide text&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2. Hand-Drawn Whiteboard Art with Rough.js
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7dgpilopa7opmayswg2y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7dgpilopa7opmayswg2y.png" alt="Hand-Drawn Whiteboard Art with Rough.js" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To move away from generic AI visuals, I integrated &lt;a href="https://roughjs.com/" rel="noopener noreferrer"&gt;Rough.js&lt;/a&gt; directly into the studio.&lt;/p&gt;

&lt;p&gt;Instead of writing complex canvas math, you can draw sketchy whiteboard boxes using simple HTML attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Hand-drawn sketchy box with cross-hatch fill --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; 
  &lt;span class="na"&gt;data-rough-rect=&lt;/span&gt;&lt;span class="s"&gt;"fill: #8b5cf6; fillStyle: cross-hatch; roughness: 2; stroke: #a78bfa"&lt;/span&gt; 
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"absolute inset-0 w-full h-full -z-10"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"p-6 font-comic text-white"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-xl font-bold"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;API Gateway &lt;span class="err"&gt;&amp;amp;&lt;/span&gt; Edge Proxy&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-300"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Rate limiting, SSL termination, JWT checks&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paired with open-source fonts like &lt;code&gt;Comic Neue&lt;/code&gt;, &lt;code&gt;Caveat&lt;/code&gt;, and &lt;code&gt;Patrick Hand&lt;/code&gt;, your diagrams feel personal and human — like notes from a real engineering whiteboard session.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Share Everything in the URL (Zero Server Storage)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxgwtrlbqd2al1jxynqo3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxgwtrlbqd2al1jxynqo3.png" alt="Share Everything in the URL (Zero Server Storage)" width="733" height="136"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most tools make you sign up for an account, log in with Google, and save everything to their database.&lt;/p&gt;

&lt;p&gt;I wanted Yuwbrndr to be 100% private and client-side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When you click &lt;strong&gt;Share&lt;/strong&gt;, your entire design (HTML, CSS, aspect ratio, theme) is compressed directly into the URL hash using the browser’s native &lt;code&gt;CompressionStream&lt;/code&gt; (&lt;code&gt;#share=v1z...&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Anyone who opens your link gets the exact same code and live preview loaded instantly.&lt;/li&gt;
&lt;li&gt;No database, no accounts, and no data tracking.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  The Engineering Trade-off:
&lt;/h4&gt;

&lt;p&gt;For short snippets, this works like magic — the link is compact and completely self-contained. But for designs with lots of code, the URL hash can become massive. I’m actively working on a better alternative to handle sharing for larger designs while keeping user privacy first.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Privacy First: No Trackers, No Cookies
&lt;/h3&gt;

&lt;p&gt;Yuwbrndr does not use Google Analytics or tracking cookies. &lt;/p&gt;

&lt;p&gt;Everything you type, upload, or render stays strictly in your browser’s local memory. For basic traffic numbers, we use &lt;a href="https://www.goatcounter.com/" rel="noopener noreferrer"&gt;GoatCounter&lt;/a&gt; — a lightweight, open-source, cookie-free tool that only counts anonymous page views. Your designs and code never leave your machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  It’s 100% Free &amp;amp; Open Source (MIT)
&lt;/h2&gt;

&lt;p&gt;Yuwbrndr is completely free to use and open source under the MIT license:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;Try the Live Studio&lt;/strong&gt;: &lt;a href="https://techaaroorian.github.io/yuwbrndr/" rel="noopener noreferrer"&gt;techaaroorian.github.io/yuwbrndr&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/TechAaroorian/yuwbrndr" rel="noopener noreferrer"&gt;github.com/TechAaroorian/yuwbrndr&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you write technical posts, share architecture diagrams, or just want full control over your visuals without subscription limits, give it a spin.&lt;/p&gt;

&lt;p&gt;I’d love to hear your feedback: &lt;strong&gt;How do you currently create visuals for your technical posts and tutorials?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>Never Use a Display Name for Authorization: Secure Anonymous Editing</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Tue, 22 Sep 2026 05:28:23 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/never-use-a-display-name-for-authorization-secure-anonymous-editing-1gn7</link>
      <guid>https://dev.to/janarthanan_soundararajan/never-use-a-display-name-for-authorization-secure-anonymous-editing-1gn7</guid>
      <description>&lt;p&gt;Public polls, surveys, and collaborative boards often ask participants for a name. The name helps other people understand who submitted each response.&lt;/p&gt;

&lt;p&gt;It should not grant permission to edit that response.&lt;/p&gt;

&lt;p&gt;A display name helps humans identify a participant. It does not prove that an incoming HTTP request came from the person who created the record. Treating identification as authorization creates a serious security flaw in applications that allow anonymous participation.&lt;/p&gt;

&lt;p&gt;Anonymous editing needs a separate credential. One practical option is a cryptographically random token stored in an &lt;code&gt;HttpOnly&lt;/code&gt; cookie, with only its SHA-256 hash saved in the database.&lt;/p&gt;




&lt;h2&gt;
  
  
  Identification and authorization
&lt;/h2&gt;

&lt;p&gt;Consider a row of lockers. Each locker has a name written on it.&lt;/p&gt;

&lt;p&gt;The name helps you locate the correct locker. It cannot open the door. You still need the key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────┐
│                      THE LOCKER                        │
│                                                        │
│  Label (Display Name)  ──▶  Identifies the record      │
│  Key (Session / Token) ──▶  Authorizes modifications   │
│  Interior (Choices/Data)──▶  The protected resource    │
└────────────────────────────────────────────────────────┘

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

&lt;/div&gt;



&lt;p&gt;The same distinction applies to application data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A display name tells people which participant submitted a response.&lt;/li&gt;
&lt;li&gt;A session or token proves that the current request may modify it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code such as this treats public user input as an authorization credential:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Dangerous: Treating user input as an authorization credential&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pollId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;participantName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;participantName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;insensitive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Anyone who knows or guesses the participant’s name could overwrite that person’s response.&lt;/p&gt;

&lt;p&gt;Display names are unsuitable for authorization because they are publicly visible, easy to guess, and often shared by multiple people. They also introduce casing and formatting ambiguities.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Identification answers:&lt;/strong&gt; “Which record are you referring to?”&lt;br&gt;
&lt;strong&gt;Authorization answers:&lt;/strong&gt; “Are you allowed to modify it?”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Two ownership paths
&lt;/h2&gt;

&lt;p&gt;An application that supports both accounts and anonymous participation needs separate authorization paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Incoming Request
       │
       ├─▶ Has Session? ──────▶ Verify session userId
       │
       └─▶ Anonymous Guest? ──▶ Verify HttpOnly edit token hash

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Authenticated participants
&lt;/h3&gt;

&lt;p&gt;For signed-in users, the server reads the user ID from the active session. It should never trust a user ID supplied in the request body or query parameters.&lt;/p&gt;

&lt;p&gt;A composite unique constraint on &lt;code&gt;pollId&lt;/code&gt; and &lt;code&gt;userId&lt;/code&gt; can enforce one participant record per authenticated user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;participant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pollParticipant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;pollId_userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;pollId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;participantName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;participantEmail&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;responseStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RESPONDED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;respondedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pollId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;participantName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;participantEmail&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;responseStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RESPONDED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;respondedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The participant can later change their display name without losing ownership. Their edit permission depends on the immutable &lt;code&gt;userId&lt;/code&gt;, not the name shown in the interface.&lt;/p&gt;




&lt;h3&gt;
  
  
  Anonymous participants
&lt;/h3&gt;

&lt;p&gt;An anonymous participant has no account or server session. The server therefore needs to issue a separate credential that grants access to that participant’s response.&lt;/p&gt;

&lt;p&gt;Generate the credential with a cryptographically secure random-number generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Generate 32 bytes of cryptographically secure entropy&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;editToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Store the raw token in a restricted browser cookie:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cookieStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cookieName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;editToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;httpOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sameSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lax&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 90 days&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Each cookie option has a specific purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;httpOnly&lt;/code&gt; prevents client-side JavaScript from reading the token. This reduces direct token exfiltration through XSS, although it does not remove every XSS risk.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;secure&lt;/code&gt; restricts the cookie to HTTPS connections in production.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sameSite: "lax"&lt;/code&gt; helps block common cross-site request forgery attempts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxAge&lt;/code&gt; limits how long the participant can use the token to edit the response.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Store the hash, not the raw token
&lt;/h2&gt;

&lt;p&gt;A raw edit token is a bearer credential. Anyone who obtains it can use it.&lt;/p&gt;

&lt;p&gt;Saving raw tokens in the database creates unnecessary exposure. If an attacker gains read access to the database, they could copy those tokens and edit active responses.&lt;/p&gt;

&lt;p&gt;Hash the token before storing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashEditToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The request flow then looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser                           Server                         PostgreSQL
   │                                │                                │
   │── Send Cookie (Raw Token) ────▶│                                │
   │                                │── SHA-256(Raw Token) ─────────▶│
   │                                │                                │
   │                                │◀─ Find record matching hash ───│
   │                                │                                │
   │◀── Allow / Reject Mutation ────│                                │

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

&lt;/div&gt;



&lt;p&gt;When the participant returns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The browser sends the raw token through the &lt;code&gt;HttpOnly&lt;/code&gt; cookie.&lt;/li&gt;
&lt;li&gt;The server hashes the token with SHA-256.&lt;/li&gt;
&lt;li&gt;The database query searches for an anonymous participant matching both the &lt;code&gt;pollId&lt;/code&gt; and &lt;code&gt;editTokenHash&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The server permits the update only when that record exists.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;participant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pollParticipant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findFirst&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pollId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;editTokenHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;hashEditToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingEditToken&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why SHA-256 is suitable here
&lt;/h3&gt;

&lt;p&gt;Argon2 and bcrypt are deliberately slow because human-created passwords usually have limited entropy. Their cost makes dictionary attacks more expensive.&lt;/p&gt;

&lt;p&gt;A token generated from 32 random bytes has 256 bits of entropy. It is not vulnerable to the same kind of password guessing, assuming it was generated securely. SHA-256 is therefore suitable for storing a lookup value without adding the CPU cost of password hashing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example: Normalized Polling Architecture in SlotSyncro
&lt;/h2&gt;

&lt;p&gt;To see this pattern in practice, consider &lt;strong&gt;SlotSyncro&lt;/strong&gt;, an open-source meeting poll application built with Prisma and PostgreSQL.&lt;/p&gt;

&lt;p&gt;In SlotSyncro, participants vote on meeting options using three preference states: &lt;em&gt;Yes&lt;/em&gt;, &lt;em&gt;If needed&lt;/em&gt;, and &lt;em&gt;No&lt;/em&gt;. To keep authorization checks independent of voting preferences, the participant record is stored separately from individual slot choices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model PollParticipant {
  id             String             @id @default(cuid())
  pollId         String
  userId         String?
  displayName    String
  email          String?
  responseStatus PollResponseStatus @default(PENDING)
  respondedAt    DateTime?
  editTokenHash  String?            @unique

  poll        Poll             @relation(fields: [pollId], references: [id])
  preferences PollPreference[]

  @@unique([pollId, userId])
}

model PollPreference {
  id            String             @id @default(cuid())
  pollId        String
  participantId String
  candidateId   String
  status        AvailabilityStatus @default(YES)

  participant PollParticipant @relation(
    fields: [participantId, pollId],
    references: [id, pollId]
  )
  candidate PollCandidate @relation(
    fields: [candidateId, pollId],
    references: [id, pollId]
  )

  @@unique([participantId, candidateId])
}

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

&lt;/div&gt;



&lt;p&gt;This separation gives the application a clear ownership boundary. The server first verifies access to &lt;code&gt;PollParticipant&lt;/code&gt;. After that check succeeds, it can modify preferences through the validated participant ID.&lt;/p&gt;

&lt;p&gt;The composite relationships also prevent cross-poll mismatches. A preference associated with Poll A cannot reference a participant or candidate belonging to Poll B.&lt;/p&gt;

&lt;p&gt;Preference replacement happens inside one database transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Clear existing choices for this validated participant&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pollPreference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;participantId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Insert new selections&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pollPreference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;votes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;slotId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;pollId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;participantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;candidateId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;slotId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;If either operation fails, the transaction rolls back. The response cannot be left with its old preferences deleted and only some of the new preferences inserted.&lt;/p&gt;




&lt;h2&gt;
  
  
  Limitations of cookie-backed editing
&lt;/h2&gt;

&lt;p&gt;The token protects anonymous responses from name-based hijacking, but it ties edit access to one browser profile.&lt;/p&gt;

&lt;p&gt;A participant may lose access when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They switch to another device or browser.&lt;/li&gt;
&lt;li&gt;They clear their cookies.&lt;/li&gt;
&lt;li&gt;They use a private browsing window that is later closed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Creating a new response is safer than falling back to an unverified name or email address.&lt;/p&gt;

&lt;p&gt;Applications that need cross-device editing without requiring a full account can add an out-of-band recovery mechanism. Options include a signed, time-limited edit link sent by email or a flow that lets a signed-in user claim an anonymous response.&lt;/p&gt;

&lt;p&gt;Rate limiting can also reduce automated response creation and token-generation abuse. It should be treated as an abuse-control measure, not proof of ownership.&lt;/p&gt;




&lt;h2&gt;
  
  
  Authorization checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Display Name&lt;/th&gt;
&lt;th&gt;Opaque Edit Token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Identification (UI context)&lt;/td&gt;
&lt;td&gt;Authorization (permission to edit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Entropy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Extremely low (human-selected)&lt;/td&gt;
&lt;td&gt;Cryptographically secure (32 bytes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Public to all viewers&lt;/td&gt;
&lt;td&gt;Private (client cookie and hash in database)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Collisions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Common&lt;/td&gt;
&lt;td&gt;Mathematically negligible&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keep display names as labels for people.&lt;/p&gt;

&lt;p&gt;Any request that modifies an existing anonymous response must present a credential that the server can verify. Depending on the application, that credential may be an authenticated session, an opaque edit token, or a signed edit link. A publicly visible name is never enough.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>database</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How to Avoid Unnecessary Re-renders in React Without Memoizing Everything</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Wed, 16 Sep 2026 10:40:55 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/how-to-avoid-unnecessary-re-renders-in-react-without-memoizing-everything-1adi</link>
      <guid>https://dev.to/janarthanan_soundararajan/how-to-avoid-unnecessary-re-renders-in-react-without-memoizing-everything-1adi</guid>
      <description>&lt;p&gt;Add a &lt;code&gt;console.log()&lt;/code&gt; inside a React component and you may see it run more often than expected. The first reaction is often to add &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, and &lt;code&gt;useCallback&lt;/code&gt; everywhere.&lt;/p&gt;

&lt;p&gt;That usually treats the symptom before finding the cause.&lt;/p&gt;

&lt;p&gt;A re-render is also not the same as a DOM update. During rendering, React calls components to calculate the next UI. During the commit phase, it updates only the DOM nodes that changed. A component running again can therefore be harmless. The optimization matters when repeated work is expensive or makes an interaction feel slow.&lt;/p&gt;

&lt;p&gt;Before changing the code, identify why the component rendered and how much work that render performs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does a React component re-render?
&lt;/h2&gt;

&lt;p&gt;A component can render again for several reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Its own state changed.&lt;/li&gt;
&lt;li&gt;One of its ancestors rendered, causing the normal render cascade through descendants.&lt;/li&gt;
&lt;li&gt;A context it reads received a different value.&lt;/li&gt;
&lt;li&gt;An external store subscription returned an updated value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Props need a small clarification. Changed props do not independently schedule a normal child component to render. The parent renders first and supplies those props. Without memoization, the child normally renders as part of that parent render whether its props changed or not.&lt;/p&gt;

&lt;p&gt;Prop equality becomes important when the child is wrapped in &lt;code&gt;React.memo&lt;/code&gt;. React can then compare each prop with its previous value using &lt;code&gt;Object.is&lt;/code&gt; and skip the child when they are equal.&lt;/p&gt;

&lt;p&gt;React's &lt;a href="https://react.dev/learn/render-and-commit" rel="noopener noreferrer"&gt;render and commit documentation&lt;/a&gt; explains this sequence in more detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, decide whether the render needs fixing
&lt;/h2&gt;

&lt;p&gt;A render is unnecessary from a performance perspective only when it repeats work that has a measurable cost without producing a useful change. A small component rendering after its parent is normal React behavior. If the interaction remains responsive, adding memoization may give the code more complexity than speed.&lt;/p&gt;

&lt;p&gt;The same caution applies to inline values. An inline callback, object, or array is not automatically a performance problem. Its changing identity matters when another part of the program observes that identity, such as a memoized child, an Effect dependency, or a store subscription.&lt;/p&gt;

&lt;p&gt;The techniques below are options to apply after identifying a specific source of wasted work. They are not rules that every component should follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Move state closer to where it is used
&lt;/h2&gt;

&lt;p&gt;Start with component structure. If an input owns the only state that changes while someone types, the page does not need to own that state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Typing causes Dashboard and its descendants to render.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;VeryExpensiveTree&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Move the state into a smaller component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SearchInput&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchInput&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;VeryExpensiveTree&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the state update starts inside &lt;code&gt;SearchInput&lt;/code&gt;. React does not need to render &lt;code&gt;Dashboard&lt;/code&gt; or &lt;code&gt;VeryExpensiveTree&lt;/code&gt; for every keystroke.&lt;/p&gt;

&lt;p&gt;This applies to other temporary UI state too. If only a modal uses a form value, it can usually own that state. A tooltip can usually own its open state. Moving either value upward is still correct when another part of the tree needs to read or control it.&lt;/p&gt;

&lt;p&gt;Yes. The idea is useful, but the explanation can be shorter. I would replace it with this:&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Keep expensive content outside the stateful wrapper
&lt;/h2&gt;

&lt;p&gt;If a wrapper has local state, pass large child content through &lt;code&gt;children&lt;/code&gt; instead of creating it inside the wrapper.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;InteractivePanelProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;InteractivePanel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;InteractivePanelProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;highlighted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setHighlighted&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;highlighted&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;highlighted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setHighlighted&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Toggle highlight
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;InteractivePanel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;VeryExpensiveTree&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;InteractivePanel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;highlighted&lt;/code&gt; changes, &lt;code&gt;InteractivePanel&lt;/code&gt; re-renders. &lt;code&gt;Dashboard&lt;/code&gt; does not, so React can reuse the same &lt;code&gt;children&lt;/code&gt; element without rendering &lt;code&gt;VeryExpensiveTree&lt;/code&gt; again.&lt;/p&gt;

&lt;p&gt;This only isolates updates caused by the wrapper's local state. &lt;code&gt;VeryExpensiveTree&lt;/code&gt; can still render when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Dashboard&lt;/code&gt; re-renders.&lt;/li&gt;
&lt;li&gt;Its own state or context changes.&lt;/li&gt;
&lt;li&gt;It is removed and mounted again through conditional rendering.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use composition to limit how far a local state update travels. It is a structural optimization, not a replacement for &lt;code&gt;React.memo&lt;/code&gt; in every case.&lt;/p&gt;

&lt;p&gt;The main improvement is the heading: “Keep expensive content outside the stateful wrapper” explains the technique more directly than “Pass stable content through composition.”&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Skip expensive cascade renders with &lt;code&gt;React.memo&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When a parent renders, its child components normally render too. &lt;code&gt;React.memo&lt;/code&gt; can skip a child when all its props remain equal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;memo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ItemListProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ItemList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ItemList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ItemListProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;React.memo&lt;/code&gt; is a reasonable option when these conditions are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The parent renders frequently.&lt;/li&gt;
&lt;li&gt;The child often receives the same props.&lt;/li&gt;
&lt;li&gt;Rendering the child performs enough work to matter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not block every render. The component still renders when its own state changes or when a context it consumes changes. React also describes memoization as a performance optimization rather than a guarantee in the &lt;a href="https://react.dev/reference/react/memo" rel="noopener noreferrer"&gt;&lt;code&gt;memo&lt;/code&gt; documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Wrapping every component in &lt;code&gt;memo&lt;/code&gt; adds prop comparisons and can make the code harder to follow without improving the user experience. In a component with cheap rendering or frequently changing props, skipping memoization may be the better choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keep function props stable with &lt;code&gt;useCallback&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;JavaScript creates a new function whenever the parent executes this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MemoizedItem&lt;/span&gt; &lt;span class="na"&gt;onDelete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;deleteItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function reference is different on every parent render. If &lt;code&gt;MemoizedItem&lt;/code&gt; is wrapped in &lt;code&gt;React.memo&lt;/code&gt;, that new reference prevents React from skipping it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useCallback&lt;/code&gt; can keep the reference stable until one of its dependencies changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleDelete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;deleteItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Count: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MemoizedItem&lt;/span&gt; &lt;span class="na"&gt;onDelete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleDelete&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The empty dependency array is correct only because this example uses a stable &lt;code&gt;deleteItem&lt;/code&gt; function declared outside the component. If the callback reads props or state, include those reactive values in its dependency list.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useCallback&lt;/code&gt; does not stop &lt;code&gt;Parent&lt;/code&gt; from rendering, and caching a callback has little value when it is passed to a non-memoized child. Its common use is keeping a function prop stable for a memoized component or satisfying another Hook's dependency requirements. See the &lt;a href="https://react.dev/reference/react/useCallback" rel="noopener noreferrer"&gt;&lt;code&gt;useCallback&lt;/code&gt; reference&lt;/a&gt; for the full behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Use &lt;code&gt;useMemo&lt;/code&gt; for calculations and reference stability
&lt;/h2&gt;

&lt;p&gt;Arrays and objects have the same reference problem as functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visibleItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;filter()&lt;/code&gt; returns a new array on every render. That new array breaks the prop comparison of a memoized list even if &lt;code&gt;items&lt;/code&gt; did not change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visibleItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MemoizedList&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;visibleItems&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This has two possible benefits. It avoids repeating a costly calculation, and it preserves the array reference so &lt;code&gt;MemoizedList&lt;/code&gt; can be skipped.&lt;/p&gt;

&lt;p&gt;Do not use &lt;code&gt;useMemo&lt;/code&gt; for every small calculation. The cache also has a cost, and React may discard cached values in some situations. Use it for performance, not correctness. The &lt;a href="https://react.dev/reference/react/useMemo" rel="noopener noreferrer"&gt;&lt;code&gt;useMemo&lt;/code&gt; documentation&lt;/a&gt; recommends measuring whether the calculation is expensive enough to justify caching.&lt;/p&gt;

&lt;p&gt;Sometimes the cleaner fix is to pass smaller props. If a child only needs &lt;code&gt;user.name&lt;/code&gt;, pass the name instead of the full &lt;code&gt;user&lt;/code&gt; object. Primitive props are often easier to keep stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Split contexts to reduce subscriptions
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;React.memo&lt;/code&gt; cannot protect a component from updates to a context that it reads. When the provider receives a different value, consumers of that context render again.&lt;/p&gt;

&lt;p&gt;One large context makes unrelated consumers depend on the same update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AppContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dispatch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AppContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Changing &lt;code&gt;notifications&lt;/code&gt; can cause a component that only reads &lt;code&gt;theme&lt;/code&gt; to render. Splitting unrelated values into separate contexts narrows the update.&lt;/p&gt;

&lt;p&gt;State and dispatch can also be separated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Dispatch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AppAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RESET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;StateContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppState&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DispatchContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Dispatch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppAction&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AppProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useReducer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DispatchContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;dispatch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StateContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;StateContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DispatchContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ActionButton&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dispatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DispatchContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ActionButton must be used inside AppProvider&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RESET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Reset
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dispatch&lt;/code&gt; function returned by &lt;code&gt;useReducer&lt;/code&gt; has a stable identity. &lt;code&gt;ActionButton&lt;/code&gt; can therefore dispatch actions without subscribing to the state context.&lt;/p&gt;

&lt;p&gt;This split does not solve every context render. A component reading &lt;code&gt;StateContext&lt;/code&gt; still renders whenever the &lt;code&gt;state&lt;/code&gt; object changes. For a large shared state, split contexts by concern or use a store that supports selecting a smaller state slice. React's guide on &lt;a href="https://react.dev/learn/scaling-up-with-reducer-and-context" rel="noopener noreferrer"&gt;scaling with reducer and context&lt;/a&gt; uses the same state-and-dispatch separation.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Use &lt;code&gt;useRef&lt;/code&gt; for data that does not affect the UI
&lt;/h2&gt;

&lt;p&gt;State should represent data used during rendering. If changing a value should not change the JSX, a ref may be a better fit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Tracker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clickCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;clickCount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleClick&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Track click&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Updating &lt;code&gt;clickCount.current&lt;/code&gt; does not schedule a render. Refs are useful for timer IDs, DOM nodes, previous values, and mutable tracking data.&lt;/p&gt;

&lt;p&gt;Do not move visible data into a ref just to avoid rendering. If the count must appear on screen, it belongs in state. React will not update the UI when only &lt;code&gt;ref.current&lt;/code&gt; changes. The &lt;a href="https://react.dev/reference/react/useRef" rel="noopener noreferrer"&gt;&lt;code&gt;useRef&lt;/code&gt; documentation&lt;/a&gt; makes this distinction explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Remove Effects that only derive state
&lt;/h2&gt;

&lt;p&gt;An Effect that calculates one piece of state from another often creates an avoidable second render.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastName&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ProfileProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fullName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFullName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setFullName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;fullName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component first renders with the old &lt;code&gt;fullName&lt;/code&gt;. React commits that result, runs the Effect, updates state, and renders again.&lt;/p&gt;

&lt;p&gt;Calculate the value during rendering instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastName&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ProfileProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;fullName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;useMemo&lt;/code&gt; only if the derived calculation is expensive. React's &lt;a href="https://react.dev/learn/you-might-not-need-an-effect" rel="noopener noreferrer"&gt;You Might Not Need an Effect&lt;/a&gt; guide covers this pattern and other unnecessary Effect chains.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Subscribe to the smallest store value you need
&lt;/h2&gt;

&lt;p&gt;The same principle applies to Redux, Zustand, and other external stores. A component that subscribes to a large object may render for changes it never displays.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Broad subscription&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSelector&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RootState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Narrow subscription&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSelector&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RootState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second component depends only on &lt;code&gt;user.name&lt;/code&gt;. Whether a render is skipped depends on the store library's selector and equality behavior, so check that library's documentation before adding custom comparison functions. For example, Redux &lt;code&gt;useSelector&lt;/code&gt; and Zustand selectors do not have identical APIs or defaults.&lt;/p&gt;

&lt;p&gt;Also avoid returning a fresh object from a selector unless the library provides shallow comparison or memoized selectors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// New object on every selector call&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userSummary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSelector&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RootState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Selecting primitives separately or using a memoized selector gives the subscription a stable result when the underlying values have not changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Profile before and after optimizing
&lt;/h2&gt;

&lt;p&gt;Console logs can show that a component rendered, but they do not show whether the render was expensive. React Strict Mode may also call component functions more than once during development to detect impure rendering. That can make logs look worse than production behavior.&lt;/p&gt;

&lt;p&gt;Use React DevTools Profiler or React's &lt;a href="https://react.dev/reference/react/Profiler" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;Profiler&amp;gt;&lt;/code&gt; API&lt;/a&gt; to answer more useful questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which interaction feels slow?&lt;/li&gt;
&lt;li&gt;Which components rendered during it?&lt;/li&gt;
&lt;li&gt;How long did they take?&lt;/li&gt;
&lt;li&gt;Did memoization reduce the work?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measure a production build when comparing performance. Development checks and tooling add overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small details that can change the diagnosis
&lt;/h2&gt;

&lt;p&gt;A few cases can make render behavior look different from the simplified examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting state to its current value can let React skip the update. You may still see the component function called in some cases before React discards the result.&lt;/li&gt;
&lt;li&gt;A memoized component still renders when its own state changes or a context it reads changes.&lt;/li&gt;
&lt;li&gt;A custom comparison function for &lt;code&gt;React.memo&lt;/code&gt; can cost more than rendering the component. Deep comparisons are especially risky when the data structure can grow.&lt;/li&gt;
&lt;li&gt;Changing a component's &lt;code&gt;key&lt;/code&gt; resets its identity and state. Keys are useful for reconciliation, but changing them is not a general re-render optimization.&lt;/li&gt;
&lt;li&gt;Conditionally removing a component unmounts it. Showing it again creates a new instance, even when its JSX was previously passed through &lt;code&gt;children&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Suspense, transitions, hydration, and concurrent rendering can cause React to start, pause, retry, or discard render work. A function call in a log does not prove that React committed a DOM update.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These cases do not invalidate the earlier techniques. They explain why a small demo, development log, and production profile may show different numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick diagnostic matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;First option to consider&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Typing in one input renders a large page&lt;/td&gt;
&lt;td&gt;Input state is owned too high in the tree&lt;/td&gt;
&lt;td&gt;Move the state into the input or form subtree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expensive child renders when its parent changes unrelated state&lt;/td&gt;
&lt;td&gt;Normal parent-to-child render cascade&lt;/td&gt;
&lt;td&gt;Use composition or &lt;code&gt;React.memo&lt;/code&gt; if the child's props remain stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memoized child still renders because of a callback prop&lt;/td&gt;
&lt;td&gt;A new function reference is passed each time&lt;/td&gt;
&lt;td&gt;Consider &lt;code&gt;useCallback&lt;/code&gt; if stable identity lets that child skip work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memoized list still renders with unchanged source data&lt;/td&gt;
&lt;td&gt;Filtering creates a new array&lt;/td&gt;
&lt;td&gt;Consider &lt;code&gt;useMemo&lt;/code&gt; when the calculation or stable reference matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A theme consumer renders after an unrelated context update&lt;/td&gt;
&lt;td&gt;One context contains unrelated changing values&lt;/td&gt;
&lt;td&gt;Split the context by concern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updating a tracking value renders the component&lt;/td&gt;
&lt;td&gt;Non-visual data is stored in state&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;useRef&lt;/code&gt; if the value does not affect JSX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component renders twice after props change&lt;/td&gt;
&lt;td&gt;An Effect copies or derives state&lt;/td&gt;
&lt;td&gt;Calculate the value during rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What about React Compiler?
&lt;/h2&gt;

&lt;p&gt;When React Compiler is enabled and successfully compiles the relevant components, it can apply memoization automatically and reduce the need for manual &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, and &lt;code&gt;useCallback&lt;/code&gt;. That does not make state placement, context boundaries, or unnecessary Effects irrelevant. Those choices determine how much of the tree participates in an update and how easy the code is to understand.&lt;/p&gt;

&lt;p&gt;If the compiler is not enabled in your project, manual memoization remains useful when profiling identifies repeated expensive work.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical order for optimization
&lt;/h2&gt;

&lt;p&gt;When a screen feels slow, work through the problem in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm which interaction is slow with the Profiler.&lt;/li&gt;
&lt;li&gt;Move temporary state closer to the components that use it.&lt;/li&gt;
&lt;li&gt;Remove Effects that derive state unnecessarily.&lt;/li&gt;
&lt;li&gt;Split broad contexts and store subscriptions.&lt;/li&gt;
&lt;li&gt;Consider &lt;code&gt;React.memo&lt;/code&gt; for expensive children that often receive unchanged props.&lt;/li&gt;
&lt;li&gt;Consider &lt;code&gt;useCallback&lt;/code&gt; or &lt;code&gt;useMemo&lt;/code&gt; when unstable references are defeating that memoization or the calculation itself is expensive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The shortest rule is: reduce the scope of the update before caching values and functions.&lt;/p&gt;

&lt;p&gt;React is designed to re-render components. The goal is not to reach zero re-renders. The goal is to prevent expensive work that produces no useful change for the user.&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>What to Check When Reviewing AI-Generated UI</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Wed, 09 Sep 2026 06:05:57 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/what-to-check-when-reviewing-ai-generated-ui-h28</link>
      <guid>https://dev.to/janarthanan_soundararajan/what-to-check-when-reviewing-ai-generated-ui-h28</guid>
      <description>&lt;p&gt;AI can help us build a form, a modal, or a dashboard quickly.&lt;/p&gt;

&lt;p&gt;The result may look good. The inputs accept text. The buttons respond. The layout fits the screen.&lt;/p&gt;

&lt;p&gt;But before merging the code, there is another question to ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the UI work well beyond the happy path?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some details are easy to miss during a visual review. A label may not be connected to its input. An icon button may have no accessible name. A secondary button may submit a form by accident.&lt;/p&gt;

&lt;p&gt;These are review checkpoints, not claims that every AI tool makes these mistakes. They apply to code we write ourselves too.&lt;/p&gt;

&lt;p&gt;The examples below use React, but most of the ideas come from HTML and apply across frameworks. The snippets focus on individual details rather than a complete application.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Are labels connected to their inputs?
&lt;/h3&gt;

&lt;p&gt;This looks reasonable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email address&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The text is visible, but the label and input are not connected. In JSX, use &lt;code&gt;htmlFor&lt;/code&gt; with a matching input &lt;code&gt;id&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email address&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the input an accessible name. Clicking the label also focuses the input. In plain HTML, the attribute is &lt;code&gt;for&lt;/code&gt;; React uses &lt;code&gt;htmlFor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Wrapping an input inside its label is another valid approach. The point is to create the association, not to add &lt;code&gt;htmlFor&lt;/code&gt; everywhere.&lt;/p&gt;

&lt;p&gt;Also, remember that a placeholder is a hint. It should never replace a visible label.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Will IDs stay unique when components are reused?
&lt;/h3&gt;

&lt;p&gt;A hardcoded &lt;code&gt;id="email"&lt;/code&gt; can work for one field. But what happens when the same component appears twice on the same screen?&lt;/p&gt;

&lt;p&gt;Duplicate IDs break label associations and assistive technology references. For reusable React components, React's &lt;code&gt;useId&lt;/code&gt; hook ensures instance-level uniqueness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email address&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;autoComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each instance gets its own unique ID for the label connection.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; &lt;code&gt;useId&lt;/code&gt; is for accessibility relationships. List keys should come from your underlying data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Do icon-only buttons have accessible names?
&lt;/h3&gt;

&lt;p&gt;A magnifying glass icon may clearly mean “Search” to someone looking at the screen. Make sure the button also provides a programmatic name for screen readers, as well as a visual cue for sighted users who might find the icon ambiguous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Search"&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Search"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt; &lt;span class="na"&gt;aria-hidden&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;viewBox&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;circle&lt;/span&gt; &lt;span class="na"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"6"&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt; &lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"m15 15 6 6"&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;aria-label&lt;/code&gt; supplies the accessible name, while &lt;code&gt;aria-hidden="true"&lt;/code&gt; hides the decorative SVG to avoid redundant announcements. Adding &lt;code&gt;title&lt;/code&gt; or a tooltip gives sighted mouse and keyboard users clarity on hover and focus.&lt;/p&gt;

&lt;p&gt;If a button already contains visible text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Save changes&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not need an additional &lt;code&gt;aria-label&lt;/code&gt;. Prefer visible text whenever the design allows.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Are button types intentional?
&lt;/h3&gt;

&lt;p&gt;Inside a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;, an ordinary &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; without an explicit &lt;code&gt;type&lt;/code&gt; defaults to &lt;code&gt;type="submit"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That becomes an immediate issue when its purpose is “Cancel” or “Show password.” Make the intention explicit on every button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onCancel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Cancel
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Create account
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review every secondary button inside a form. A single missing &lt;code&gt;type="button"&lt;/code&gt; attribute can trigger accidental form submissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Are we using the right HTML elements?
&lt;/h3&gt;

&lt;p&gt;A clickable &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; can look identical to a button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;openSettings&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Settings&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; does not provide native button keyboard handling, focusability, or form integration. Adding &lt;code&gt;role="button"&lt;/code&gt; without implementing &lt;code&gt;Enter&lt;/code&gt; and &lt;code&gt;Space&lt;/code&gt; key handlers still leaves keyboard users stranded.&lt;/p&gt;

&lt;p&gt;Use native semantic elements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* For an action */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;openSettings&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Settings
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* For navigation */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/settings"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Settings&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Native HTML elements give you focus management, accessibility traits, and keyboard operability for free before you write a single line of extra script.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Can someone use the UI with a keyboard?
&lt;/h3&gt;

&lt;p&gt;Set your mouse aside and test the page using only &lt;code&gt;Tab&lt;/code&gt;, &lt;code&gt;Shift + Tab&lt;/code&gt;, &lt;code&gt;Enter&lt;/code&gt;, &lt;code&gt;Space&lt;/code&gt;, and &lt;code&gt;Escape&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can you clearly see which element currently has focus?&lt;/li&gt;
&lt;li&gt;Can you open, navigate, and close custom dropdowns and dialogs?&lt;/li&gt;
&lt;li&gt;When a modal opens, does focus move inside and stay trapped within it?&lt;/li&gt;
&lt;li&gt;Does focus return to the trigger element when the modal closes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When styling focus states in CSS, prefer &lt;code&gt;:focus-visible&lt;/code&gt; over &lt;code&gt;:focus&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Better: Shows strong outline only during keyboard navigation */&lt;/span&gt;
&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="nd"&gt;:focus-visible&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#2563eb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outline-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never set &lt;code&gt;outline: none&lt;/code&gt; without providing a prominent, high-contrast &lt;code&gt;:focus-visible&lt;/code&gt; replacement.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Are validation errors connected and useful?
&lt;/h3&gt;

&lt;p&gt;A red border alone does not explain the issue to users. An error message should identify what went wrong and be programmatically tied to the field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UsernameField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-error`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Username&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;
        &lt;span class="na"&gt;aria-invalid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-describedby&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;errorId&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errorId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"alert"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aria-invalid&lt;/code&gt; flags the field's state to assistive technology.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-describedby&lt;/code&gt; associates the error text with the input so it reads aloud when the field receives focus.&lt;/li&gt;
&lt;li&gt;Adding &lt;code&gt;role="alert"&lt;/code&gt; or using &lt;code&gt;aria-live="polite"&lt;/code&gt; ensures dynamically rendered client-side errors are announced immediately, even if the input is already focused.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8. What happens while data loads or when a request fails?
&lt;/h3&gt;

&lt;p&gt;Review more than just the successful response.&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is it obvious that a network request is running?&lt;/li&gt;
&lt;li&gt;Can a user accidentally submit multiple times?&lt;/li&gt;
&lt;li&gt;Does a failed submission preserve the user's filled-in data?&lt;/li&gt;
&lt;li&gt;If a search input fires rapid requests, can an older response overwrite newer results?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When indicating submission progress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isSubmitting&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;aria-busy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isSubmitting&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isSubmitting&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Saving…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Save changes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on disabled buttons:&lt;/strong&gt; Adding &lt;code&gt;disabled&lt;/code&gt; directly to a focused button can cause screen readers to immediately drop focus back to the top of the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;. For critical flows, consider keeping the button focusable with &lt;code&gt;aria-disabled="true"&lt;/code&gt; while ignoring pointer and click events in your handler.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  9. Does the layout survive real content?
&lt;/h3&gt;

&lt;p&gt;AI prompts and sample mocks often feature convenient, short text. Real production data is messy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test extreme character lengths, such as very long email addresses, multiline error messages, and long URLs.&lt;/li&gt;
&lt;li&gt;Test localized strings that might expand by 30% or more.&lt;/li&gt;
&lt;li&gt;Test zoom up to 200% to ensure text does not clip or overflow out of containers.&lt;/li&gt;
&lt;li&gt;For images, ensure non-decorative images have descriptive &lt;code&gt;alt&lt;/code&gt; text, while decorative illustrations use &lt;code&gt;alt=""&lt;/code&gt; to avoid cluttering screen readers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Before merging: quick checklist
&lt;/h2&gt;

&lt;p&gt;Use this checklist during code review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Inputs have programmatically connected labels (&lt;code&gt;htmlFor&lt;/code&gt; / &lt;code&gt;useId&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;[ ] Generated IDs remain unique when components repeat.&lt;/li&gt;
&lt;li&gt;[ ] Icon-only controls have accessible names (&lt;code&gt;aria-label&lt;/code&gt;) and visual tooltips.&lt;/li&gt;
&lt;li&gt;[ ] Buttons have explicit &lt;code&gt;type="button"&lt;/code&gt; or &lt;code&gt;type="submit"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Interactive actions use &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, while navigation uses &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Keyboard navigation works smoothly with visible &lt;code&gt;:focus-visible&lt;/code&gt; indicators.&lt;/li&gt;
&lt;li&gt;[ ] Field errors use &lt;code&gt;aria-invalid&lt;/code&gt; and &lt;code&gt;aria-describedby&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Loading, disabled, empty, and failure states are explicitly handled.&lt;/li&gt;
&lt;li&gt;[ ] Layout holds up under browser zoom and long, unexpected text strings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Turn the checklist into agent instructions
&lt;/h2&gt;

&lt;p&gt;This checklist does not have to remain a manual reference. You can also include it in your coding agent’s review instructions.&lt;/p&gt;

&lt;p&gt;Ask the agent to check generated UI for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connected labels and unique IDs&lt;/li&gt;
&lt;li&gt;Accessible names for icon-only controls&lt;/li&gt;
&lt;li&gt;Explicit button types&lt;/li&gt;
&lt;li&gt;Native semantic elements&lt;/li&gt;
&lt;li&gt;Keyboard and focus handling&lt;/li&gt;
&lt;li&gt;Programmatically connected validation errors&lt;/li&gt;
&lt;li&gt;Loading, empty, disabled, and failure states&lt;/li&gt;
&lt;li&gt;Long content, zoom, and responsive-layout issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An agent can catch many code-level problems before a pull request reaches manual review. However, it cannot fully reproduce how the interface feels with a keyboard, screen reader, browser zoom, or real production content. Use agent review as an additional review layer, then verify the important interactions manually.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>react</category>
    </item>
    <item>
      <title>Uniqueness Pre-Checks vs. Database Constraints: Why You Need Both</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Wed, 02 Sep 2026 02:55:08 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/uniqueness-pre-checks-vs-database-constraints-why-you-need-both-48j0</link>
      <guid>https://dev.to/janarthanan_soundararajan/uniqueness-pre-checks-vs-database-constraints-why-you-need-both-48j0</guid>
      <description>&lt;p&gt;Many applications need to make sure that a value is unique.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email addresses&lt;/li&gt;
&lt;li&gt;Usernames&lt;/li&gt;
&lt;li&gt;Phone numbers&lt;/li&gt;
&lt;li&gt;Product slugs&lt;/li&gt;
&lt;li&gt;Invitation codes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common approach is to check whether the value already exists before saving it.&lt;/p&gt;

&lt;p&gt;That check is useful, but it is not enough.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A uniqueness pre-check improves UX.&lt;br&gt;&lt;br&gt;
A unique database constraint protects data integrity.&lt;br&gt;&lt;br&gt;
You usually want both.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The uniqueness pre-check
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx4dujgavvp9qi65gj7hi.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx4dujgavvp9qi65gj7hi.jpg" alt="The uniqueness pre-check" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
Suppose a user is registering with an email address.&lt;/p&gt;

&lt;p&gt;Before creating the account, the application can check whether the email already exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existingUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This email is already registered.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the application to show a clear message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This email cannot be used.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is more helpful than showing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Something went wrong.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pre-check improves the user experience because it explains the problem early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; Be careful with email availability messages. Saying “This email is already registered” can reveal whether someone has an account. In sensitive flows, use a more general message, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“This email cannot be used.”&lt;/li&gt;
&lt;li&gt;“If an account exists for this email, you will receive further instructions.”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  But a pre-check is not a guarantee
&lt;/h2&gt;

&lt;p&gt;A pre-check asks the database a question first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Does this email already exist?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in a separate operation, the application creates the record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create the user.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a small gap between these two operations.&lt;/p&gt;

&lt;p&gt;During that gap, another request can create the same value.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple race-condition example
&lt;/h2&gt;

&lt;p&gt;Imagine two people submit the same username at almost the same time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request A: Is "alex" available? → Yes
Request B: Is "alex" available? → Yes

Request A: Create "alex" → Success
Request B: Create "alex" → Success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the database does not enforce uniqueness, both requests may succeed.&lt;/p&gt;

&lt;p&gt;This is called a race condition.&lt;/p&gt;

&lt;p&gt;The username was available when both requests checked it. But both requests checked it before either request saved it.&lt;/p&gt;

&lt;p&gt;This is also known as a TOCTOU problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Time Of Check
        ↓
Time Of Use
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result of the check can become outdated before the actual write happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The database constraint
&lt;/h2&gt;

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

&lt;p&gt;A unique database constraint makes the database responsible for enforcing the rule.&lt;/p&gt;

&lt;p&gt;For example, in Prisma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model User {
  id       String @id @default(cuid())
  username String @unique
  email    String @unique
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the database guarantees that two users cannot have the same username or email address.&lt;/p&gt;

&lt;p&gt;Even if two requests arrive at the same time, only one can successfully create the record.&lt;/p&gt;

&lt;p&gt;The other request will fail.&lt;/p&gt;

&lt;p&gt;This protects the data even when the write comes from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A web form&lt;/li&gt;
&lt;li&gt;An API client&lt;/li&gt;
&lt;li&gt;A background job&lt;/li&gt;
&lt;li&gt;An admin tool&lt;/li&gt;
&lt;li&gt;A migration&lt;/li&gt;
&lt;li&gt;A webhook&lt;/li&gt;
&lt;li&gt;A script&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The database enforces the rule for every write path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling the database error
&lt;/h2&gt;

&lt;p&gt;The database constraint protects the data, but users should not see a raw database error.&lt;/p&gt;

&lt;p&gt;The server should catch the unique-constraint error and return a friendly response.&lt;/p&gt;

&lt;p&gt;With Prisma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;Prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PrismaClientKnownRequestError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;P2002&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;That username or email is already in use.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pre-check gives an early explanation in normal situations.&lt;/p&gt;

&lt;p&gt;The database constraint handles the situations that the pre-check cannot guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not use only the database constraint?
&lt;/h2&gt;

&lt;p&gt;You can rely only on the database constraint for data integrity.&lt;/p&gt;

&lt;p&gt;Your data will still be safe.&lt;/p&gt;

&lt;p&gt;However, the user experience may be less helpful.&lt;/p&gt;

&lt;p&gt;The user might submit a form and receive an error only after the request finishes.&lt;/p&gt;

&lt;p&gt;A pre-check can provide earlier feedback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This username is already taken.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is better than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unique constraint violation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why not use only the pre-check?
&lt;/h2&gt;

&lt;p&gt;A pre-check alone cannot protect your data.&lt;/p&gt;

&lt;p&gt;It can fail because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two requests can arrive at the same time.&lt;/li&gt;
&lt;li&gt;A different API may skip the check.&lt;/li&gt;
&lt;li&gt;A background job may write directly to the database.&lt;/li&gt;
&lt;li&gt;A bug may bypass the application validation.&lt;/li&gt;
&lt;li&gt;A malicious client may send requests directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Application-level checks are useful, but they are not the final authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recommended flow
&lt;/h2&gt;

&lt;p&gt;A robust implementation usually follows this flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User submits the form
        ↓
Application performs a pre-check
        ↓
If the value exists, show a friendly message
        ↓
If it appears available, attempt the insert
        ↓
Database constraint enforces uniqueness
        ↓
If a race occurs, catch the error
        ↓
Show a friendly message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pre-check and the database constraint are not competing solutions.&lt;/p&gt;

&lt;p&gt;They solve different problems:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&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;Application pre-check&lt;/td&gt;
&lt;td&gt;Gives users early and helpful feedback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database constraint&lt;/td&gt;
&lt;td&gt;Guarantees that duplicate data cannot be stored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error handling&lt;/td&gt;
&lt;td&gt;Converts conflicts into a friendly response&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  One detail about checking while typing
&lt;/h2&gt;

&lt;p&gt;Some applications check username availability while the user is typing.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alex
alex_
alex_d
alex_de
alex_dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can create many database requests.&lt;/p&gt;

&lt;p&gt;If you use this approach, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debouncing the request&lt;/li&gt;
&lt;li&gt;Checking only after a minimum number of characters&lt;/li&gt;
&lt;li&gt;Rate-limiting the endpoint&lt;/li&gt;
&lt;li&gt;Checking again when the form is submitted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Availability shown in the UI can become outdated.&lt;/p&gt;

&lt;p&gt;The final database write must still rely on the unique constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;A uniqueness pre-check is helpful, but it is only a preview of the current state.&lt;/p&gt;

&lt;p&gt;A unique database constraint is the actual rule.&lt;/p&gt;

&lt;p&gt;The pre-check improves the user experience.&lt;/p&gt;

&lt;p&gt;The constraint protects the truth of your data.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pre-checks communicate.&lt;br&gt;&lt;br&gt;
Constraints enforce.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For reliable applications, use the pre-check to guide users, use the database constraint to protect your data, and handle the final conflict gracefully.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>backend</category>
      <category>database</category>
    </item>
    <item>
      <title>Monorepo, Turborepo, Modular Monolith, and Microservices: What Actually Changes?</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Tue, 25 Aug 2026 18:08:36 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/monorepo-turborepo-modular-monolith-and-microservices-what-actually-changes-29kl</link>
      <guid>https://dev.to/janarthanan_soundararajan/monorepo-turborepo-modular-monolith-and-microservices-what-actually-changes-29kl</guid>
      <description>&lt;p&gt;A Turborepo can contain multiple applications under &lt;code&gt;apps/&lt;/code&gt;. If those applications can be built and deployed separately, does that mean the system uses microservices?&lt;/p&gt;

&lt;p&gt;Not necessarily. Multiple applications in one repository tell us how the source code is organized. Microservices are defined by runtime and operational boundaries.&lt;/p&gt;

&lt;p&gt;Consider this small structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;platform/
├── apps/
│   ├── marketing/
│   └── app/
└── packages/
    └── db/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is this a monorepo, a modular monolith, or a microservices architecture?&lt;/p&gt;

&lt;p&gt;It is clearly a monorepo. It might contain a modular-monolith application. The tree alone, however, cannot prove that any microservices exist.&lt;/p&gt;

&lt;p&gt;While formalizing the architecture of SlotSyncro, a portfolio application, I wanted to sharpen how I distinguished these familiar concepts. They are often discussed together, even though each describes a different layer of a system. Placing them at their correct layers turns a broad understanding into a more precise architectural model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four concepts at four different layers
&lt;/h2&gt;

&lt;p&gt;Here is the central distinction:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;What it describes&lt;/th&gt;
&lt;th&gt;Primary question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monorepo&lt;/td&gt;
&lt;td&gt;Repository organization&lt;/td&gt;
&lt;td&gt;Where does the code live?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turborepo&lt;/td&gt;
&lt;td&gt;Task orchestration&lt;/td&gt;
&lt;td&gt;How are builds, tests, and other tasks coordinated?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modular monolith&lt;/td&gt;
&lt;td&gt;Internal application design&lt;/td&gt;
&lt;td&gt;How is one deployable application divided?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microservices&lt;/td&gt;
&lt;td&gt;Runtime architecture&lt;/td&gt;
&lt;td&gt;How are services deployed, scaled, and connected?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Turbopack is related by name, but it solves another problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Repository organization → Monorepo
Task orchestration      → Turborepo
Application structure   → Modular monolith
Runtime architecture    → Microservices
Compilation             → Turbopack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turborepo coordinates tasks across workspaces. Turbopack bundles a Next.js application. These concepts do not occupy the same architectural layer, so they are not direct alternatives.&lt;/p&gt;

&lt;p&gt;That is why questions such as “Should I use a monorepo or microservices?” are difficult to answer as stated. A team can use both, either, or neither.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes SlotSyncro a monorepo?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;monorepo&lt;/strong&gt; is one repository that contains multiple related projects. Those projects might be applications, shared libraries, configuration packages, or services. They share Git history and can be changed together, but they can still have separate dependencies, scripts, and release processes.&lt;/p&gt;

&lt;p&gt;SlotSyncro currently has this workspace structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;slotsyncro/
├── apps/
│   ├── marketing/    # Public marketing website
│   └── app/          # Scheduling product
├── packages/
│   └── db/           # Prisma and database infrastructure
├── package.json
├── pnpm-workspace.yaml
└── turbo.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The examples in this article are based on SlotSyncro, a scheduling product I am creating as a portfolio application. The repository is publicly viewable as a practical implementation reference: &lt;a href="https://github.com/TechAaroorian/slotsyncro" rel="noopener noreferrer"&gt;SlotSyncro&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is a monorepo because multiple identifiable projects live in one Git repository. The applications and database package have their own &lt;code&gt;package.json&lt;/code&gt; files, while pnpm workspaces connect them. A change to the product and its database package can be reviewed and committed together.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;workspace&lt;/strong&gt; is one project recognized and managed by the repository's package manager. In this example, &lt;code&gt;apps/app&lt;/code&gt;, &lt;code&gt;apps/marketing&lt;/code&gt;, and &lt;code&gt;packages/db&lt;/code&gt; are separate pnpm workspaces. Each can define its own package name, dependencies, and commands, while using another local workspace as a dependency without publishing it to a package registry.&lt;/p&gt;

&lt;p&gt;The monorepo label does not tell us how many production processes exist. It does not mean every folder is independently deployed, every application is a microservice, or every shared file needs its own package.&lt;/p&gt;

&lt;p&gt;Turborepo is not required either. A repository can be a monorepo using only pnpm, npm, or Yarn workspaces. Turborepo is an additional task runner, not the feature that makes it a monorepo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Turborepo actually does
&lt;/h2&gt;

&lt;p&gt;In this repository, the tools sit roughly in this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Git repository
      ↓
pnpm workspaces
      ↓
Turborepo
      ↓
Next.js, Prisma, Vitest, and other workspace tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The root commands delegate work across the repository. Typical Turborepo commands look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;turbo run build
turbo run &lt;span class="nb"&gt;test
&lt;/span&gt;turbo run lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turborepo understands workspace dependencies, schedules tasks in the right order, caches successful outputs, avoids repeated work, and can filter execution to selected workspaces. For example, a task graph can ensure that database-client generation runs before dependent application builds.&lt;/p&gt;

&lt;p&gt;None of this provides service-to-service communication. Turborepo does not create API gateways, message queues, independent databases, distributed transactions, container orchestration, or production scaling rules.&lt;/p&gt;

&lt;p&gt;Turborepo knows that one workspace depends on another. It does not decide whether those workspaces become separate production services.&lt;/p&gt;

&lt;p&gt;In current Next.js versions, &lt;a href="https://nextjs.org/docs/app/getting-started/installation" rel="noopener noreferrer"&gt;Turbopack is the default bundler&lt;/a&gt; for development and production builds unless Webpack is explicitly selected. The similar names can make the tools sound interchangeable, but their scopes are different: Turborepo operates on the repository's task graph, while Turbopack operates on an application's module graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  A modular monolith inside a monorepo
&lt;/h2&gt;

&lt;p&gt;Now move one level inward, from the repository to &lt;code&gt;apps/app&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;modular monolith&lt;/strong&gt; is one deployable application organized into distinct business modules. The modules have defined responsibilities and controlled ways to interact, but they remain part of the same application boundary instead of becoming independently operated network services. It keeps the operational simplicity of a monolith while making internal ownership and dependencies clearer.&lt;/p&gt;

&lt;p&gt;SlotSyncro’s current product code is still mainly grouped by technical concerns such as routes, actions, components, and utilities. The accepted direction is to evolve it incrementally into business modules. A possible target shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/app/
├── app/                       # Next.js routes
└── modules/                   # Proposed structure
    ├── identity/
    ├── availability/
    ├── booking/
    ├── polls/
    ├── meetings/
    └── notifications/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These would be business modules, not microservices. They would have clear responsibilities and controlled interfaces, but remain part of the same Next.js application, deploy together, and use one PostgreSQL database.&lt;/p&gt;

&lt;p&gt;The runtime remains simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monorepo
├── Marketing application
└── Scheduling application ───────── one deployment boundary
    ├── Identity module
    ├── Availability module
    ├── Booking module
    ├── Polls module
    └── Notifications module
                 ↓
             PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The boundaries are architectural, but they are not network boundaries. A booking operation can call availability logic as a typed function and update related state in one local database transaction.&lt;/p&gt;

&lt;p&gt;That is the useful middle ground of a modular monolith: explicit ownership without immediately accepting the operational cost of a distributed system. A monolith is not synonymous with unstructured legacy code. It can have deliberate, well-tested internal boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Folder names are not module boundaries
&lt;/h2&gt;

&lt;p&gt;A booking module might eventually use a small internal structure like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules/booking/
├── domain/
│   ├── booking.ts
│   └── booking-rules.ts
├── application/
│   ├── create-booking.ts
│   └── cancel-booking.ts
├── infrastructure/
│   └── booking-repository.ts
├── ui/
│   └── booking-form.tsx
└── index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact folders matter less than the ownership they express. The module’s &lt;code&gt;index.ts&lt;/code&gt; can act as its public API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createBooking&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/modules/booking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code outside the module should avoid reaching through that API into implementation details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Avoid reaching into another module's internals.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bookingRepository&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/modules/booking/infrastructure/booking-repository&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Folders help people see the intended boundary, but they do not enforce it. Public APIs, code review, dependency rules, and tests make the boundary real. Modular-monolith architecture depends more on enforced boundaries than on folder names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why multiple applications still do not mean microservices
&lt;/h2&gt;

&lt;p&gt;Return to the original two applications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/
├── marketing/
└── app/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;marketing&lt;/code&gt; presents public product information. &lt;code&gt;app&lt;/code&gt; owns authentication, availability, bookings, polls, and other scheduling workflows. They are different user-facing surfaces, and the repository can build them independently.&lt;/p&gt;

&lt;p&gt;That still says nothing about the product’s internal business capabilities being independently operated services. Its booking, polls, and notification concerns currently belong to the same product application. No internal network call is required merely because the repository contains two Next.js apps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Multiple applications” is a repository observation. “Microservices” is a runtime conclusion.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even independent deployment of the marketing and product sites would not make either one a microservice architecture. They may simply be two deployable web applications with different purposes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would actually create microservices?
&lt;/h2&gt;

&lt;p&gt;Imagine the repository later changed to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;platform/
├── apps/
│   ├── marketing/
│   ├── web/
│   ├── scheduling-service/
│   ├── billing-service/
│   ├── notification-service/
│   └── background-worker/
└── packages/
    ├── contracts/
    ├── observability/
    └── config/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The names suggest microservices, but the folders still do not prove it. These components become services through meaningful operational independence: they run as separate processes, deploy and scale independently, communicate through APIs or messages, isolate at least some failures, and have intentionally defined business and data ownership.&lt;/p&gt;

&lt;p&gt;A simplified runtime might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────┐
│   Web application   │  Deployment A
└──────────┬──────────┘
           │ HTTPS
┌──────────▼──────────┐
│ Scheduling service │  Deployment B
└──────────┬──────────┘
           │ MeetingScheduled event
┌──────────▼──────────┐
│Notification service│  Deployment C
└─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the boundaries are also network and deployment boundaries. That creates capabilities such as independent scaling and release ownership, but it also introduces timeouts, retries, message delivery, distributed tracing, versioned contracts, and consistency decisions.&lt;/p&gt;

&lt;p&gt;The trade-off is easier to see side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Modular monolith&lt;/th&gt;
&lt;th&gt;Microservices&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Modules call one another in-process&lt;/td&gt;
&lt;td&gt;Services communicate over a network or queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usually deployed together&lt;/td&gt;
&lt;td&gt;Independently deployable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Easier cross-module transactions&lt;/td&gt;
&lt;td&gt;Distributed consistency must be handled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One primary runtime unit&lt;/td&gt;
&lt;td&gt;Multiple runtime units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lower operational complexity&lt;/td&gt;
&lt;td&gt;Higher operational complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boundaries enforced in code&lt;/td&gt;
&lt;td&gt;Boundaries also enforced by the network&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Independent deployment is important, but it is not the whole definition. If several processes must always release together, share unclear data ownership, and cannot fail independently, the system may be a distributed monolith instead.&lt;/p&gt;

&lt;p&gt;Microservices are not the inevitable final stage of a successful modular monolith. For SlotSyncro’s current scale and requirements, local calls and transactions are valuable, while independent service operation has not earned its cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turborepo supports either runtime architecture
&lt;/h2&gt;

&lt;p&gt;Turborepo can coordinate a modular-monolith repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/marketing
apps/app
packages/db
packages/ui       # possible future extraction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can also coordinate a repository containing services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/web
apps/scheduling-service
apps/notification-service
apps/worker
packages/contracts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In both cases, it can run the complete build or target a workspace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;turbo run build
turbo run build &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;scheduling-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second repository may use microservices because of how those applications run, communicate, and deploy—not because Turborepo launched their build tasks. Turborepo can manage a repository containing microservices, but it does not turn applications into microservices.&lt;/p&gt;

&lt;p&gt;A monorepo can therefore contain a marketing application, a modular-monolith product, and several genuine services at the same time. Repository boundaries and runtime boundaries do not need to match one-to-one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep dependency direction clear
&lt;/h2&gt;

&lt;p&gt;One general monorepo rule prevents a great deal of confusion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Applications may import packages.
Packages must not import applications.
Good: apps/app → packages/db
Bad:  packages/db → apps/app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;packages/db&lt;/code&gt; imports application authentication code, the dependency becomes circular:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/app → packages/db → apps/app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package is no longer reusable infrastructure; it secretly depends on one consumer. Ownership becomes unclear, and isolated testing becomes harder.&lt;/p&gt;

&lt;p&gt;For example, database infrastructure should not retrieve application-specific session context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad: packages/db knows about an application's authentication.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getCurrentUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../../apps/app/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, the database package can expose infrastructure without importing from an application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/db&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The booking module can own its product-specific query while depending on that infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/app/modules/booking/infrastructure/booking-repository.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@slotsyncro/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findBookings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;booking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application layer supplies the authentication context it owns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/app/modules/booking/application/list-my-bookings.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getCurrentUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bookings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findBookings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This principle applies to monorepos generally. Turborepo can understand a dependency graph, but it cannot decide whether that graph represents sensible ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should a module become a package?
&lt;/h2&gt;

&lt;p&gt;Once developers see a &lt;code&gt;packages/&lt;/code&gt; directory, it is tempting to move every cleanly named capability into it. That usually creates boundaries before there is evidence for them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Application module&lt;/th&gt;
&lt;th&gt;Shared package&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Belongs to one application&lt;/td&gt;
&lt;td&gt;Lives outside a specific application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Represents a product capability&lt;/td&gt;
&lt;td&gt;Provides a reusable capability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May depend on application conventions&lt;/td&gt;
&lt;td&gt;Should expose a stable interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usually has one consumer&lt;/td&gt;
&lt;td&gt;Often has multiple consumers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploys with its application&lt;/td&gt;
&lt;td&gt;May be consumed by several applications&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, SlotSyncro should begin with booking behavior close to its only consumer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/app/modules/booking/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should not create &lt;code&gt;packages/booking&lt;/code&gt; merely to make the repository tree look more sophisticated. Keeping code inside its only consumer is not poor architecture. It is often the clearest expression of ownership.&lt;/p&gt;

&lt;p&gt;Possible future extractions include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/
├── domain/    # Framework-independent scheduling rules
├── email/     # Shared templates or rendering infrastructure
└── ui/        # Components genuinely shared by both applications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are proposals, not current SlotSyncro workspaces. &lt;code&gt;packages/domain&lt;/code&gt; becomes useful if scheduling calculations need to run in the product, a worker, and isolated tests. &lt;code&gt;packages/email&lt;/code&gt; becomes useful after multiple notification workflows need common rendering. &lt;code&gt;packages/ui&lt;/code&gt; becomes especially useful when the marketing and product applications share a maintained design system, or when the component system has meaningful independent development and testing value.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical extraction test
&lt;/h2&gt;

&lt;p&gt;A package should have at least one strong justification, and preferably more than one.&lt;/p&gt;

&lt;h3&gt;
  
  
  More than one consumer
&lt;/h3&gt;

&lt;p&gt;Two applications or runtimes need the same capability. Reuse is real, not predicted from one similar-looking component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Framework-independent domain value
&lt;/h3&gt;

&lt;p&gt;Scheduling calculations can run without React, Next.js, Prisma, or a provider SDK. That isolation makes the code easier to reuse and reason about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stable inputs and outputs
&lt;/h3&gt;

&lt;p&gt;The capability exposes a small, intentional API instead of importing application internals. If its interface changes with every feature, extraction may be premature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Independent testing value
&lt;/h3&gt;

&lt;p&gt;Its behavior can be tested meaningfully without starting the complete application. This is especially useful for scheduling, timezone, or consensus rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Different build or deployment concerns
&lt;/h3&gt;

&lt;p&gt;Database generation, email rendering, shared contracts, or worker code may need distinct tasks. A workspace boundary can make those workflows explicit.&lt;/p&gt;

&lt;p&gt;Do not extract code merely because &lt;code&gt;packages/&lt;/code&gt; makes the repository look more sophisticated. Extraction should clarify ownership or enable a real workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision framework
&lt;/h2&gt;

&lt;p&gt;When the terminology starts to blur, I return to these questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do multiple related projects need to live together?
→ Consider a monorepo.
Do you need caching and coordinated workspace tasks?
→ Consider Turborepo.
Does one deployable application need stronger internal boundaries?
→ Use a modular monolith.
Does one capability require independent deployment, scaling,
failure isolation, data ownership, or team autonomy?
→ Consider extracting a service.
Is some code used by only one application?
→ Keep it close to that application for now.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sequence avoids using a runtime solution for a repository problem—or creating a network boundary when a code boundary is enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository note:&lt;/strong&gt; SlotSyncro is a public portfolio and reference repository, not an open-source project or starter template. Some modules and packages discussed in this article are architectural proposals and do not exist in the current implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complete mental model
&lt;/h2&gt;

&lt;p&gt;A monorepo organizes where code lives.&lt;/p&gt;

&lt;p&gt;A modular monolith organizes boundaries inside one application.&lt;/p&gt;

&lt;p&gt;Microservices define independently operated runtime boundaries.&lt;/p&gt;

&lt;p&gt;Turborepo coordinates tasks across the repository.&lt;/p&gt;

&lt;p&gt;Turbopack compiles a Next.js application.&lt;/p&gt;

&lt;p&gt;The number of folders under &lt;code&gt;apps/&lt;/code&gt; does not determine the runtime architecture. Deployment, communication, scaling, and ownership boundaries do.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>monorepo</category>
      <category>turborepo</category>
    </item>
    <item>
      <title>I thought Vite broke my 3D physics. It actually taught me how tree-shaking really works.</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Sun, 16 Aug 2026 09:05:54 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/i-thought-vite-broke-my-3d-physics-it-actually-taught-me-how-tree-shaking-really-works-30oj</link>
      <guid>https://dev.to/janarthanan_soundararajan/i-thought-vite-broke-my-3d-physics-it-actually-taught-me-how-tree-shaking-really-works-30oj</guid>
      <description>&lt;p&gt;In modern browser-based game development, every kilobyte matters. Larger JavaScript payloads increase download, parse, compile, and execution times, which can delay interactivity—especially on slower devices or networks.&lt;/p&gt;

&lt;p&gt;To enforce a strict, lightweight architecture for our latest project at &lt;strong&gt;techaaroorian-games&lt;/strong&gt;, we utilized Babylon's ES module packages (&lt;code&gt;@babylonjs/core&lt;/code&gt;). For a modular application, this structure gives the bundler as much opportunity as possible to remove code the application doesn't actively use.&lt;/p&gt;

&lt;p&gt;We set up Vite, TypeScript, Babylon.js, and the Havok WebAssembly physics engine.&lt;/p&gt;

&lt;p&gt;Then, the engine crashed.&lt;br&gt;
&lt;em&gt;Error: `No Physics Engine available.&lt;/em&gt;`&lt;/p&gt;

&lt;p&gt;Here is the debugging story of why our physics vanished, and what it teaches us about modern web bundlers.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. The Simple Tree-Shaking Model
&lt;/h3&gt;

&lt;p&gt;To understand why my game crashed, I had to look at how modern bundlers view our code. Vite uses &lt;strong&gt;Rolldown&lt;/strong&gt; for its production builds—it doesn't read our minds; it reads the module graph.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is the code reachable and needed?&lt;/strong&gt; → Keep it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can the bundler safely determine it's unnecessary?&lt;/strong&gt; → Remove it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But what happens when a module doesn't export an API you need to call, but instead contains &lt;em&gt;module-level registration code&lt;/em&gt; you need to run?&lt;/p&gt;
&lt;h3&gt;
  
  
  2. The Wasm Trap
&lt;/h3&gt;

&lt;p&gt;My first hurdle wasn't even tree-shaking; it was dependency optimization. My terminal completely froze when I tried to start the development server.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@babylonjs/havok&lt;/code&gt; ships JavaScript alongside WebAssembly assets, and in my setup, Vite's dependency optimization stalled while processing the package.&lt;/p&gt;

&lt;p&gt;I fixed it by telling Vite not to pre-bundle that dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;optimizeDeps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@babylonjs/havok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This lets Vite handle the package outside the dependency pre-bundling step.&lt;/p&gt;

&lt;p&gt;With the server finally running, I expected to see my 3D sphere bouncing with gravity. Instead, I got a blank canvas and the fatal error: &lt;strong&gt;&lt;code&gt;No Physics Engine available.&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The "Aha!" Moment: Vite Didn't Break My Physics
&lt;/h3&gt;

&lt;p&gt;Vite didn't break my physics. &lt;strong&gt;It exposed an architectural design I didn't know existed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To keep base payloads lightweight, highly optimized frameworks separate their pure implementation logic from their &lt;em&gt;runtime registration&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Because I only imported the core &lt;code&gt;Scene&lt;/code&gt; and &lt;code&gt;Mesh&lt;/code&gt; builders, I never directly referenced the physics module's exported API. My import graph didn't establish a need for that registration module to execute. Because its runtime registration wasn't reachable through the imports I had written, the production build could treat it as unnecessary.&lt;/p&gt;

&lt;p&gt;The physics code wasn't missing because it was broken. It was missing because its only purpose was to execute registration code, and I didn't tell my bundler that mattered!&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Fix: Side-Effect Imports
&lt;/h3&gt;

&lt;p&gt;To fix this, I had to learn the difference between asking a bundler for an &lt;em&gt;API&lt;/em&gt; versus asking it for an &lt;em&gt;action&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Normal Import:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Mesh&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@babylonjs/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Translation: "I need to use the Mesh API."&lt;/span&gt;
&lt;span class="c1"&gt;// Bundler trace: API is used, preserve the module.&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Side-Effect Import:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@babylonjs/core/Physics/physicsEngineComponent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Translation: "I don't need an exported API from this module, but I do need its module-level registration code to execute."&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;By adding that single side-effect import, the module executes immediately upon loading. It performs the registration necessary for Babylon's physics functionality to be available at runtime, while still allowing the bundler to aggressively tree-shake everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Result
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F85gfxz8zz52x5uml8tqu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F85gfxz8zz52x5uml8tqu.gif" alt="Marble in Canvas" width="799" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Big Lesson
&lt;/h3&gt;

&lt;p&gt;As I continue building out techaaroorian-games, this completely changed how I look at build tools.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static dependency&lt;/strong&gt; → Bundler can trace it → &lt;strong&gt;Keep what's reachable&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Module-level side effect&lt;/strong&gt; → Execution itself matters → &lt;strong&gt;Make that dependency explicit&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tree-shaking isn't magic. It's static analysis. And once I understood that, "unused code" became a much more interesting concept. The bundler isn't trying to understand what my game means; it is analyzing what my module graph tells it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(I am documenting my whole journey learning WebGPU and game dev. You can clone the full working engine setup we built today in the &lt;a href="https://github.com/techaaroorian-games/techaaroorian-games" rel="noopener noreferrer"&gt;techaaroorian-games GitHub repository&lt;/a&gt;!)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>vite</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>The DOM Is Lying to Your Types: 3 Subtle React + Zod Validation Traps</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Thu, 13 Aug 2026 06:38:47 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/the-dom-is-lying-to-your-types-3-subtle-react-zod-validation-traps-2a0p</link>
      <guid>https://dev.to/janarthanan_soundararajan/the-dom-is-lying-to-your-types-3-subtle-react-zod-validation-traps-2a0p</guid>
      <description>&lt;p&gt;A common production stack for React forms is &lt;strong&gt;React Hook Form&lt;/strong&gt; for client-side state, &lt;strong&gt;Zod&lt;/strong&gt; for schema validation, and &lt;strong&gt;Next.js Server Actions&lt;/strong&gt; for the backend mutation.&lt;/p&gt;

&lt;p&gt;It feels incredibly robust. But beneath these excellent tools, a subtle friction point is waiting to corrupt your data: &lt;strong&gt;Native form values are transport-oriented, not domain-oriented.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prefer a visual explanation? &lt;a href="https://youtu.be/eyE8JeGp2CI?si=s9s4yoNKM4BeuvC1" rel="noopener noreferrer"&gt;https://youtu.be/eyE8JeGp2CI?si=s9s4yoNKM4BeuvC1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even when the browser exposes typed helpers like &lt;code&gt;input.valueAsNumber&lt;/code&gt; or &lt;code&gt;input.checked&lt;/code&gt;, the raw values crossing a form boundary rarely match your application's domain model directly. React Hook Form can help normalize some primitives at registration (e.g., &lt;code&gt;{ valueAsNumber: true }&lt;/code&gt;), but this doesn't eliminate the need for boundary validation: &lt;code&gt;NaN&lt;/code&gt;, empty values, optional semantics, and unexpected server-side inputs still require explicit handling.&lt;/p&gt;

&lt;p&gt;When engineers rush to bridge the gap between transport data and strict TypeScript domain logic, it’s incredibly easy to implement "quick fixes" (like Zod's &lt;code&gt;z.coerce&lt;/code&gt;) that make the TypeScript compiler happy but introduce silent data corruption.&lt;/p&gt;

&lt;p&gt;To build robust systems, we need to internalize a core architectural principle:&lt;br&gt;
&lt;strong&gt;Validation ≠ Type Conversion ≠ Normalization.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Look at the modern form pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DOM
 ↓
React Hook Form / FormData (Transport Representation)
 ↓
Normalization (Where "30" becomes 30)
 ↓
Validation (Does 30 meet our business rules?)
 ↓
Domain Value (Strictly Typed)
 ↓
Persistence (Database)

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

&lt;/div&gt;



&lt;p&gt;Whether you pull data from React Hook Form state or native &lt;code&gt;FormData&lt;/code&gt;, your transport representation is limited. It contains strings and &lt;code&gt;File&lt;/code&gt; objects, while missing fields are represented by the absolute absence of an entry.&lt;/p&gt;

&lt;p&gt;As engineers, our golden rule at the normalization boundary should be: &lt;strong&gt;Don't ask "How do I make Zod accept this?" Ask "What does this external value actually mean in my domain?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here are three subtle data-boundary traps in the React/Zod ecosystem that happen when those responsibilities blur, and how to design reliable pipelines to prevent them.&lt;/p&gt;




&lt;h3&gt;
  
  
  Trap 1: The Number Coercion "Zero-Bypass" (Semantic Loss)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Scenario:&lt;/strong&gt; You are building a scheduling app and have an optional "Buffer Time" input (&lt;code&gt;&amp;lt;input type="number" name="buffer" /&amp;gt;&lt;/code&gt;). Because the DOM sends a string, &lt;code&gt;z.number().optional()&lt;/code&gt; fails. To fix it, you reach for Zod's coercion API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ The Trap&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bufferTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Actual Problem:&lt;/strong&gt; This isn't just a type error; it's semantic data loss.&lt;br&gt;
In JavaScript, &lt;code&gt;Number("")&lt;/code&gt; evaluates to &lt;code&gt;0&lt;/code&gt;. If a user leaves the input blank because they &lt;em&gt;don't want a buffer&lt;/em&gt;, Zod intercepts the empty string, coerces it to &lt;code&gt;0&lt;/code&gt;, and validates it. Because &lt;code&gt;0&lt;/code&gt; is a valid number, the &lt;code&gt;.optional()&lt;/code&gt; check is bypassed entirely.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User Intent&lt;/th&gt;
&lt;th&gt;DOM Input&lt;/th&gt;
&lt;th&gt;Naive Coercion&lt;/th&gt;
&lt;th&gt;Real Problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No buffer configured&lt;/td&gt;
&lt;td&gt;&lt;code&gt;""&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Semantic Loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit 0 minutes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"0"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You just erased the semantic distinction between "not provided" (&lt;code&gt;undefined&lt;/code&gt;) and "explicitly provided as zero minutes" (&lt;code&gt;0&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt;&lt;br&gt;
Define explicit normalization semantics using &lt;code&gt;z.preprocess()&lt;/code&gt; before Zod attempts to validate. Crucially, if the user submits garbage data (like &lt;code&gt;"banana"&lt;/code&gt;), we must not silently swallow it—we pass it through so &lt;code&gt;z.number()&lt;/code&gt; can properly reject it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Explicit Normalization&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;optionalNumberSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preprocess&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Preserve the semantic meaning of "empty"&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Safely attempt conversion&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Pass bad data through&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; 

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

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Note: Normalization answers "What value is this?" by intentionally accepting JavaScript's flexible numeric string syntax, like &lt;code&gt;"+30"&lt;/code&gt;. If your domain requires stricter constraints—for example, whole integers only—you enforce that as a **Validation&lt;/em&gt;* rule: &lt;code&gt;z.number().int().min(0)&lt;/code&gt;).*&lt;/p&gt;




&lt;h3&gt;
  
  
  Trap 2: The Boolean Checkbox Nightmare
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Scenario:&lt;/strong&gt; You have a checkbox for an event setting (&lt;code&gt;&amp;lt;input type="checkbox" name="isPrivate" /&amp;gt;&lt;/code&gt;). You grab the value from React Hook Form or &lt;code&gt;FormData&lt;/code&gt; and pass it to Zod.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ The Trap&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;isPrivate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Actual Problem:&lt;/strong&gt; Native HTML checkboxes do not submit &lt;code&gt;false&lt;/code&gt; when unchecked; the field is omitted entirely. Calling &lt;code&gt;formData.get("isPrivate")&lt;/code&gt; therefore returns &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Furthermore, if you are passing JSON payloads from a client UI, passing the string &lt;code&gt;"false"&lt;/code&gt; through &lt;code&gt;Boolean("false")&lt;/code&gt; actually evaluates to &lt;code&gt;true&lt;/code&gt; (because it's a non-empty string). Zod's &lt;code&gt;z.coerce.boolean()&lt;/code&gt; is blind to your domain intent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt;&lt;br&gt;
Don't use arbitrary form strings for boolean coercion. Normalize the transport representation explicitly, and—just like with numbers—don't silently swallow invalid data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Explicit Normalization&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkboxSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;isPrivate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preprocess&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Recognize explicit truthy transport values&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;on&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Recognize explicit falsy transport values&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Let unexpected values pass through so z.boolean() can catch and reject them!&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This guarantees that expected inputs map cleanly to &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;, but if a bad actor or a bug sends &lt;code&gt;"banana"&lt;/code&gt;, it reaches &lt;code&gt;z.boolean()&lt;/code&gt; and gets properly rejected. &lt;strong&gt;Unknown ≠ false.&lt;/strong&gt; Normalization should not silently destroy information.&lt;/p&gt;




&lt;h3&gt;
  
  
  Trap 3: The Timezone Date Parse (Ambiguous Instants)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Scenario:&lt;/strong&gt; A user selects a date and time for an event using &lt;code&gt;&amp;lt;input type="datetime-local" /&amp;gt;&lt;/code&gt;. It outputs a string like &lt;code&gt;"2026-08-15T09:00"&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ The Trap&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Actual Problem:&lt;/strong&gt; A JavaScript &lt;code&gt;Date&lt;/code&gt; object represents an &lt;em&gt;absolute instant&lt;/em&gt; in time (milliseconds since the epoch). But a user's selection of "August 15, 09:00" is a &lt;em&gt;local wall-clock time&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If a user in New York submits &lt;code&gt;"2026-08-15T09:00"&lt;/code&gt; and the server interprets the value using its own runtime timezone, the application has created an absolute instant without knowing which timezone the user intended. You've created an ambiguous point in time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Note: This is one of the exact problems the JavaScript &lt;code&gt;Temporal&lt;/code&gt; proposal is designed to model explicitly: separating a &lt;code&gt;PlainDateTime&lt;/code&gt; from a &lt;code&gt;ZonedDateTime&lt;/code&gt;.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt;&lt;br&gt;
Never parse localized date strings natively in your schema without explicitly capturing and applying the timezone context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ The Fix&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// Accept the raw wall-clock time string (e.g., "2026-08-15T09:00")&lt;/span&gt;
  &lt;span class="na"&gt;localTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{4}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;T&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// Accept the user's explicit timezone (e.g., "America/New_York")&lt;/span&gt;
  &lt;span class="na"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Explicitly combine the wall-clock time and timezone &lt;/span&gt;
  &lt;span class="c1"&gt;// into an absolute UTC instant before saving to the database&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;absoluteUtcInstant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;convertToUtcInstant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Takeaway
&lt;/h3&gt;

&lt;p&gt;Validation isn't just about rejecting invalid input. At application boundaries, it's also where we define how external, untyped representations officially become typed domain values.&lt;/p&gt;

&lt;p&gt;By explicitly separating normalization from validation—and paying attention to semantic zero-bypasses, truthy strings, and wall-clock ambiguity—you can build resilient systems that prevent data corruption before it ever reaches your database.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why Vite Asked Me to Choose Between Oxlint and ESLint</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Sat, 08 Aug 2026 10:39:44 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/why-vite-asked-me-to-choose-between-oxlint-and-eslint-3ihl</link>
      <guid>https://dev.to/janarthanan_soundararajan/why-vite-asked-me-to-choose-between-oxlint-and-eslint-3ihl</guid>
      <description>&lt;p&gt;While initializing a new Vite project (&lt;code&gt;npx create-vite&lt;/code&gt;), I hit an interactive CLI prompt that paused my standard workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;◆ Which linter to use?
  ● Oxlint
  ○ ESLint

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

&lt;/div&gt;



&lt;p&gt;Having relied on ESLint as my default linter for years, this piqued my curiosity. The filled bullet (&lt;code&gt;●&lt;/code&gt;) wasn't a formal endorsement from Vite—it was simply where the CLI cursor landed—but it made me pause. &lt;strong&gt;Why is Vite offering Oxlint alongside ESLint, and when does it make sense to use it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here are my first impressions after looking into what Oxlint is, how it works under the hood, and how I plan to test it out.&lt;/p&gt;




&lt;h2&gt;
  
  
  ❓ Why Is Vite Offering Oxlint?
&lt;/h2&gt;

&lt;p&gt;Vite doesn't recommend Oxlint over ESLint—it simply presents both as first-class options during project creation. Given the growing adoption of native tooling such as SWC, Biome, and Rolldown, adding Oxlint feels consistent with the broader direction of the frontend ecosystem, while still leaving the choice to developers.&lt;/p&gt;

&lt;p&gt;Rather than forcing one choice, Vite simply lets developers decide which workflow best fits their needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 What is Oxlint?
&lt;/h2&gt;

&lt;p&gt;From my preliminary research, &lt;strong&gt;Oxlint&lt;/strong&gt; is a linter developed as part of the &lt;strong&gt;Oxc&lt;/strong&gt; (Oxidized Compiler) project.&lt;/p&gt;

&lt;p&gt;The core difference seems to be the engine: while ESLint runs on Node.js/JavaScript, Oxlint is written in &lt;strong&gt;Rust&lt;/strong&gt;. Because Oxlint is a native executable rather than a JavaScript package running inside Node.js, it can reduce startup overhead and take advantage of parallel execution on modern CPUs.&lt;/p&gt;

&lt;p&gt;Rather than trying to replace every custom rule or plugin in the JavaScript ecosystem on day one, Oxlint's current focus appears to be catching &lt;strong&gt;common correctness bugs, React hook pitfalls, and type safety issues&lt;/strong&gt; with minimal setup overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚔️ How They Seem to Compare
&lt;/h2&gt;

&lt;p&gt;Here is a high-level snapshot based on the documentation and early community feedback I've gathered so far:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric / Feature&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;ESLint&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Oxlint&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime Engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Node.js / JavaScript&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Rust (Oxidized)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Varies by project size, plugins, and custom rules&lt;/td&gt;
&lt;td&gt;Official Oxlint benchmarks report significantly faster execution than ESLint, and early community feedback has generally reflected similar experiences, though results depend on the project.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup &amp;amp; Config&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires &lt;code&gt;eslint.config.js&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Designed around sensible zero-config defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Ecosystem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Very mature and extensive&lt;/td&gt;
&lt;td&gt;Smaller plugin ecosystem, with built-in support for many common React, TypeScript, JSX, and Jest rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary Value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deep customization &amp;amp; strict team policies&lt;/td&gt;
&lt;td&gt;Fast local feedback and low initial setup friction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  ⚡ Why the Speed Pitch Caught My Eye
&lt;/h2&gt;

&lt;p&gt;What made me want to investigate Oxlint further is the promise of shorter developer feedback loops.&lt;/p&gt;

&lt;p&gt;In past projects, running pre-commit hooks (&lt;code&gt;husky&lt;/code&gt; / &lt;code&gt;lint-staged&lt;/code&gt;) or saving files during local hot reloading (HMR) sometimes created a small delay while validating code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Typical local feedback loop:&lt;/span&gt;
&lt;span class="c"&gt;# 1. Save or stage a file.&lt;/span&gt;
&lt;span class="c"&gt;# 2. Run the linting process.&lt;/span&gt;
&lt;span class="c"&gt;# 3. Wait for rule validation before moving on.&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;If Rust-based linters can make that validation step feel nearly instant during active coding, that could be a subtle but nice improvement in daily flow. I'm curious to see if that holds true as my project grows from a fresh scaffold into a complete application.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ The Hybrid Idea I'm Curious to Try
&lt;/h2&gt;

&lt;p&gt;As I read through discussions on how developers are approaching this, one approach I came across repeatedly was &lt;strong&gt;a hybrid setup&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For local dev &amp;amp; pre-commit hooks:&lt;/strong&gt; Use &lt;strong&gt;Oxlint&lt;/strong&gt; as a lightweight frontline defender to catch common mistakes instantly without slowing down commits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For CI/CD pipelines:&lt;/strong&gt; Keep &lt;strong&gt;ESLint&lt;/strong&gt; running in GitHub Actions if complex, custom ESLint plugins or team-wide policies are required later on.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Conceptual&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hybrid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;setup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;I&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;may&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;experiment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Frontline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;HMR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pre-commit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hooks&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint:fast"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"oxlint --deny-warnings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Deep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;structural&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;audit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;CI/CD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pipelines&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint:ci"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eslint ."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  🧪 Next Steps &amp;amp; What I'm Testing Next
&lt;/h2&gt;

&lt;p&gt;I decided to select &lt;strong&gt;Oxlint&lt;/strong&gt; for this new Vite build as an initial experiment. Since this is a brand-new project, it felt like a good opportunity to evaluate a newer tool without worrying about migrating an existing lint configuration.&lt;/p&gt;

&lt;p&gt;Right now, the codebase is in its early stages, so getting started required virtually no configuration. As I build out components, integrate state management, and add visualizations (like D3 charts), I'm planning to observe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How well Oxlint catches real React/TypeScript errors during active development.&lt;/li&gt;
&lt;li&gt;Whether I run into any rule limitations that force me to bring ESLint back into the loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'll be keeping notes as the project develops and might share a follow-up post with actual hands-on impressions once the site is fully built!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; If my experience changes after building a larger React application, I'll publish a follow-up comparing Oxlint and ESLint with real-world usage rather than first impressions.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>oxlint</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Fixing Vitest CI Worker Crashes by Switching from jsdom to happy-dom</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Fri, 07 Aug 2026 10:43:45 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/fixing-vitest-ci-worker-crashes-by-switching-from-jsdom-to-happy-dom-1h95</link>
      <guid>https://dev.to/janarthanan_soundararajan/fixing-vitest-ci-worker-crashes-by-switching-from-jsdom-to-happy-dom-1h95</guid>
      <description>&lt;p&gt;Our Vitest suite passed every time locally, but GitHub Actions failed before executing a single test. Instead of failed assertions, every test worker crashed during startup, leaving us with 0% coverage and a broken CI pipeline.&lt;/p&gt;

&lt;p&gt;After narrowing the issue down to our test environment, we replaced &lt;code&gt;jsdom&lt;/code&gt; with &lt;code&gt;happy-dom&lt;/code&gt;. That change resolved the worker crashes in our environment and restored a stable GitHub Actions pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Environment Context
&lt;/h2&gt;

&lt;p&gt;To help determine if this applies to your setup, here is the stack where we encountered this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework:&lt;/strong&gt; Next.js (App Router)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing:&lt;/strong&gt; Vitest, &lt;code&gt;@testing-library/react&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package Manager:&lt;/strong&gt; &lt;code&gt;pnpm&lt;/code&gt; (Workspace / Monorepo)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI Environment:&lt;/strong&gt; GitHub Actions (&lt;code&gt;ubuntu-latest&lt;/code&gt;, Node.js 20)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. The Error: Unhandled Worker Failures in CI
&lt;/h2&gt;

&lt;p&gt;During the &lt;code&gt;pnpm --filter app test:coverage&lt;/code&gt; step in GitHub Actions, Vitest failed immediately upon initializing the worker pool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Vitest caught 7 unhandled errors during the test run.

⎯⎯⎯⎯⎯⎯ Unhandled Error ⎯⎯⎯⎯⎯⎯⎯
Error: [vitest-pool]: Failed to start forks worker for test files /apps/app/__tests__/poll.test.ts.

Caused by: TypeError: webidl.util.markAsUncloneable is not a function
 ❯ new CacheStorage ../node_modules/jsdom/node_modules/undici/lib/web/cache/cachestorage.js:20:17
 ❯ Object.&amp;lt;anonymous&amp;gt; ../node_modules/jsdom/node_modules/undici/index.js:179:25
 ❯ Object.&amp;lt;anonymous&amp;gt; ../node_modules/jsdom/lib/api.js:12:33

 Test Files  no tests
      Tests  no tests
     Errors  7 errors

ERROR: Coverage for lines (0%) does not meet global threshold (80%)

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

&lt;/div&gt;



&lt;p&gt;If you encounter errors such as &lt;code&gt;webidl.util.markAsUncloneable is not a function&lt;/code&gt; or &lt;code&gt;Failed to start forks worker&lt;/code&gt; in a &lt;code&gt;vitest-pool&lt;/code&gt; setup, the problem may lie in your test environment's dependency tree rather than in your test code.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Investigation
&lt;/h2&gt;

&lt;p&gt;At first, we assumed one of the test files was failing or throwing an uncaught exception.&lt;/p&gt;

&lt;p&gt;But the stack trace told a different story. None of the 7 test files had actually started executing. Every failure occurred while Vitest was initializing worker processes, which meant the problem was happening before our application code even loaded.&lt;/p&gt;

&lt;p&gt;The stack trace pointed directly into &lt;code&gt;jsdom&lt;/code&gt;'s internal &lt;code&gt;undici&lt;/code&gt; dependency during worker initialization (&lt;code&gt;jsdom/node_modules/undici/lib/web/cache/cachestorage.js&lt;/code&gt;). Rather than a bug in our application code, this suggested an environment-level compatibility issue involving the test environment rather than our application code.&lt;/p&gt;

&lt;p&gt;Beyond initial loading issues, &lt;code&gt;jsdom&lt;/code&gt; emulates a large portion of standard browser behavior and maintains a detailed DOM object model. In resource-constrained CI environments (like standard Linux runners in GitHub Actions), this extra memory footprint can increase resource usage and startup time, especially during coverage runs.&lt;/p&gt;

&lt;p&gt;We could have spent more time investigating the exact dependency mismatch, but since our tests only required standard DOM APIs, switching to a lighter implementation was the simpler and lower-risk solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Switch: Moving to &lt;code&gt;happy-dom&lt;/code&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;happy-dom&lt;/code&gt; Worked
&lt;/h3&gt;

&lt;p&gt;Unlike &lt;code&gt;jsdom&lt;/code&gt;, &lt;code&gt;happy-dom&lt;/code&gt; focuses specifically on implementing the browser APIs commonly needed by modern frontend unit tests (such as React synthetic events and DOM queries) rather than reproducing every full browser behavior. Because of its smaller scope, &lt;code&gt;happy-dom&lt;/code&gt; generally starts faster and has a lower resource footprint than &lt;code&gt;jsdom&lt;/code&gt; for many component-testing scenarios, making it a good fit for React component testing in Vitest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Replace Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; app remove jsdom
pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; app add &lt;span class="nt"&gt;-D&lt;/span&gt; happy-dom @vitest/coverage-v8

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Update Vitest Configuration
&lt;/h3&gt;

&lt;p&gt;Update &lt;code&gt;vitest.config.ts&lt;/code&gt; to use &lt;code&gt;happy-dom&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitejs/plugin-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;happy-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;setupFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./vitest.setup.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json-summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node_modules/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.next/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest.config.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest.setup.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*.d.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Recommended Test Setup
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;vitest.setup.ts&lt;/code&gt;, ensure component trees are explicitly unmounted after each test block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/jest-dom/vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cleanup&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;afterEach&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: This cleanup setup was not required to fix the worker initialization issue, but we keep it to ensure each test runs with a clean DOM and to avoid state leaking between tests.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before (&lt;code&gt;jsdom&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;After (&lt;code&gt;happy-dom&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Worker Initialization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Failed across all 7 test files&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;All workers started successfully&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code Coverage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 0% (Threshold failure)&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Coverage generated successfully&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pipeline Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ CI failed consistently&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;CI passed consistently&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Failed during worker initialization&lt;/td&gt;
&lt;td&gt;⚡ &lt;strong&gt;Entire suite finished in ~15 seconds&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;While we didn't identify the exact package combination that triggered the incompatibility, replacing &lt;code&gt;jsdom&lt;/code&gt; removed the issue entirely for our CI environment, making further investigation unnecessary for our use case.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. When Should You Keep &lt;code&gt;jsdom&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;Every tool comes with trade-offs, and &lt;code&gt;happy-dom&lt;/code&gt; isn't a 1:1 replacement for every project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep &lt;code&gt;jsdom&lt;/code&gt; if:&lt;/strong&gt; Your tests depend on advanced browser APIs that &lt;code&gt;happy-dom&lt;/code&gt; doesn't fully implement, or you are testing behavior that closely mirrors browser layout and parsing internals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;happy-dom&lt;/code&gt; if:&lt;/strong&gt; You are primarily running React, Vue, or Svelte component tests with &lt;code&gt;@testing-library&lt;/code&gt;, where rendering components and asserting on DOM output is all you need.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Lessons Learned &amp;amp; Takeaways
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaway:&lt;/strong&gt; Before changing test code, check whether the failure occurs before the first test executes. If worker initialization fails, the root cause is often the test environment, dependency graph, or runtime—not your application logic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The lesson wasn't that &lt;code&gt;happy-dom&lt;/code&gt; is universally better than &lt;code&gt;jsdom&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It was that your test environment should match your testing needs. For our React component tests, a lightweight DOM implementation was enough—and choosing the simplest test environment that satisfied our requirements made the CI pipeline both faster and more reliable.&lt;/p&gt;

&lt;p&gt;Sometimes the quickest path to a stable CI pipeline isn't finding the perfect root cause—it's choosing the simplest tool that satisfies your testing requirements.&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>cicd</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
