<?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: mohammad rostami</title>
    <description>The latest articles on DEV Community by mohammad rostami (@mohammad_rostami_).</description>
    <link>https://dev.to/mohammad_rostami_</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%2F3628004%2F2099a6d6-913c-4088-ba3c-9b587f8b8fd4.jpg</url>
      <title>DEV Community: mohammad rostami</title>
      <link>https://dev.to/mohammad_rostami_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohammad_rostami_"/>
    <language>en</language>
    <item>
      <title>Reliable Form Submissions: Dirty State, Fingerprints, and Idempotency</title>
      <dc:creator>mohammad rostami</dc:creator>
      <pubDate>Fri, 21 Aug 2026 07:07:30 +0000</pubDate>
      <link>https://dev.to/mohammad_rostami_/meaningful-dirty-state-fingerprints-and-idempotency-three-details-that-make-form-submissions-36e0</link>
      <guid>https://dev.to/mohammad_rostami_/meaningful-dirty-state-fingerprints-and-idempotency-three-details-that-make-form-submissions-36e0</guid>
      <description>&lt;h2&gt;
  
  
  A disabled Submit button protects the UI. It does not protect the data.
&lt;/h2&gt;

&lt;p&gt;It is tempting to treat this as enough protection against duplicate submissions:&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="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="p"&gt;&amp;gt;&lt;/span&gt;
  Submit
&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;The button is worth disabling. It gives the user immediate feedback and prevents an obvious second click. But it only controls one interaction in one browser tab.&lt;/p&gt;

&lt;p&gt;It cannot tell us whether a request was retried by the client, whether two requests crossed the network at nearly the same time, or whether the server completed a write before the response disappeared.&lt;/p&gt;

&lt;p&gt;Consider a small order form:&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;type&lt;/span&gt; &lt;span class="nx"&gt;OrderFormValues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;firstName&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;lastName&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;amount&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;price&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A straightforward submission might look like this:&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&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;values&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 imagine that the server creates the order, but the response never reaches the browser. The UI sees a network error. The user tries again. Without any other protection, the second request can create a second order.&lt;/p&gt;

&lt;p&gt;The difficult part is not the second click. The difficult part is deciding whether the second request represents a new operation or a retry of the first one.&lt;/p&gt;

&lt;p&gt;The mental model I use separates two identities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;fingerprint&lt;/strong&gt; identifies the content of the request.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;idempotency key&lt;/strong&gt; identifies the operation the user intended to perform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They solve different problems, and neither is a replacement for the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content identity starts with normalization
&lt;/h2&gt;

&lt;p&gt;Two form values can be different in JavaScript while representing the same business input.&lt;/p&gt;

&lt;p&gt;These names are not byte-for-byte equal:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sara&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; Sara &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if the application trims names before saving them, that whitespace is not a meaningful change. The same issue appears with values such as &lt;code&gt;"150"&lt;/code&gt; and &lt;code&gt;150&lt;/code&gt;, or &lt;code&gt;150&lt;/code&gt; and &lt;code&gt;150.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I prefer to make those rules explicit before creating a fingerprint:&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;normalizeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderFormValues&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;firstName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;values&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="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;values&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="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&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;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;priceInCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&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;values&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="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&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;canonicalizeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderFormValues&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="k"&gt;return&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="nf"&gt;normalizeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&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;Converting the price to cents is part of the example's business normalization. It avoids using small floating-point differences as evidence that two prices are different.&lt;/p&gt;

&lt;p&gt;For local comparisons, the canonical string is already a useful fingerprint:&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;fingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonicalizeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For storage or comparison across system boundaries, I would usually hash that canonical value:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createOrderFingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderFormValues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonicalizeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&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;bytes&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;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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;digest&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;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&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;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bytes&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;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&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;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;byte&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;byte&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="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The normalization rules must come from the domain. If array order has no business meaning, sort the array before hashing it. If order is meaningful, sorting it would create the wrong identity. A fingerprint is only as correct as the canonical representation behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dirty state is also a domain question
&lt;/h2&gt;

