0

I have a range input with a react component, it's only moving the slider. not triggering the callback function that's there onChange

Trigering event explicitly isnt working either for react.

const volumeResetHandler = async () => {
  document.querySelector('input[type=range]').value = 0;//as mentioned by @mathias-s setSlv(0) should also be fine , my concern is the next line
 document.querySelector('input[type=range]').dispatchEvent(new Event("change"));
}
<input
 className="slider_input"
 type="range"
 value={slv}
 min="0"
 max="600"
 step="10"
 autoFocus
 onChange={volumeChangeHandler}
/>
<h3 onClick={volumeResetHandler} className="cursor-pointer">Reset</h3>

I was expecting volumeChangeHandler would get triggered on document.querySelector('input[type=range]').value = 0; but it doesn't

All solutions mentioned here suffers from same issue

this does for jquery, without looking at react or javscript, it doesnt answer as it is for jquery , changing jquery to js like this in react isn't working either.

So there must be something different going on with events in react.

Here is the updated code after trying useRef (as suggested by @LãNgọcHải)

import { useRef } from 'preact/hooks'
const inputRef = useRef<HTMLInputElement>(null)
const volumeResetHandler = async () => {
   inputRef.current.value = 0
}
<input
 className="slider_input"
 type="range"
 value={slv}
 min="0"
 max="600"
 step="10"
 autoFocus
 onChange={volumeChangeHandler}
 ref={inputRef}
/>
<h3 onClick={volumeResetHandler} className="cursor-pointer">Reset</h3>

Inspiration from this

3 Answers 3

1

Actually, directly manipulating the DOM will not make React recognize the change. You are calling volumeChangeHandler but in a complicated way.

This doesn't work:

  const volumeResetHandler = async () => {
    inputRef.current.value = 0;
    (inputRef.current as any).addEventListener('change', volumeChangeHandler);
    (inputRef.current as any).dispatchEvent(new Event('change', { bubbles: true }));
    // if you replace volumeChangeHandler with another function you will see that
    // it won't get triggered when the actual <input> changes naturally
  }

This is how it should be:

  const volumeResetHandler = async () => {
     const valueSetter= Object.getOwnPropertyDescriptor(
         window.HTMLInputElement.prototype,"value")
          .set;                             //Take out the setter function
     valueSetter.call(inputRef.current, 0); //This is how React internally updates state

     inputRef.current.dispatchEvent(new Event('change', { bubbles: true }));
     //now this event will actually trigger the onchange of the <input>
  }

Remember that React uses virtual DOM, direct change of real DOM without state management won't be reconciled so it doesn't trigger the onchange event of the <input>

Edit: my code suggestion is written in js, if you're using typescript you might need to change the code a bit to rule out the null/undefined

Sign up to request clarification or add additional context in comments.

11 Comments

Well it isn't working for me, probably you need to take out dispatchEvent as well.
I understand that first method will fail if I replace volumeChangeHandler with another function . I had to do it since dispatchEvent wasn't reaching the real addEventListener code. Basically I created a parallel ecosystem since the original addEventListener is not reachable. But using valueSetter didn't change that , the real addEventListener code is still not reachable.
you still have to use dispatchEvent to manually trigger the event of other element in the DOM (make React recognizes the change in DOM). i tested and it worked in JS, so for typescript you might need to do something like using non-null assertion before calling the methods. Here is the working stackblitz in JS
Well Fork of your stackblitz works with rangeInput.current.value = 0; as well. So my point of using valueSetter or not doesn't make any difference holds. Probably there's something wrong in setup of my project that dispatchEvent isn't calling the original addEventListener
@ishandutta2007 you copied the wrong link to your fork, i can't see the code. I tried rangeInput.current.value = 0 but it does not trigger the onChange of the <input>, unless that is not what you want
|
0

In React, you need to use a controlled input to solve this. You should avoid using DOM manipulations like querySelector in combination with React for cases like this.

To render a controlled input, pass the value prop to it. React will force the input to always have the value you passed.

Use the onChange prop to call setValue to ensure that the input value always reflects the React state.

function MyComponent() {
  const [value, setValue] = useState(10);

  return (
    <input
      type="range"
      value={value} // ...force the input's value to match the state variable...
      onChange={(e) => setValue(e.target.value)} // ... and update the state variable on any edits!
    />
  );
}

You can also call setValue from other buttons or elements, like this:

function MyComponent() {
  const [value, setValue] = useState(10);

  const volumeResetHandler = () => {
    setValue(0);
  };

  return (
    <>
      <input
        type="range"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <button onClick={volumeResetHandler}>Reset</button>
    </>
  );
}

If you have more logic in your volumeChangeHandler, that should be run for both volumeChangeHandler and volumeResetHandler, the idiomatic way to solve this in React would be to move that custom logic into its own function like this:

function MyComponent() {
  const [value, setValue] = useState(10);

  const customChangeHandler = (value) => {
    // Do some custom logic here
    setValue(value);
  };

  const volumeResetHandler = () => {
    customChangeHandler(0);
  };

  const volumeChangeHandler = (e) => {
    customChangeHandler(e.target.value);
  };

  return (
    <>
      <input
        type="range"
        value={value}
        onChange={volumeChangeHandler}
      />
      <button onClick={volumeResetHandler}>Reset</button>
    </>
  );
}

15 Comments

I get what you are saying but in my case the trigger has to come from javascript which was triggered onclick of some other element.
@ishandutta2007 maybe you can use Refs to trigger the event?
@ishandutta2007 i wasn't sure because i only found an answer that worked on class component. Can you simply call the function of the onChange or do you have to trigger the change event? Edit: nevermind just read the answer right under the one i found
@ishandutta2007 Why do you need to dispatch your own event or update the field value imperatively by directly manipulating the DOM? From your updated code, it looks like both your button/h3 tag and input fields are in the same component. If that's the case, it seems like you're thinking about this in the wrong way. Would my updated solution solve your needs? If not, can you give an example of an operation where you really need to use refs here?
@ishandutta2007 It's circumventing the problem altogether, and that's the point. Using a common function gives you a simpler component, improving both readability and maintainability. If you don't need to use custom events, then you probably shouldn't.
|
0

I got it working with slight modification of what @LãNgọcHải suggested:

  const volumeResetHandler = async () => {
    inputRef.current.value = 0;
    (inputRef.current as any).addEventListener('change', volumeChangeHandler);
    (inputRef.current as any).dispatchEvent(new Event('change', { bubbles: true }));
  }

Thanks a lot @LãNgọcHải . Only a small issue I have is I had a logic based on event.isTrusted==true which is now false for any explicitly triggering event. I would have to find some work around for that. I guess that's what bubbles is for, isn't it @LãNgọcHải ?

4 Comments

bubbles is how the event "spread"/ "climb" the DOM tree. For example when you click on a button that is inside a span, the event "bubbles" up from the button to the span and trigger a click event. More info and example here
@LãNgọcHải For my case there is no bubbles passed to volumeChangeHandler when actual input range element is moved, that's what why I could change that logic to (event.isTrusted==true || event.isTrusted==false && event.bubbles==true) as a workaround.
the bubbles is added because React handles events in higher level in the DOM, event needs to bubble up for React to trigger onChange (well, i guess thats why in html it's onchange while in React it's onChange lol)
how can I get isTrusted to be true in such case ?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.