<?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: Navya Sinha</title>
    <description>The latest articles on DEV Community by Navya Sinha (@navya_sinha_f15ddf3e8e056).</description>
    <link>https://dev.to/navya_sinha_f15ddf3e8e056</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%2F1575161%2F633e8ff2-7fe8-4eb0-84b0-23c08fb2d674.png</url>
      <title>DEV Community: Navya Sinha</title>
      <link>https://dev.to/navya_sinha_f15ddf3e8e056</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/navya_sinha_f15ddf3e8e056"/>
    <language>en</language>
    <item>
      <title>Why React lets your number input lie to you</title>
      <dc:creator>Navya Sinha</dc:creator>
      <pubDate>Tue, 06 Oct 2026 20:04:06 +0000</pubDate>
      <link>https://dev.to/navya_sinha_f15ddf3e8e056/why-react-lets-your-number-input-lie-to-you-199c</link>
      <guid>https://dev.to/navya_sinha_f15ddf3e8e056/why-react-lets-your-number-input-lie-to-you-199c</guid>
      <description>&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="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="nx"&gt;setAmount&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;handleAmountChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ChangeEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="nf"&gt;setAmount&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;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;val&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;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;"number"&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;amount&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&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="nx"&gt;handleAmountChange&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;If we type &lt;code&gt;0&lt;/code&gt;, then &lt;code&gt;5&lt;/code&gt;. The field shows &lt;strong&gt;&lt;code&gt;05&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;State said &lt;code&gt;5&lt;/code&gt;. The payload we'd send said &lt;code&gt;5&lt;/code&gt;. The screen said &lt;code&gt;05&lt;/code&gt;. That's a controlled&lt;br&gt;
component, which is supposed to mean "the DOM always shows state", and yet here it was&lt;br&gt;
showing something else.&lt;/p&gt;

&lt;p&gt;It isn't a bug in React, and it isn't a bug in our code either. It's a deliberate&lt;br&gt;
trade-off, and to understand it you need to look at two layers: the &lt;strong&gt;browser&lt;/strong&gt; and&lt;br&gt;
&lt;strong&gt;React DOM&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layer 1: what the browser thinks "05" is
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;&amp;lt;input type="number"&amp;gt;&lt;/code&gt; keeps a &lt;em&gt;string&lt;/em&gt; value, like every input. The HTML spec defines&lt;br&gt;
a &lt;strong&gt;value sanitization algorithm&lt;/strong&gt; for number inputs: if the value isn't a &lt;em&gt;valid&lt;br&gt;
floating-point number&lt;/em&gt;, the browser sets it to the empty string.&lt;/p&gt;

&lt;p&gt;Is &lt;code&gt;"05"&lt;/code&gt; a valid floating-point number? The spec grammar is roughly: an optional &lt;code&gt;-&lt;/code&gt;,&lt;br&gt;
then digits, then optionally &lt;code&gt;.&lt;/code&gt; and more digits, then optionally an exponent. Nothing&lt;br&gt;
there says "no leading zeros". So &lt;code&gt;"05"&lt;/code&gt; is perfectly valid, and the browser leaves it&lt;br&gt;
alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;          &lt;span class="c1"&gt;// "05"   ← the string, as typed&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;valueAsNumber&lt;/span&gt;  &lt;span class="c1"&gt;// 5      ← the parsed number&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What if the user types something that &lt;em&gt;isn't&lt;/em&gt; valid yet, like &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;1e&lt;/code&gt;? The browser&lt;br&gt;
can't sanitize a half-typed string, so it reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;            &lt;span class="c1"&gt;// ""     ← yes, empty, even though you can see "-" on screen&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;validity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;badInput&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember that one. It comes back later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: what React does on every render
&lt;/h2&gt;