&lt;p&gt;Form libraries usually expose an &lt;code&gt;isDirty&lt;/code&gt; flag. That flag is useful, but it normally compares raw form values with the original defaults. It does not know which differences matter to the business.&lt;/p&gt;

&lt;p&gt;A business-aware check can compare the initial and current fingerprints instead:&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;initialFingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonicalizeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialValues&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;currentFingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonicalizeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentValues&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;isMeaningfullyDirty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;currentFingerprint&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;initialFingerprint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the user changes &lt;code&gt;"Sara"&lt;/code&gt; to &lt;code&gt;" Sara "&lt;/code&gt;, the raw form may be dirty while &lt;code&gt;isMeaningfullyDirty&lt;/code&gt; remains &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the user changes &lt;code&gt;amount&lt;/code&gt; from &lt;code&gt;2&lt;/code&gt; to &lt;code&gt;3&lt;/code&gt;, the fingerprint changes. If they change it back to &lt;code&gt;2&lt;/code&gt;, the fingerprints match again and the form is no longer meaningfully dirty.&lt;/p&gt;

&lt;p&gt;This is also why UI-only state should stay out of the fingerprint. A search query, an expanded panel, or the selected tab may change the component state without changing the order that will be sent to the server.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;customerSearch&lt;/code&gt; does not belong in the normalized payload:&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;type&lt;/span&gt; &lt;span class="nx"&gt;OrderPageState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;OrderFormValues&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;customerSearch&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeOrderPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderPageState&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;normalizeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&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;Using the same canonicalization rules for Dirty State and submission gives both features the same definition of a meaningful change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operation identity belongs to the idempotency key
&lt;/h2&gt;

&lt;p&gt;A fingerprint tells us that two payloads contain the same business data. It does not tell us whether the user intended one operation or two.&lt;/p&gt;

&lt;p&gt;A user may intentionally create two identical orders. Those orders have the same fingerprint, but they are still separate operations. This is why I would not derive the idempotency key from the fingerprint:&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;// Incorrect: identical payloads can still be separate operations.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fingerprint&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 client creates a random key for a new operation and reuses it only when retrying that operation with the same fingerprint.&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;type&lt;/span&gt; &lt;span class="nx"&gt;PendingOperation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;fingerprint&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;idempotencyKey&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pendingOperation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PendingOperation&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderFormValues&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;fingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonicalizeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&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;operation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;pendingOperation&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;fingerprint&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;fingerprint&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;pendingOperation&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;pendingOperation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;operation&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;response&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;Content-Type&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;application/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="s2"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&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;values&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;Order submission failed&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;result&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// A later submission now represents a new operation,&lt;/span&gt;
  &lt;span class="c1"&gt;// even if the user enters the same values again.&lt;/span&gt;
  &lt;span class="nx"&gt;pendingOperation&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="nx"&gt;result&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;There are two deliberate choices in this code.&lt;/p&gt;

&lt;p&gt;First, a failed attempt does not immediately discard the key. Retrying the same payload should reuse the identity of the pending operation.&lt;/p&gt;

&lt;p&gt;Second, a successful response clears that identity. If the user intentionally submits the same values again later, the client generates a new key and the server is allowed to create another order.&lt;/p&gt;

&lt;p&gt;Disabling the button still makes sense, but the key is what allows the server to recognize a repeated operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server has to bind the key to the payload
&lt;/h2&gt;

&lt;p&gt;The client can help manage operation identity, but the guarantee must exist on the server.&lt;/p&gt;

&lt;p&gt;For each idempotency key, the server needs to store at least:&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;type&lt;/span&gt; &lt;span class="nx"&gt;IdempotencyRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&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="nl"&gt;fingerprint&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;succeeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;response&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;orderId&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server should calculate the fingerprint itself. Trusting a fingerprint supplied by the browser would allow a modified client to claim that two different payloads are identical.&lt;/p&gt;

