Paso
Paso

Reputation: 363

Why does adding a input event listener break a managed input in react?

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

Answers (1)

DBS
DBS

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

Related Questions