<?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: Arafat</title>
    <description>The latest articles on DEV Community by Arafat (@flinthive).</description>
    <link>https://dev.to/flinthive</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%2F3817845%2F1190d78e-9ae7-47fb-8f03-c0acb6486dac.png</url>
      <title>DEV Community: Arafat</title>
      <link>https://dev.to/flinthive</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/flinthive"/>
    <language>en</language>
    <item>
      <title>Why input.value = 'x' doesn't fill a React form (and what actually works)</title>
      <dc:creator>Arafat</dc:creator>
      <pubDate>Thu, 20 Aug 2026 04:13:45 +0000</pubDate>
      <link>https://dev.to/flinthive/why-inputvalue-x-doesnt-fill-a-react-form-and-what-actually-works-1hl7</link>
      <guid>https://dev.to/flinthive/why-inputvalue-x-doesnt-fill-a-react-form-and-what-actually-works-1hl7</guid>
      <description>&lt;p&gt;If you have ever tried to script a React form — a test helper, a browser extension, a bookmarklet, an onboarding demo — you have probably hit 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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#email&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field shows the text. It looks filled. Then you submit, and React sends an empty string. Or the "Next" button stays disabled. Or the value vanishes the moment anything else on the page re-renders.&lt;/p&gt;

&lt;p&gt;I ran into this repeatedly while building a form-filling extension, and the fix is not obvious from React's docs. Here is what is actually happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  React keeps its own copy of the value
&lt;/h2&gt;

&lt;p&gt;React attaches a &lt;em&gt;value tracker&lt;/em&gt; to every controlled input. It is an internal object stored on the DOM node under a property React owns, and its only job is to remember what React last saw in that field.&lt;/p&gt;

&lt;p&gt;When a real user types, two things happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The browser updates &lt;code&gt;input.value&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The browser fires an &lt;code&gt;input&lt;/code&gt; event&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;React's synthetic event system catches that event, compares the node's current value against the tracker's remembered value, sees they differ, and &lt;em&gt;therefore&lt;/em&gt; concludes something changed and calls your &lt;code&gt;onChange&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That comparison is the whole story. React does not ask "did the value change?" — it asks "does the DOM disagree with what I remember?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Why direct assignment breaks it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;HTMLInputElement.prototype.value&lt;/code&gt; is an accessor property with a setter. React replaces that setter on the individual node with its own version, one that updates the tracker as it writes.&lt;/p&gt;

&lt;p&gt;So when you do &lt;code&gt;input.value = 'x'&lt;/code&gt;, you go through React's patched setter. It writes the value &lt;strong&gt;and&lt;/strong&gt; updates the tracker to match. The DOM and the tracker now agree.&lt;/p&gt;

&lt;p&gt;Then you dispatch an &lt;code&gt;input&lt;/code&gt; event, React compares the two, finds them identical, and concludes nothing changed. &lt;code&gt;onChange&lt;/code&gt; never fires. React's state still holds the old value, and the next render wipes your text away.&lt;/p&gt;

&lt;p&gt;You did not fail to notify React. You notified React and React decided you were lying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: bypass the patched setter
&lt;/h2&gt;

&lt;p&gt;You need to write the value &lt;em&gt;without&lt;/em&gt; going through React's setter, so the tracker stays stale and the comparison detects a difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setNativeValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;HTMLTextAreaElement&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;HTMLTextAreaElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&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;setter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOwnPropertyDescriptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;setter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&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;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&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;bubbles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set&lt;/code&gt; is the browser's original setter, untouched. Calling it with &lt;code&gt;.call(el, value)&lt;/code&gt; writes straight to the DOM, leaving React's tracker holding the old value. Now the comparison fails, React believes you, and &lt;code&gt;onChange&lt;/code&gt; fires.&lt;/p&gt;