&lt;p&gt;The key also has to be claimed atomically. A separate &lt;code&gt;find()&lt;/code&gt; followed by &lt;code&gt;insert()&lt;/code&gt; leaves a race in which two requests can both observe that the key is missing.&lt;/p&gt;

&lt;p&gt;The storage API can expose that atomic decision directly:&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;type&lt;/span&gt; &lt;span class="nx"&gt;ClaimResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mismatch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&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;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;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency key is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&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;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;OrderFormValues&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;fingerprint&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;createOrderFingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&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;claim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClaimResult&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;idempotencyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;claim&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;fingerprint&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;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mismatch&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Key reused with a different payload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;422&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;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&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;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processing&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Operation is already processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;202&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;order&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;orderRepository&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="nf"&gt;normalizeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;idempotencyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&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;response&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;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;201&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;In a relational database, a unique constraint on the key is the minimum protection against concurrent claims:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idempotency_key_unique&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;idempotency_records&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact persistence model depends on the operation, but the invariant should remain the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same key and the same fingerprint refer to the same operation.&lt;/li&gt;
&lt;li&gt;The same key with a different fingerprint is rejected.&lt;/li&gt;
&lt;li&gt;A new key represents a new operation, even when its fingerprint matches an older one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The mistakes I would avoid
&lt;/h2&gt;

&lt;p&gt;The first mistake is treating &lt;code&gt;isSubmitting&lt;/code&gt; as a data-integrity mechanism. It is UI state, and it cannot protect the server from concurrent or repeated requests.&lt;/p&gt;

&lt;p&gt;The second is using a fingerprint as the idempotency key. That incorrectly merges separate operations that happen to have identical payloads.&lt;/p&gt;

&lt;p&gt;The third is hashing raw form state. Whitespace, numeric representation, property order, or UI-only fields can create different hashes for the same business input. Canonicalization has to happen before hashing.&lt;/p&gt;

&lt;p&gt;The final mistake is implementing the guarantee only in the browser. The browser can choose and reuse a key, but only durable, atomic server-side storage can enforce it.&lt;/p&gt;

&lt;p&gt;The compact version of the model is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dirty State compares the current content with the initial content.&lt;br&gt;&lt;br&gt;
A fingerprint identifies that content.&lt;br&gt;&lt;br&gt;
An idempotency key identifies the operation performed with it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once those responsibilities are separate, the Submit button can remain what it should be: a useful interaction detail, not the last line of defense against duplicate writes.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>typescript</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why Your Zod Validation Fails Before It Even Runs (And How to Fix It)</title>
      <dc:creator>mohammad rostami</dc:creator>
      <pubDate>Thu, 11 Jun 2026 08:43:43 +0000</pubDate>
      <link>https://dev.to/mohammad_rostami_/why-your-zod-validation-fails-before-it-even-runs-and-how-to-fix-it-51d3</link>
      <guid>https://dev.to/mohammad_rostami_/why-your-zod-validation-fails-before-it-even-runs-and-how-to-fix-it-51d3</guid>
      <description>&lt;p&gt;If you're using Zod with react-hook-form, you've probably seen this at least once:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Invalid input: expected number, received NaN&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;At first glance, it looks like a simple validation issue.&lt;br&gt;
It’s not.&lt;/p&gt;
&lt;h3&gt;
  
  
  The real problem
&lt;/h3&gt;

&lt;p&gt;When working with form inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All values come in as &lt;strong&gt;strings&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;z.coerce.number()&lt;/code&gt; tries to convert them&lt;/li&gt;
&lt;li&gt;Empty input (&lt;code&gt;""&lt;/code&gt;) or invalid values → &lt;code&gt;NaN&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here’s the catch:&lt;br&gt;
Zod fails &lt;strong&gt;before&lt;/strong&gt; your &lt;code&gt;.min()&lt;/code&gt; / &lt;code&gt;.max()&lt;/code&gt; validations run.&lt;/p&gt;