&lt;p&gt;A "controlled" input works like this: after each render, React DOM compares the &lt;code&gt;value&lt;/code&gt;&lt;br&gt;
prop with the DOM node's current value, and writes the prop to the DOM if they differ.&lt;br&gt;
For most inputs the comparison is a strict string comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For &lt;code&gt;type="number"&lt;/code&gt; it's different.&lt;/strong&gt; Here's the relevant code from React DOM&lt;br&gt;
(&lt;code&gt;ReactDOMInput.js&lt;/code&gt;, React 18.2; React 19 has the same check):&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&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;if &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="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;value&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="o"&gt;||&lt;/span&gt;
    &lt;span class="c1"&gt;// We explicitly want to coerce to number here if possible.&lt;/span&gt;
    &lt;span class="c1"&gt;// eslint-disable-next-line&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;value&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toString&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;node&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="nf"&gt;toString&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;node&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="nf"&gt;toString&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;!=&lt;/code&gt; is &lt;strong&gt;loose equality&lt;/strong&gt;, used on purpose. Our case works out like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;node&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;05&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// what the user typed&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;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// our state&lt;/span&gt;

&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;05&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;   &lt;span class="c1"&gt;// → false, because "05" is coerced to 5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React decides the DOM already shows the right thing and &lt;strong&gt;doesn't write to it&lt;/strong&gt;. The&lt;br&gt;
input keeps showing &lt;code&gt;05&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why would React do that on purpose?
&lt;/h2&gt;

&lt;p&gt;Imagine React used strict comparison instead. The user wants to type &lt;code&gt;1.5&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Keystroke&lt;/th&gt;
&lt;th&gt;&lt;code&gt;node.value&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;parseFloat&lt;/code&gt; → state&lt;/th&gt;
&lt;th&gt;React writes&lt;/th&gt;
&lt;th&gt;User sees&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"1"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"1."&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"1"&lt;/code&gt; 💥&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"15"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;15&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;15&lt;/code&gt; 😱&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With strict comparison, every normal parse-on-change handler would make it &lt;strong&gt;impossible&lt;br&gt;
to type a decimal point&lt;/strong&gt;, and the same goes for &lt;code&gt;1.0&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;1e3&lt;/code&gt;. The number is fine,&lt;br&gt;
but the text representation gets rewritten underneath the user's cursor.&lt;/p&gt;

&lt;p&gt;Loose equality says: &lt;em&gt;"if the DOM's text already means the same number as the prop,&lt;br&gt;
leave the user's text alone."&lt;/em&gt; That keeps typing smooth for &lt;code&gt;1.&lt;/code&gt;, &lt;code&gt;1.50&lt;/code&gt; and &lt;code&gt;-0&lt;/code&gt;. The&lt;br&gt;
side effect is that &lt;code&gt;05&lt;/code&gt; also counts as "the same number as 5".&lt;/p&gt;

&lt;p&gt;So the input isn't really lying. It's showing &lt;strong&gt;the user's text&lt;/strong&gt;, while your state holds&lt;br&gt;
&lt;strong&gt;the number that text means&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Proof: pass a string instead
&lt;/h2&gt;

&lt;p&gt;Loose equality only coerces when one side is a number. Give React a &lt;em&gt;string&lt;/em&gt; and it's a&lt;br&gt;
plain string comparison:&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;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;"number"&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;amount&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="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="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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;05&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;5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;// → true, so React overwrites the DOM with "5"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;05&lt;/code&gt; snaps to &lt;code&gt;5&lt;/code&gt;. We had this exact pattern in another screen (the loan-amount editor&lt;br&gt;
passes &lt;code&gt;loanAmount.toString()&lt;/code&gt;), and there the leading zero disappears. It's the same&lt;br&gt;
component type, but a different prop type, so it behaves differently.&lt;/p&gt;

&lt;p&gt;Of course, that brings back the decimal problem. &lt;code&gt;"1."&lt;/code&gt; != &lt;code&gt;"1"&lt;/code&gt;, so the dot gets removed&lt;br&gt;
again. Neither option is free.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bonus bug: onChange that never fires
&lt;/h2&gt;

&lt;p&gt;Remember &lt;code&gt;badInput&lt;/code&gt;? React's &lt;code&gt;onChange&lt;/code&gt; for inputs is built on a value tracker. It only&lt;br&gt;
fires when the DOM value &lt;em&gt;actually changed&lt;/em&gt; since last time.&lt;/p&gt;