&lt;p&gt;Note the prototype check. &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; has its own prototype with its own value setter, and using the input one on a textarea throws.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three more things that bite
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; needs &lt;code&gt;change&lt;/code&gt;, not &lt;code&gt;input&lt;/code&gt;.&lt;/strong&gt; Select elements notify on &lt;code&gt;change&lt;/code&gt;. Dispatch &lt;code&gt;input&lt;/code&gt; at a &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; and nothing happens. Checkboxes and radios need &lt;code&gt;click()&lt;/code&gt; or a &lt;code&gt;change&lt;/code&gt; event, not a value write at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some libraries want the full keyboard sequence.&lt;/strong&gt; A handful of masked-input and autocomplete components listen for &lt;code&gt;keydown&lt;/code&gt;/&lt;code&gt;keyup&lt;/code&gt; rather than &lt;code&gt;input&lt;/code&gt;. If the native-setter approach alone does not stick, dispatching &lt;code&gt;keydown&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;keyup&lt;/code&gt; in order usually does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vue 3 is easier, Angular is different.&lt;/strong&gt; Vue's &lt;code&gt;v-model&lt;/code&gt; listens for plain &lt;code&gt;input&lt;/code&gt; events, so the ordinary path works. Angular's &lt;code&gt;ControlValueAccessor&lt;/code&gt; also listens for &lt;code&gt;input&lt;/code&gt; on most controls, but reactive forms sometimes need a &lt;code&gt;blur&lt;/code&gt; afterwards to run validators — otherwise the field is filled and still marked untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the write
&lt;/h2&gt;

&lt;p&gt;This is the part I would most encourage you to steal, because it changed how much time I spent debugging.&lt;/p&gt;

&lt;p&gt;After writing a value, read it back:&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="nf"&gt;setNativeValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&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="c1"&gt;// let the framework's microtask queue drain&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&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="c1"&gt;// the framework rejected or transformed the input&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 value came back different, the page's framework rejected it — a mask reformatted it, a validator cleared it, a controlled component overwrote it on re-render. That is a completely different failure from "the selector did not match", and it needs a completely different fix.&lt;/p&gt;

&lt;p&gt;Reporting those two cases separately turned my most common bug report from "it doesn't work" into "your app rejected this input, here is what it became" — which is actionable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shadow DOM: the other half of the problem
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;document.querySelectorAll('input')&lt;/code&gt; does not see inside shadow roots. A lot of modern component libraries put their real &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; inside one, so a script that works fine on a plain HTML page finds nothing at all on a component-library page.&lt;/p&gt;

&lt;p&gt;Open shadow roots you can walk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;deepInputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &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;el&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="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;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input, textarea, select&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;el&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;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shadowRoot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;deepInputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shadowRoot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// nested roots too&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;Shadow roots nest, so this has to recurse — a component inside a component inside a component is common, and a single-level check misses it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Closed shadow roots you cannot reach, at all.&lt;/strong&gt; &lt;code&gt;element.shadowRoot&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; when the root was created with &lt;code&gt;{ mode: 'closed' }&lt;/code&gt;, and there is no workaround from page or extension context. This is by design. The honest thing is to detect the likely case and say so rather than silently filling nothing: a custom element with no light-DOM children and no accessible &lt;code&gt;shadowRoot&lt;/code&gt; is probably a closed root, and telling the user that is far better than a silent no-op.&lt;/p&gt;

&lt;p&gt;Same-origin iframes work through &lt;code&gt;iframe.contentDocument&lt;/code&gt;. Cross-origin iframes do not, and no amount of cleverness changes that — you need an explicit host permission for that origin, which is a real cost to weigh rather than something to engineer around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not fill everything you find
&lt;/h2&gt;

&lt;p&gt;The last thing, and the one I got wrong first: a script that fills every field it can find is worse than one that fills nothing.&lt;/p&gt;