&lt;p&gt;So instead of your custom message, you get a generic (and not very helpful) error.&lt;/p&gt;
&lt;h3&gt;
  
  
  It gets trickier
&lt;/h3&gt;

&lt;p&gt;If you're using TypeScript:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;z.input&amp;lt;typeof schema&amp;gt;&lt;/code&gt; ≠ &lt;code&gt;z.output&amp;lt;typeof schema&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;react-hook-form&lt;/code&gt; works with the &lt;strong&gt;input type&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Zod gives you the &lt;strong&gt;output type after parsing&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mismatch can lead to confusing type errors and wrong assumptions about your data.&lt;/p&gt;
&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;You need to handle invalid input &lt;em&gt;before&lt;/em&gt; Zod tries to validate 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="nx"&gt;readTime&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="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;undefined&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;num&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="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;num&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="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;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Minimum read time is 2 minutes&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Maximum read time is 60 minutes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  What this solves
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Empty input → handled properly&lt;/li&gt;
&lt;li&gt;Invalid numbers → no more NaN issues&lt;/li&gt;
&lt;li&gt;Custom validation messages → actually shown&lt;/li&gt;
&lt;li&gt;Cleaner UX overall&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Takeaway
&lt;/h3&gt;

&lt;p&gt;If you're using &lt;code&gt;z.coerce.number()&lt;/code&gt; in forms, don't rely on it blindly.&lt;br&gt;
Always normalize your input first.&lt;/p&gt;

&lt;p&gt;Because sometimes the bug isn't in your validation rules…&lt;br&gt;
it's in how your data &lt;em&gt;arrives&lt;/em&gt; before validation even starts.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>zod</category>
      <category>nan</category>
      <category>react</category>
    </item>
    <item>
      <title>Technical Deep Dive: How React Server Components Work and Where the Vulnerabilities Appear</title>
      <dc:creator>mohammad rostami</dc:creator>
      <pubDate>Mon, 15 Dec 2025 20:42:52 +0000</pubDate>
      <link>https://dev.to/mohammad_rostami_/technical-deep-dive-how-react-server-components-work-and-where-the-vulnerabilities-appear-po6</link>
      <guid>https://dev.to/mohammad_rostami_/technical-deep-dive-how-react-server-components-work-and-where-the-vulnerabilities-appear-po6</guid>
      <description>&lt;p&gt;Hi everyone&lt;br&gt;
In a recent, I examined CVE-2025-55182, a critical vulnerability in React Server Components caused by an unsafe deserialization path that could lead to unauthenticated remote code execution. That issue highlighted how deeply framework internals can influence the security posture of applications built on top of them.&lt;/p&gt;

&lt;p&gt;Shortly after that disclosure, an additional set of security vulnerabilities was published affecting the same architectural layer in React Server Components (RSC) and Next.js. While these issues do not result in immediate remote code execution, they introduce significant risks, including Denial of Service (DoS) and unintended exposure of server-side source code and internal implementation details under specific conditions.&lt;/p&gt;

&lt;p&gt;This article builds on that context and focuses on the underlying execution model of React Server Components, explaining how these issues emerge at the framework level and why they matter for production deployments.&lt;/p&gt;

&lt;p&gt;To understand why the recently disclosed issues in React Server Components (RSC) lead to Denial of Service and source code exposure, it helps to look at how the RSC execution pipeline actually works on the server.&lt;/p&gt;
&lt;h2&gt;
  
  
  React Server Components: Server-Side Execution Flow
&lt;/h2&gt;

