<?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 Gradle 9 broke our Flutter SDK, and how we're fixing it</title>
      <dc:creator>Navya Sinha</dc:creator>
      <pubDate>Thu, 22 Oct 2026 22:06:58 +0000</pubDate>
      <link>https://dev.to/navya_sinha_f15ddf3e8e056/why-gradle-9-broke-our-flutter-sdk-and-how-were-fixing-it-32d8</link>
      <guid>https://dev.to/navya_sinha_f15ddf3e8e056/why-gradle-9-broke-our-flutter-sdk-and-how-were-fixing-it-32d8</guid>
      <description>&lt;p&gt;One of our partners upgraded their Android build tools and their app stopped building. The cause wasn't their code. It was a single outdated line in our Flutter plugins, pointing at a download server the new tools no longer know about.&lt;/p&gt;

&lt;p&gt;The fix is two lines per plugin. The more interesting story is why it happened, why the fix lives where it does, and why we're deliberately not shipping the bigger upgrade at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 60-second primer 🧠
&lt;/h2&gt;

&lt;p&gt;Three names that come up regulary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Gradle&lt;/code&gt; is the build tool. It takes an Android project, downloads the libraries it needs and compiles it into an app. Think of it as the cook.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AGP (Android Gradle Plugin)&lt;/code&gt; is Google's add-on that teaches Gradle how to build Android apps. It's the recipe book, and it has versions: 8.x, and now 9.x.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Kotlin&lt;/code&gt; is the language our Android plugin code is written in. Until AGP 9 you needed a separate Kotlin add-on (KGP), like a second cookbook. AGP 9 binds Kotlin into the main recipe book, which Google calls "built-in Kotlin".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important point: the partner's app picks the versions of all three, not us. Our SDK has to work with whatever kitchen it's dropped into.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened? 🔍
&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%2Fhec8bxee6kh78ik571ft.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%2Fhec8bxee6kh78ik571ft.png" alt=" " width="799" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The partner builds its app with Gradle 9.1 and AGP 9.0.1, and the app uses our plugin. The build failed during setup, before our SDK ran a single line.&lt;/p&gt;

&lt;p&gt;The culprit was one word in our plugin's build file: &lt;code&gt;jcenter()&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;That line tells Gradle to download libraries from JCenter, an old public library server. JCenter stopped taking new uploads in 2021 and since then has only passed on copies of what's on Maven Central, the main public server. Gradle deprecated jcenter() in Gradle 7 and deleted it completely in Gradle 9. So when Gradle 9 reads our file, it hits a command it doesn't recognise and stops.&lt;/p&gt;

&lt;p&gt;It wasn't just one partner's problem. Every version of our Gateway and Loans Flutter plugins we've published has that line, so any partner on Gradle 9 would hit the same wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is our file in their build at all?🤔
&lt;/h2&gt;

&lt;p&gt;When a partner adds a Flutter plugin, Flutter pulls the plugin's Android folder into the partner's build, and the partner's Gradle runs it. &lt;/p&gt;

&lt;p&gt;Our native Android SDK works differently: partners download it as a finished package from our Artifactory, and its build files never run on their machines. That's why the fix belongs in the Flutter plugin repo and not the Android SDK repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we don't just use the newest everything 🤷‍♀️
&lt;/h2&gt;

&lt;p&gt;It's tempting to ask why our SDK wasn't already on the latest tools. The answer is that an SDK is a guest in someone else's kitchen. &lt;/p&gt;

&lt;p&gt;Each partner decides when to upgrade their tools, and upgrading touches their whole app, so some move fast and many move slowly. If our SDK demanded the newest tools, every partner who hadn't upgraded would be stuck on an old version of our SDK, without our bug fixes. &lt;/p&gt;

&lt;p&gt;So we deliberately support old setups: our plugins accept Flutter versions as old as 3.0 and Android versions as old as 5.0 (API 21).&lt;/p&gt;