&lt;p&gt;Skip these, always:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;disabled&lt;/code&gt; and &lt;code&gt;readonly&lt;/code&gt; fields&lt;/li&gt;
&lt;li&gt;Anything hidden — &lt;code&gt;type="hidden"&lt;/code&gt;, &lt;code&gt;display: none&lt;/code&gt;, zero-size, &lt;code&gt;visibility: hidden&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSRF tokens&lt;/strong&gt; — usually a hidden input with &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;csrf&lt;/code&gt; or &lt;code&gt;authenticity&lt;/code&gt; in the name. Overwriting one breaks the submit in a way that is genuinely hard to diagnose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAPTCHA fields&lt;/strong&gt; — never touch them&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt; — you cannot set it programmatically for good security reasons, and trying throws&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ended up writing more test fixtures for the &lt;em&gt;must-not-fill&lt;/em&gt; cases than for the fill cases, and that ratio turned out to be right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it against adversarial fixtures, not happy paths
&lt;/h2&gt;

&lt;p&gt;Every one of the failures above is invisible on a simple form and obvious on a real one. So the fixtures worth writing are the nasty ones: a React controlled input, an open shadow root, a &lt;em&gt;nested&lt;/em&gt; shadow root, a same-origin iframe, a field whose only signal is an &lt;code&gt;autocomplete&lt;/code&gt; token, a field with no signals at all, an input with dots in its &lt;code&gt;name&lt;/code&gt;, a CAPTCHA, a hidden CSRF token.&lt;/p&gt;

&lt;p&gt;Mine runs 56 fixtures and grades 75 cases — the extras are suite-level invariants like "one report row per discovered field" and "widget selections are deterministic". Current state is 65 passed, 0 failed, 10 correctly skipped, where the 10 skips are the guard cases that &lt;em&gt;should&lt;/em&gt; be refused.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl60njtn8n9oohzyy0jil.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%2Fl60njtn8n9oohzyy0jil.png" alt="FormForge self-test report showing 65 passed, 0 failed, 10 correctly skipped" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The number that matters there is the 10, not the 65. Anyone can make a suite go green by only testing what already works; refusing to fill a CAPTCHA is the harder assertion.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;I packaged all of this into &lt;strong&gt;FormForge&lt;/strong&gt;, a Chrome extension that fills forms with realistic test data. It ships the self-test page described above, so you can run all 56 fixtures in your own browser build rather than taking my word for any of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chrome Web Store:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://chromewebstore.google.com/detail/alkelkiiomnmjiajjcecjeicmhfhconb" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/alkelkiiomnmjiajjcecjeicmhfhconb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More detail and pricing:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://flinthive.com/formforge" rel="noopener noreferrer"&gt;https://flinthive.com/formforge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Free tier has no daily limit, no account and no card. It runs entirely locally — no server, no telemetry, no AI, and zero standing host permissions.&lt;/p&gt;

&lt;p&gt;But the native-setter trick above is the useful part whether or not you ever install anything. It is the single fix that solves the most common form-scripting bug in React, and it took me far too long to find.&lt;/p&gt;

&lt;p&gt;If you know a form that breaks any of this, I would genuinely like to hear about it.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>I built 125+ free calculators with React + TanStack Router on Cloudflare Pages — here's what I learned</title>
      <dc:creator>Arafat</dc:creator>
      <pubDate>Mon, 06 Jul 2026 08:33:43 +0000</pubDate>
      <link>https://dev.to/flinthive/i-built-125-free-calculators-with-react-tanstack-router-on-cloudflare-pages-heres-what-i-21lj</link>
      <guid>https://dev.to/flinthive/i-built-125-free-calculators-with-react-tanstack-router-on-cloudflare-pages-heres-what-i-21lj</guid>
      <description>&lt;p&gt;A few months ago I set out to build something simple: a collection of free, useful calculators that anyone could use without signing up or dealing with ads.&lt;/p&gt;

&lt;p&gt;That turned into &lt;strong&gt;freefixo.com&lt;/strong&gt; — 125+ calculators covering finance, health, legal, and travel. Here's the stack I used and what surprised me along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React&lt;/strong&gt; (Vite) — component-per-calculator approach kept things modular&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TanStack Router&lt;/strong&gt; — file-based routing made adding new calculators trivially easy. Each new tool = one new file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Pages&lt;/strong&gt; — free tier, global CDN, deploys in ~30 seconds from a &lt;code&gt;git push&lt;/code&gt;. Zero config for a CSR SPA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; — rapid styling without a separate stylesheet per component&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No backend. No database. Everything runs client-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why TanStack Router over React Router?
&lt;/h2&gt;

