Trigger Input Updates with React Controlled Inputs
Cory Rylan
- 3 minutes
When using React for HTML text inputs, you may run into an issue of component state not updating. Missed updates are a common problem when interfacing with third-party or non-React components. This post will cover how React handles HTML inputs and fix common issues with out-of-sync Controlled inputs.
React has two different APIs for HTML inputs, Controlled and Uncontrolled. Uncontrolled inputs allow you to interact with input directly with refs just like you would with plain HTML and JavaScript. The preferred Controlled inputs work a bit differently.
Controlled Inputs
By default, HTML inputs retain their internal state and emit an event when that state has changed due to user input. Controlled inputs in React will manage the input state and ensure the input state is only managed within React. This ensures that there is only ever one copy of the form state value in the component. However, while reducing state is good, it causes issues with how native inputs behave. Let's take a look at an example:
import React, { useState } from 'react';
import { render } from 'react-dom';
function App() {
const [input, setInput] = useState(Math.random().toString());
function setNativeInput() {
const input = document.querySelector('#input');
// This will update the input but the state in React will not be updated.
input.value = Math.random().toString();
}
return (
<div>
<label htmlFor="input">Input:</label>
<input id="input" type="text" value={input} onChange={e => setInput(e.target.value)} />
<p>value: {input}</p>
<button onClick={() => setNativeInput()}>Set Native Input Value</button>
</div>
);
}
render(<App />, document.getElementById("root"));
In this example, if you click the button, the input will be updated; however, the text in the paragraph will not.
React does not use native DOM events nor native Custom Elements. React will overload the input value setter to know when the input state has been set and changed. When overriding the native setter, this can break the input if that input is managed by something outside of React. An example of this could be a non-React component wrapped in React, Web Components, or e2e testing frameworks.
Fixing Out of Sync React State
The fix when using a third-party input as a Controlled input is to manually trigger a DOM event a second time to trigger React to re-render. React will de-duplicate updates if an event fires and the state haven't changed. By triggering the second event, we can force a new Render cycle.
In our fix, we first call the original native value setter that React overloaded. This will update the input state. Once updated, we dispatch a new change
event on the input, so React will trigger a new re-render as the input value will be different from the component's state.
import React, { useState } from 'react';
import { render } from 'react-dom';
function App() {
const [input, setInput] = useState(Math.random().toString());
function setNativeInput() {
const input = document.querySelector('#input');
// input.value = Math.random().toString(); // nope
// This will work by calling the native setter bypassing Reacts incorrect value change check
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')
.set.call(input, Math.random().toString());
// This will trigger a new render wor the component
input.dispatchEvent(new Event('change', { bubbles: true }));
}
return (
<div>
<label htmlFor="input">Input:</label>
<input id="input" type="text" value={input} onChange={e => setInput(e.target.value)} />
<p>value: {input}</p>
<button onClick={() => setNativeInput()}>Set Native Input Value</button>
</div>
);
}
render(<App />, document.getElementById("root"));
If you are using a checkbox input, the event should be a click
as the change
event won't trigger the re-render.
import React, { useState } from 'react';
import { render } from 'react-dom';
import "./style.css";
function App() {
const [checkbox, setCheckbox] = useState(false)
function setNativeCheckbox() {
const checkbox = document.querySelector('#checkbox');
// This will not update the React component state
// checkbox.checked = !checkbox.checked;
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'checked')
.set.call(checkbox, !checkbox.checked);
checkbox.dispatchEvent(new Event('click', { bubbles: true }));
}
return (
<div>
<label htmlFor="checkbox">Checkbox:</label>
<input id="checkbox" type="checkbox" checked={checkbox} onChange={e => setOne(e.target.checked)} />
<p>checked: {checkbox ? 'true' : 'false'}</p>
<button onClick={() => setNativeCheckbox()}>Set Native Checkbox Checked</button>
</div>
);
}
render(<App />, document.getElementById("root"));
Like the regular input, we call the original setter, in this case, the checked property. Once set checked
, we dispatch a new event, this time the click
event. Checkboxes and Radio inputs did not respond to the change
event like the native text inputs. Controlled inputs are a great way to manage input state in React but be aware of some of the issues when interacting with third-party components or directly with the DOM.