&lt;p&gt;Start with an empty field and type &lt;code&gt;-&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;before&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;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;
&lt;span class="nx"&gt;after&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;value&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="nx"&gt;badInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;because&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="nx"&gt;isn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;t a number yet)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As far as the value tracker can see, nothing changed, so &lt;strong&gt;your &lt;code&gt;onChange&lt;/code&gt; doesn't&lt;br&gt;
run&lt;/strong&gt;. The user sees a &lt;code&gt;-&lt;/code&gt; on screen that your state knows nothing about. Type &lt;code&gt;-5&lt;/code&gt; and&lt;br&gt;
you get the event, with &lt;code&gt;"-5"&lt;/code&gt;. This explains another class of odd reports ("I typed&lt;br&gt;
&lt;code&gt;e&lt;/code&gt; and the validation didn't show").&lt;/p&gt;
&lt;h2&gt;
  
  
  Other things &lt;code&gt;type="number"&lt;/code&gt; does that you probably don't want
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mouse wheel changes the value&lt;/strong&gt; while the input is focused and hovered. Scroll the
page, and your loan amount goes from ₹50,000 to ₹49,999.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;e&lt;/code&gt;, &lt;code&gt;E&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;.&lt;/code&gt; are allowed&lt;/strong&gt; anywhere, because scientific notation is a
valid number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;maxLength&lt;/code&gt; is ignored.&lt;/strong&gt; The spec only applies it to text-like input types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Localisation:&lt;/strong&gt; some browsers accept &lt;code&gt;,&lt;/code&gt; as a decimal separator depending on the
locale, so &lt;code&gt;value&lt;/code&gt; changes meaning across users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spinner buttons&lt;/strong&gt; appear on desktop unless you hide them with vendor CSS.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  What we did
&lt;/h2&gt;

&lt;p&gt;We switched to a text input with a numeric keyboard hint:&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;TextInput&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;"text"&lt;/span&gt;
  &lt;span class="na"&gt;inputMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"numeric"&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;amount&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&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="nx"&gt;onAmountChange&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;ul&gt;
&lt;li&gt;
&lt;code&gt;inputMode="numeric"&lt;/code&gt; still shows the number pad on mobile, which is the main reason
people choose &lt;code&gt;type="number"&lt;/code&gt; anyway.&lt;/li&gt;
&lt;li&gt;The value is a normal string, so React compares strictly and what you see is exactly
what's in state.&lt;/li&gt;
&lt;li&gt;We handle parsing and validation ourselves (strip non-digits, clamp to the max amount),
so behaviour is identical in every browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;code&gt;inputMode="decimal"&lt;/code&gt; if you need a decimal point, and keep the &lt;strong&gt;raw string&lt;/strong&gt; in&lt;br&gt;
state (e.g. &lt;code&gt;"1."&lt;/code&gt;) instead of parsing on every keystroke. Parse when you submit or blur.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;A controlled input doesn't mean "the DOM is always a serialization of state". React
DOM makes different trade-offs for different input types.&lt;/li&gt;
&lt;li&gt;For &lt;code&gt;type="number"&lt;/code&gt;, React compares with &lt;strong&gt;loose equality&lt;/strong&gt; on purpose, so users can
type intermediate states like &lt;code&gt;1.&lt;/code&gt;. The same rule keeps &lt;code&gt;05&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Passing a number vs a string to &lt;code&gt;value&lt;/code&gt; changes the behaviour.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;input.value&lt;/code&gt; can be &lt;code&gt;""&lt;/code&gt; while the user sees text on screen (&lt;code&gt;badInput&lt;/code&gt;), and React's
&lt;code&gt;onChange&lt;/code&gt; may not fire at all in that case.&lt;/li&gt;
&lt;li&gt;For amounts, use &lt;code&gt;type="text"&lt;/code&gt; + &lt;code&gt;inputMode&lt;/code&gt; + your own parsing.&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
  </channel>
</rss>
