"You set input.value = 'foo'. The UI shows 'foo'. The user clicks submit and the server gets an empty string. React owns the input now, and your naive set is invisible to its tracker. Lesson 5 is the one-line trick that fixes it on every framework you'll meet in production."
Why the Naive Set Fails
React patches the prototype of HTMLInputElement and HTMLTextAreaElement to track value changes via a custom setter. When you write input.value = 'foo', the patched setter notes that the change came from outside React's component lifecycle and silently discards it from the change tracker. The visible value updates (the underlying property still moves), but React's internal state stays whatever it was, and the next render snaps it back.
Vue, Svelte, Solid have similar mechanisms with slightly different shapes. The fix is the same: bypass whatever wrapper the framework installed and write directly to the native setter.
The Native Setter Trick
Get the original setter from the prototype before any framework has had a chance to overwrite it:
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
).set;
nativeInputValueSetter.call(input, 'foo');
input.dispatchEvent(new Event('input', { bubbles: true }));
Two things happen here:
- The native setter writes the actual property. No tracker wrap; the DOM updates with no fanfare.
- The synthetic
inputevent fires. React (and every other framework) listens for nativeinputevents to update internal state. Bubbling matters because most frameworks attach the listener up at the root, not on the input itself.
Together they look indistinguishable from a real keystroke. React's onChange runs, internal state updates, the next render renders the new value. Submit works.
Textareas and ContentEditable
For <textarea>, use HTMLTextAreaElement.prototype:
const nativeTextareaValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
'value'
).set;
For contenteditable elements (Slack, Notion, Gmail's compose), the trick is different:
el.focus();
document.execCommand('insertText', false, 'foo');
document.execCommand is deprecated but still works in every browser. The Selection-API-based replacement (navigator.clipboard.write + paste, or InputEvent dispatch) is more involved and varies by framework. For ClipDeck, execCommand is the pragmatic choice.
When This Matters
ClipDeck v1 doesn't fill inputs — clips are read-out, not paste-in. v2's roadmap has 'paste this clip into the focused input' for the workflow where a user collects snippets and pastes them into their text editor or chat app. That's exactly when the native setter trick matters; without it, pasting into Slack or Gmail leaves the UI showing the text but the actual sent message empty.
Detection — Is This Framework-Managed?
A quick check: read the input's __reactProps or __reactInternalInstance (React's internal field; the exact name has varied across React versions). If present, the framework is React. Vue uses __vueParentComponent on the element. Svelte has no obvious marker but its inputs respond to native events normally, so the trick still works without detection.
You can also feature-detect: try the naive set, then read the value back after a tick. If it reverts, the framework owns it. In practice, just always use the native setter — it's correct in both cases.
One Helper to Cover Them All
Wrap it:
function fillInput(el, value) {
if (el.tagName === 'INPUT') {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
setter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
} else if (el.tagName === 'TEXTAREA') {
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
setter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
} else if (el.isContentEditable) {
el.focus();
document.execCommand('insertText', false, value);
}
}
Three branches, one consistent caller. Works across React, Vue, Svelte, Solid, and most contenteditable rich editors.
input.value = ... bypasses the tracker silently. The fix is one line: pull the native setter from the prototype, call it, dispatch a bubbling input event. Indistinguishable from a real keystroke.