&lt;p&gt;A simplified but accurate RSC request lifecycle looks like this:&lt;/p&gt;
&lt;h2&gt;
  
  
  1 - Incoming request reaches the RSC endpoint
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;In Next.js, this typically maps to an internal route such as /_rsc&lt;/li&gt;
&lt;li&gt;The request is processed before authentication and before application middleware&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  2 - Request body parsing and protocol decoding
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The payload is parsed according to the React Flight protocol&lt;/li&gt;
&lt;li&gt;This includes decoding serialized component references, props, and execution hints&lt;/li&gt;
&lt;li&gt;At this stage, React assumes the payload shape is valid enough to be processed&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  3 - Component graph reconstruction
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;React reconstructs the Server Component tree from the serialized input&lt;/li&gt;
&lt;li&gt;Module references are resolved and loaded&lt;/li&gt;
&lt;li&gt;Async boundaries and Suspense segments are registered&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  4 - Server rendering and streaming
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Components are executed on the server&lt;/li&gt;
&lt;li&gt;Output is serialized again and streamed back to the client in chunks&lt;/li&gt;
&lt;li&gt;Error boundaries and partial results are flushed incrementally&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  5 - Error and recovery
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If an exception occurs mid-stream, React attempts to recover gracefully&lt;/li&gt;
&lt;li&gt;Metadata, stack traces, and module identifiers are still available internally&lt;/li&gt;
&lt;li&gt;Each of these phases is sensitive to malformed or adversarial input.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How Denial of Service Is Triggered
&lt;/h2&gt;

&lt;p&gt;The DoS issue arises primarily in steps 2 and 3, before any application logic is involved.&lt;br&gt;
A malicious request can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inflate the serialized payload size&lt;/li&gt;
&lt;li&gt;Introduce deeply nested or cyclic references&lt;/li&gt;
&lt;li&gt;Force excessive async boundary resolution&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /_rsc
Content-Type: application/json