&lt;p&gt;The catch is that supporting old setups means keeping some old pieces in our files. Usually that's harmless. But sometimes the newest tools delete an old piece outright, and then partners on the newest tools break. That's exactly what happened with jcenter().&lt;/p&gt;

&lt;p&gt;We call this staying on the borderline: our SDK has to work at both ends at the same time, the oldest setup partners still run and the newest one they're moving to. &lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: small now, bigger later 🔨
&lt;/h2&gt;

&lt;p&gt;We're shipping this as two separate changes, on purpose.&lt;/p&gt;

&lt;h4&gt;
  
  
  The patch
&lt;/h4&gt;

&lt;p&gt;In both the Gateway and Loans plugins, jcenter() becomes mavenCentral(). That's two lines per plugin. Maven Central holds the same libraries JCenter was passing on, and it works on every Gradle version we support, old and new. Nothing else changes: same SDK features, same native SDK versions, same minimum Flutter and Android versions. A partner on older tools won't notice anything.&lt;/p&gt;

&lt;h4&gt;
  
  
  The upgrade
&lt;/h4&gt;

&lt;p&gt;This moves the plugins fully onto AGP 9's way of doing things. It drops the separate Kotlin add-on and relies on AGP 9's built-in Kotlin, renames some old settings to their new names, and raises the minimum Flutter version to 3.44. It also upgrades our demo app to the newest tools so we can test it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just ship the upgrade?
&lt;/h2&gt;

&lt;p&gt;Because it breaks the first rule. Raising the Flutter minimum, and depending on built-in Kotlin, would lock out partners who haven't upgraded. We expect that would include the partner who reported the bug, since they run AGP 9 with built-in Kotlin switched off. The patch unblocks everyone today, and the upgrade can follow as a planned, announced release.&lt;/p&gt;

&lt;p&gt;One thing that confuses people: neither bumps the AGP version inside our plugins. That's because the plugins never choose AGP; the partner's app does. Being "AGP 9 ready" means our build file works when AGP 9 runs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it means for partners?
&lt;/h2&gt;

&lt;p&gt;The easiest way to see the impact is to picture three kinds of partner.&lt;/p&gt;

&lt;h4&gt;
  
  
  Partner A hasn't upgraded yet and is still on Gradle 8 or older
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Today: everything works.&lt;/li&gt;
&lt;li&gt;After the patch: still works, and nothing changes for them.&lt;/li&gt;
&lt;li&gt;After the upgrade: likely breaks. Below Flutter 3.44 they can't install it at all, and without built-in Kotlin our plugin's Kotlin code wouldn't be compiled.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Partner B upgraded to AGP 9 but kept Kotlin the old way by switching built-in Kotlin off.
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Today: the build fails on jcenter().&lt;/li&gt;
&lt;li&gt;After the patch: the jcenter() error is gone, and we expect the build to work. We're testing that now.&lt;/li&gt;
&lt;li&gt;After the upgrade: likely breaks, because the upgrade expects built-in Kotlin and they've switched it off.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Partner C upgraded fully and uses built-in Kotlin. This needs Flutter 3.47 or newer.
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Today: the build fails on jcenter().&lt;/li&gt;
&lt;li&gt;After the patch: probably still fails. Our plugin still adds the old Kotlin add-on, and built-in Kotlin replaces it.&lt;/li&gt;
&lt;li&gt;After the upgrade: works. This is the setupthe upgrade is built for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the patch helps A and B and hurts nobody, and the upgrade is what C needs. Neither covers all three yet. &lt;/p&gt;

&lt;p&gt;The open question for the upgrade is whether one version of our plugin can serve both B and C, for example by adding the old Kotlin add-on only when built-in Kotlin is switched off.&lt;/p&gt;

</description>
      <category>android</category>
      <category>flutter</category>
      <category>mobile</category>
      <category>softwaredevelopment</category>
    </item>
    <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>