&lt;p&gt;I wanted type-safe routes from day one. TanStack Router's file-based routing + full TypeScript inference meant I never had a mistyped route string again. The tradeoff: slightly steeper learning curve for the search param handling, but worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare Pages for a CSR SPA
&lt;/h2&gt;

&lt;p&gt;One gotcha: Cloudflare Pages serves a 404 for direct URL hits on client-side routes unless you add a &lt;code&gt;_redirects&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="err"&gt;/*&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt;index.html 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line fixes SPA routing entirely. Took me embarrassingly long to figure out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling to 125+ calculators without losing my mind
&lt;/h2&gt;

&lt;p&gt;The key was a consistent pattern: every calculator is a self-contained React component that receives no props from outside — all state is local. This meant I could build, test, and ship each one independently without touching anything else.&lt;/p&gt;

&lt;p&gt;I also built a simple JSON config file that drives the homepage grid, search, and category filters. Adding a new calculator = add the component + one line in the config.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add meta tags earlier.&lt;/strong&gt; I retrofitted per-page &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;description&amp;gt;&lt;/code&gt; tags after launch. Should've done this from calculator #1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the search first.&lt;/strong&gt; Users immediately want to search. I built it as an afterthought.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't wait for perfection.&lt;/strong&gt; I had 40 calculators ready weeks before I launched. Ship earlier, iterate publicly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Live at &lt;strong&gt;&lt;a href="https://freefixo.com" rel="noopener noreferrer"&gt;https://freefixo.com&lt;/a&gt;&lt;/strong&gt; — no signup, no ads, just tools.&lt;/p&gt;

&lt;p&gt;Would love feedback from the DEV community, especially on the tech choices. Anyone else using TanStack Router in production? How are you handling SEO for CSR apps on Cloudflare Pages?&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Built a Free All-in-One Tool Website with 70+ Tools — Here's the Story</title>
      <dc:creator>Arafat</dc:creator>
      <pubDate>Wed, 11 Mar 2026 05:50:32 +0000</pubDate>
      <link>https://dev.to/flinthive/i-built-a-free-all-in-one-tool-website-with-70-tools-heres-the-story-jej</link>
      <guid>https://dev.to/flinthive/i-built-a-free-all-in-one-tool-website-with-70-tools-heres-the-story-jej</guid>
      <description>&lt;h2&gt;
  
  
  Why I Built FixFlowHub
&lt;/h2&gt;

&lt;p&gt;I was constantly frustrated switching between different websites &lt;br&gt;
for simple tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compress an image → TinyPNG&lt;/li&gt;
&lt;li&gt;Merge a PDF → SmallPDF (with watermark 😤)&lt;/li&gt;
&lt;li&gt;Check grammar → Grammarly (paid)&lt;/li&gt;
&lt;li&gt;Generate a QR code → some random sketchy site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most tools either require sign-up, add watermarks, or lock &lt;br&gt;
basic features behind a paywall.&lt;/p&gt;

&lt;p&gt;So I decided to build &lt;strong&gt;one place where everything is actually free&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://fixflowhub.com" rel="noopener noreferrer"&gt;FixFlowHub&lt;/a&gt;&lt;/strong&gt; — a free all-in-one &lt;br&gt;
online toolbox with 70+ tools across 5 categories.&lt;/p&gt;

&lt;h3&gt;
  
  
  🖼️ Image Tools (23 tools)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Compress images without quality loss&lt;/li&gt;
&lt;li&gt;Convert between JPG, PNG, WebP, HEIC, AVIF, SVG, BMP, TIFF, ICO&lt;/li&gt;
&lt;li&gt;Resize, crop, rotate, flip, watermark&lt;/li&gt;
&lt;li&gt;Grayscale converter, image editor&lt;/li&gt;
&lt;li&gt;Image to Base64 encoder&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📄 PDF Tools (8 tools)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Merge multiple PDFs into one&lt;/li&gt;
&lt;li&gt;Split PDF into pages&lt;/li&gt;
&lt;li&gt;Compress PDF size&lt;/li&gt;
&lt;li&gt;Convert PDF to image &amp;amp; image to PDF&lt;/li&gt;
&lt;li&gt;Lock/unlock PDF with password&lt;/li&gt;
&lt;li&gt;Reorder PDF pages&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🤖 AI Text Tools (11 tools)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Resume builder&lt;/li&gt;
&lt;li&gt;Cover letter generator&lt;/li&gt;
&lt;li&gt;Grammar checker&lt;/li&gt;
&lt;li&gt;Text rewriter &amp;amp; paraphraser&lt;/li&gt;
&lt;li&gt;Email writer&lt;/li&gt;
&lt;li&gt;LinkedIn profile generator&lt;/li&gt;
&lt;li&gt;YouTube SEO generator&lt;/li&gt;
&lt;li&gt;SEO tag generator&lt;/li&gt;
&lt;li&gt;AI text summarizer&lt;/li&gt;
&lt;li&gt;Business slogan generator&lt;/li&gt;
&lt;li&gt;Product description generator&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🧮 Calculator Tools (15 tools)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Loan EMI calculator&lt;/li&gt;
&lt;li&gt;BMI calculator&lt;/li&gt;
&lt;li&gt;Age calculator&lt;/li&gt;
&lt;li&gt;Compound interest calculator&lt;/li&gt;
&lt;li&gt;GPA calculator&lt;/li&gt;
&lt;li&gt;Calorie calculator&lt;/li&gt;
&lt;li&gt;Pregnancy due date calculator&lt;/li&gt;
&lt;li&gt;Currency converter&lt;/li&gt;
&lt;li&gt;Salary calculator&lt;/li&gt;
&lt;li&gt;And more...&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🛠️ Utility Tools (10 tools)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;QR code generator&lt;/li&gt;
&lt;li&gt;Password generator&lt;/li&gt;
&lt;li&gt;JSON formatter &amp;amp; validator&lt;/li&gt;
&lt;li&gt;Lorem ipsum generator&lt;/li&gt;
&lt;li&gt;Color picker (HEX, RGB, HSL)&lt;/li&gt;
&lt;li&gt;Time zone converter&lt;/li&gt;
&lt;li&gt;Case converter&lt;/li&gt;
&lt;li&gt;Text cleaner&lt;/li&gt;
&lt;li&gt;HTML entities encoder&lt;/li&gt;
&lt;li&gt;YouTube thumbnail downloader&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React + Vite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling:&lt;/strong&gt; Tailwind CSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Processing:&lt;/strong&gt; HTML5 Canvas API (client-side)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF Processing:&lt;/strong&gt; PDF.js&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Tools:&lt;/strong&gt; Anthropic Claude API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; Lovable + Custom domain&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Design Decisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Privacy First:&lt;/strong&gt; All image and PDF processing happens &lt;br&gt;
in the browser. Files are never uploaded to any server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No Sign-Up:&lt;/strong&gt; I wanted zero friction. Open the tool, &lt;br&gt;
use it, done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No Watermarks:&lt;/strong&gt; Every tool is fully functional for free.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Building is the easy part&lt;/strong&gt; — getting traffic is hard 😅&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO takes time&lt;/strong&gt; — don't expect results overnight&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;People appreciate simplicity&lt;/strong&gt; — no popups, no forced 
sign-ups, just working tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tools attract users&lt;/strong&gt; — but monetization needs thought&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://fixflowhub.com" rel="noopener noreferrer"&gt;fixflowhub.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Would love your feedback! What tools should I add next? &lt;br&gt;
Drop a comment below 👇&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
  </channel>
</rss>