{
  "type": "ServerComponent",
  "props": {
    "children": {
      "children": {
        "children": {
          ...
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if the payload is syntactically valid, React will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Attempt to deserialize it&lt;/li&gt;
&lt;li&gt;Build an in-memory component graph&lt;/li&gt;
&lt;li&gt;Schedule execution and streaming work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High CPU usage during decoding&lt;/li&gt;
&lt;li&gt;Memory pressure during graph reconstruction&lt;/li&gt;
&lt;li&gt;Worker or event-loop saturation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Crucially, this happens without hitting user-defined code and without authentication.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  This makes traditional defensive assumptions at the application layer largely ineffective.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  How Source Code Exposure Can Occur
&lt;/h2&gt;

&lt;p&gt;Source code exposure is a side effect of how React handles errors during steps 4 and 5.&lt;br&gt;
When a rendering or serialization error occurs mid-stream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React still holds references to internal module paths&lt;/li&gt;
&lt;li&gt;Stack traces include resolved file locations&lt;/li&gt;
&lt;li&gt;Component metadata is partially serialized&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In certain edge cases, this information may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Leak into the streamed response&lt;/li&gt;
&lt;li&gt;Appear in error payloads&lt;/li&gt;
&lt;li&gt;Be flushed before the stream is properly aborted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example of leaked information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Failed to resolve Server Component
  at /app/src/components/admin/UserList.server.tsx
  at react-server-dom-webpack/server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reveals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project directory structure&lt;/li&gt;
&lt;li&gt;Server-only component boundaries&lt;/li&gt;
&lt;li&gt;Internal module layout and naming&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  While not a standalone exploit, this materially reduces the effort required for targeted attacks.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Why Server Actions Are Not Required
&lt;/h2&gt;

&lt;p&gt;A common misconception is that these risks only apply when using Server Actions.&lt;br&gt;
In reality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server Actions are an API surface&lt;/li&gt;
&lt;li&gt;RSC is a runtime and protocol&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even a simple page like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default async function Page() {
  return &amp;lt;Dashboard /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still triggers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request deserialization&lt;/li&gt;
&lt;li&gt;Component graph reconstruction&lt;/li&gt;
&lt;li&gt;Server execution and streaming&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The vulnerable logic executes regardless of whether Server Actions are defined.
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;React Server Components introduce a fundamentally new server-side execution model that combines request deserialization, component graph reconstruction, and streaming-based rendering into a single framework-managed pipeline.&lt;/li&gt;
&lt;li&gt;As a result, the effective attack surface exists before authentication, before application middleware, and before any user-defined business logic is executed.&lt;/li&gt;
&lt;li&gt;Malformed or adversarial RSC requests can lead to:&lt;/li&gt;
&lt;li&gt;Denial of Service, through excessive CPU usage, memory pressure, and worker or event-loop saturation during deserialization and component graph reconstruction&lt;/li&gt;
&lt;li&gt;Accidental disclosure of server-side implementation details, including internal file paths, component boundaries, and module layout, as a side effect of streaming error handling&lt;/li&gt;
&lt;li&gt;These issues are not caused by misconfigured applications or unsafe user code. They are framework-level vulnerabilities in the React Server Components runtime itself and cannot be mitigated reliably through application-layer defenses alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Official advisories and patches&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components" rel="noopener noreferrer"&gt;Denial of Service and Source Code Exposure in React Server Components&lt;/a&gt;
&lt;/h3&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://nextjs.org/blog/security-update-2025-12-11" rel="noopener noreferrer"&gt;Next.js security update&lt;/a&gt;
&lt;/h3&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>security</category>
      <category>servercomponent</category>
    </item>
    <item>
      <title>A Deep Dive into Nested ScrollView Behavior in React Native: Root Causes and Practical Solutions</title>
      <dc:creator>mohammad rostami</dc:creator>
      <pubDate>Mon, 24 Nov 2025 21:24:08 +0000</pubDate>
      <link>https://dev.to/mohammad_rostami_/a-deep-dive-into-nested-scrollview-behavior-in-react-native-root-causes-and-practical-solutions-bmk</link>
      <guid>https://dev.to/mohammad_rostami_/a-deep-dive-into-nested-scrollview-behavior-in-react-native-root-causes-and-practical-solutions-bmk</guid>
      <description>&lt;h2&gt;
  
  
  Hi everyone
&lt;/h2&gt;

&lt;p&gt;During the development of a recent screen in one of our applications, I faced a scenario where a section of the content needed to scroll independently, while the entire page itself was wrapped inside a parent ScrollView. At first glance, this seems like a straightforward layout. In practice, however, the behavior of nested scroll containers became unexpectedly challenging, especially when tested across platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observed Behavior Across Platforms
&lt;/h2&gt;

&lt;p&gt;On iOS, the internal ScrollView generally behaves as expected without requiring additional configuration.&lt;/p&gt;

&lt;p&gt;On Android, the situation is different. The inner scroll often fails to activate, gets interrupted by the outer scroll, or reacts inconsistently. This occurred not only on lower-end devices but on high-end models as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The issue becomes even more noticeable when:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The two scroll layers have different directions (for example, vertical parent and horizontal child)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The inner content has dynamic height or width&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multiple simultaneous gestures are triggered during rapid scrolling&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It quickly became clear that this was not simply a styling conflict, but a deeper issue related to gesture priority and event handling at the platform level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause Analysis: What Actually Happens Behind the Scenes
&lt;/h2&gt;

&lt;p&gt;After reviewing React Native’s gesture responder system and native scroll behavior in both iOS and Android, several key points emerged:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;On Android, the parent ScrollView receives gesture priority by default &lt;br&gt;
It attempts to determine whether the gesture belongs to itself before allowing the child ScrollView to respond. This decision is not always accurate, which often prevents the inner scroll from initiating.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On iOS, gesture separation is more precise&lt;br&gt;
The system determines the appropriate scroll target early and allows both parent and child scroll views to operate smoothly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mixed-direction scrolling increases complexity&lt;br&gt;
When the parent scrolls vertically and the child scrolls horizontally, the system must re-evaluate gesture direction multiple times. On Android, this frequently results in jumps, unexpected direction changes, or the parent taking control prematurely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dynamic layout measurement affects gesture routing&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Incorrect or late size calculations can cause the inner ScrollView to lose gesture ownership too early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Solutions Based on Testing
&lt;/h2&gt;

&lt;p&gt;Several solutions helped achieve consistent, reliable behavior for nested scrolling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable nestedScrollEnabled on the inner ScrollView (Android)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;ScrollView&amp;gt;

     &amp;lt;ScrollView nestedScrollEnabled&amp;gt;

        {/* content */}

     &amp;lt;/ScrollView&amp;gt;

&amp;lt;/ScrollView&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the foundational setting required to ensure the child scroll can properly receive gestures.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use flexGrow for proper content measurement
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contentContainerStyle={{ flexGrow: 1 }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures the inner ScrollView’s content is measured correctly, reducing gesture conflicts with the parent view.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handle mixed-direction scrolling intentionally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the parent and child scroll in different directions, it is often beneficial to temporarily enable or disable one of the scroll containers based on the detected gesture direction.&lt;/p&gt;

&lt;p&gt;This approach helps the system correctly determine which layer should receive the gesture and prevents unintended handoffs during scroll.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// https://www.linkedin.com/in/mohammadrostami/
import React from 'react';
import { ScrollView, View, Text, StyleSheet } from 'react-native';

export default function NestedScrollExample() {
  return (
    &amp;lt;ScrollView style={styles.parentScroll}&amp;gt;
      &amp;lt;View style={styles.container}&amp;gt;

        &amp;lt;Text style={styles.title}&amp;gt;Parent ScrollView&amp;lt;/Text&amp;gt;

        &amp;lt;ScrollView
          nestedScrollEnabled
          style={styles.childScroll}
          contentContainerStyle={{ flexGrow: 1 }}
        &amp;gt;
          &amp;lt;View style={styles.childContent}&amp;gt;
            {Array.from({ length: 25 }).map((_, i) =&amp;gt; (
              &amp;lt;Text style={styles.item} key={i}&amp;gt;
                Child Item {i + 1}
              &amp;lt;/Text&amp;gt;
            ))}
          &amp;lt;/View&amp;gt;
        &amp;lt;/ScrollView&amp;gt;

        &amp;lt;View style={styles.footer}&amp;gt;
          &amp;lt;Text style={styles.footerText}&amp;gt;Footer Content&amp;lt;/Text&amp;gt;
        &amp;lt;/View&amp;gt;

      &amp;lt;/View&amp;gt;
    &amp;lt;/ScrollView&amp;gt;
  );
}

const styles = StyleSheet.create({
  parentScroll: {
    flex: 1,
    backgroundColor: '#fafafa',
  },
  container: {
    padding: 16,
  },
  title: {
    fontSize: 18,
    marginBottom: 12,
  },
  childScroll: {
    height: 250,
    borderRadius: 8,
    borderWidth: 1,
    borderColor: '#ccc',
  },
  childContent: {
    padding: 12,
  },
  item: {
    paddingVertical: 10,
    fontSize: 16,
  },
  footer: {
    marginTop: 30,
    padding: 12,
    backgroundColor: '#eee',
    borderRadius: 8,
  },
  footerText: {
    fontSize: 16,
  },
});

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Nested ScrollViews introduce a set of platform-specific challenges in React Native, particularly in Android where gesture routing differs significantly from iOS. Understanding how gestures are prioritized, how mixed-direction scrolling is handled, and how layout measurement affects scroll behavior is essential for creating smooth, predictable user experiences.&lt;br&gt;
Properly configuring nestedScrollEnabled, managing dynamic content sizing, and intentionally controlling scroll interactions are key to resolving these issues in production-grade applications.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ui</category>
      <category>ios</category>
      <category>reactnative</category>
    </item>
  </channel>
</rss>
