Reputation: 363
I am trying to write a hook that takes a (ref to a) input element and listens to updates on it. As it is done decoupled from the rendering, in a hook, I can not use the normal onInput
prop directly on the element.
When updating unrelated state (using a useState
hook) in the event listener the input element breaks in that you can not type into it and the caret/focus changes instead.
Why does addEventListener('input', handleInput)
and onInput={handleInput}
differ in this way in react and what is the best workaround for this issue?
Here is a snippet that reproduces the issue:
const Hello = (props) => {
const inputRef = React.useRef();
const [val, setVal] = React.useState("Can't type here");
const [other, setOther] = React.useState(0);
React.useEffect(() => {
const inputEl = inputRef.current;
const handleInput = (ev) => {
console.log('In handleInput');
setOther(prev => prev + 1)
};
inputEl.addEventListener('input', handleInput);
return () => inputEl.removeEventListener('input', handleInput);
}, []);
return (
<input
ref={inputRef}
value={val}
onChange={ev => setVal(ev.target.value)}
/>
);
}
ReactDOM.render(
<Hello />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Upvotes: 3
Views: 744
Reputation: 9994
Your non-React event is triggering a state change setOther(prev => prev + 1)
which is causing the component to re-render before the React event can take place.
I would generally recommend against using non-React events where possible, and definitely if you need their resulting code to affect the state.
If you remove the state change from the vanilla JS event handler the component should function as expected:
const Hello = (props) => {
const inputRef = React.useRef();
const [val, setVal] = React.useState("Can't type here");
const [other, setOther] = React.useState(0);
React.useEffect(() => {
const inputEl = inputRef.current;
const handleInput = (ev) => {
console.log('In handleInput');
};
inputEl.addEventListener('input', handleInput);
return () => inputEl.removeEventListener('input', handleInput);
}, []);
return (
<input
ref={inputRef}
value={val}
onChange={ev => setVal(ev.target.value)}
/>
);
}
ReactDOM.render(
<Hello />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
If you need both events, why not attach them both to the React event, like this (React onChange
listens for input, rather then blur anyway):
const Hello = (props) => {
const inputRef = React.useRef();
const [val, setVal] = React.useState("Can't type here");
const [other, setOther] = React.useState(0);
return (
<input
ref={inputRef}
value={val}
onChange={ev => {
setVal(ev.target.value)
console.log('In handleInput')
setOther(prev => prev + 1)
}}
/>
);
}
ReactDOM.render(
<Hello />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Upvotes: 1