Take the following component:
function MyComponent() {
const [ inputValue, setInputValue ] = useState( '' );
const [ submittedValue, setSubmittedValue ] = useState( '' );
const inputRef = useRef<HTMLInputElement>( null );
useEffect( () => {
const inputElement = inputRef.current;
const handleEvent = ( event ) => {
if ( event.key === 'Enter' ) {
event.preventDefault();
setSubmittedValue( inputValue );
}
};
console.log( 'adding listener' );
inputElement?.addEventListener( 'keydown', handleEvent );
return () => inputElement?.removeEventListener( 'keydown', handleEvent );
}, [ inputValue ] );
return (
<>
<input value={inputValue} onChange={( e ) => setInputValue( e.target.value )} ref={inputRef}></input>
<p>{submittedValue}</p>
</>
);
}
The component lets you type in a controlled input, and hit the ‘Enter’ key to "submit" the current input value. The submitted value should display below the input and remain static until "submitting" again.
This works as intended. The problem is that a ‘keydown’ listener is added on every keystroke.
Expected results: ‘adding listener’ prints once on mount
Actual results: ‘adding listener’ prints on every keystroke
I understand why. On every keystroke, onChange runs, which runs setInputValue, which changes useEffect‘s dependency list, which adds the listener again.
The easiest way to solve this problem is to just add onKeyDown={handleEvent} to the input. But that would be too easy.
I only got to this point because the legacy Input element I’m using does not implement onKeyDown
Is there a way to achieve the expected results without using onKeyDown?
>Solution :
Since you’re also removing an event listener on every keystroke, I think what you’re doing now is perfectly fine – the handler is only being fired once per event, as desired, and the handler will be removed when the component unmounts, which is good. Listeners being added and removed frequently might not feel great, but it honestly doesn’t cause any issues in 99% of situations, and it works, so probably isn’t worth worrying about.
If you really don’t want to add the listener multiple times, I suppose you could retrieve the value from the ref instead of requiring an up-to-date closure over the inputValue.
function MyComponent() {
const [ inputValue, setInputValue ] = React.useState( '' );
const [ submittedValue, setSubmittedValue ] = React.useState( '' );
const inputRef = React.useRef( null );
React.useEffect( () => {
const inputElement = inputRef.current;
const handleEvent = ( event ) => {
if ( event.key === 'Enter' ) {
event.preventDefault();
setSubmittedValue( inputRef.current.value );
}
};
inputElement.addEventListener( 'keydown', handleEvent );
return () => inputElement.removeEventListener( 'keydown', handleEvent );
}, [] );
return (
<React.Fragment>
<input value={inputValue} onChange={( e ) => setInputValue( e.target.value )} ref={inputRef}></input>
<p>{submittedValue}</p>
</React.Fragment>
);
}
ReactDOM.render(<MyComponent />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>