45

I'm tasked with crawling website built with React. I'm trying to fill in input fields and submitting the form using javascript injects to the page (either selenium or webview in mobile). This works like a charm on every other site + technology but React seems to be a real pain.

so here is a sample code

var email = document.getElementById( 'email' );
email.value = 'example@mail.com';

I the value changes on the DOM input element, but the React does not trigger the change event.

I've been trying plethora of different ways to get the React to update the state.

var event = new Event('change', { bubbles: true });
email.dispatchEvent( event );

no avail

var event = new Event('input', { bubbles: true });
email.dispatchEvent( event );

not working

email.onChange( event );

not working

I cannot believe interacting with React has been made so difficult. I would greatly appreciate any help.

Thank you

Mohsin Awan
  • 1,176
  • 2
  • 12
  • 29
Timo Kauranen
  • 453
  • 1
  • 4
  • 5
  • `The value changes on the DOM input element, but the React does not trigger the change event.` What change event are you expecting? If you're able to successfully fill out the input field, then why not `document.forms[0].submit()`? – lux Nov 30 '16 at 17:51
  • 2
    yes this is what I try to do - but the React validator complains that you must fill in value i.e. the value is not propagated to the React component – Timo Kauranen Nov 30 '16 at 18:12
  • Ah, that's interesting - I understand. I'll toss together a codepen and play around. – lux Nov 30 '16 at 18:34
  • 1
    The answers below helped me overcome the same issue, but I wonder if you had any guidance for the 2nd half of your question, which is submitting the form? I have been unable to successfully submit a react form either by triggering `.click()` on the form's submit button, or by triggering `.submit()` on the form element itself, or by doing either of the above using the `dispatchEvent()` method suggested below. However, manually clicking the submit button does submit the form properly. What am I missing? – Danny Apr 27 '19 at 20:02

6 Answers6

54

This accepted solution appears not to work in React > 15.6 (including React 16) as a result of changes to de-dupe input and change events.

You can see the React discussion here: https://github.com/facebook/react/issues/10135

And the suggested workaround here: https://github.com/facebook/react/issues/10135#issuecomment-314441175

Reproduced here for convenience:

Instead of

input.value = 'foo';
input.dispatchEvent(new Event('input', {bubbles: true}));

You would use

function setNativeValue(element, value) {
  const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set;
  const prototype = Object.getPrototypeOf(element);
  const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;

  if (valueSetter && valueSetter !== prototypeValueSetter) {
    prototypeValueSetter.call(element, value);
  } else {
    valueSetter.call(element, value);
  }
}

and then

setNativeValue(input, 'foo');
input.dispatchEvent(new Event('input', { bubbles: true }));
meriial
  • 4,946
  • 2
  • 23
  • 21
  • 3
    this did not work for me in greasemonkey - probably something about security... it failed even just trying to call `Object.getOwnPropertyDescriptor(..)`. For me, using the `setNativeValue(..)` function from https://stackoverflow.com/a/52486921 worked in greasemonkey. – PressingOnAlways Oct 14 '19 at 03:30
  • How can you change this to work for simply clicking? Also, it appears that React has changed, so you need to send a "change" event now instead – information_interchange Mar 01 '20 at 22:49
  • 1
    It works when run directly in the console but not from an addon (`Object.getOwnPropertyDescriptor(element, 'value').set;` is failing). – remeus Jul 15 '20 at 09:48
  • Seems to work for me, but the Typescript compiler is not at all happy with it :) – panepeter Dec 10 '20 at 09:43
20

React is listening for the input event of text fields.

You can change the value and manually trigger an input event, and react's onChange handler will trigger:

class Form extends React.Component {
  constructor(props) {
    super(props)
    this.state = {value: ''}
  }
  
  handleChange(e) {
    this.setState({value: e.target.value})
    console.log('State updated to ', e.target.value);
  }
  
  render() {
    return (
      <div>
        <input
          id='textfield'
          value={this.state.value}
          onChange={this.handleChange.bind(this)}
        />
        <p>{this.state.value}</p>
      </div>      
    )
  }
}

ReactDOM.render(
  <Form />,
  document.getElementById('app')
)

document.getElementById('textfield').value = 'foo'
const event = new Event('input', { bubbles: true })
document.getElementById('textfield').dispatchEvent(event)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<div id='app'></div>
TimoStaudinger
  • 41,396
  • 16
  • 88
  • 94
  • Thanks, the timeout seems to do the trick - I don't quite understand why it would be so - but If I do the same without timeout (from selenium) the React never gets the event. I do have some problems using the timeout though so I'll have to find a way around it. – Timo Kauranen Nov 30 '16 at 18:42
  • The timeout is not necessary and I added it just to make the example more clear. See the update w/o timeout. The only thing that you need to make sure is that the code that changes the input value is executed after the react component is rendered completely. – TimoStaudinger Nov 30 '16 at 18:45
  • Sure I totally got it - the thing is that calling from Selenium process the events do not fire unless in timeout - your example is now pretty much same as what I have in the initial question so I'm pretty sure there is something in the selenium bridge that messes up the js engine – Timo Kauranen Nov 30 '16 at 18:52
  • Just FYI `const event = new Event('input', { bubbles: true }) document.getElementById('textfield').dispatchEvent(event)` solved what had been a lot of debugging for me. Thank you. – nc. Feb 12 '18 at 18:16
  • Note that if you have a `select` field, you need to dispatch a `change` event rather than an `input` event. – aroth Aug 07 '18 at 01:19
  • Thank you so much! This works, and for people doing web automation this is very important information to share! – eliteproxy Apr 30 '20 at 04:50
11

Here is the cleanest possible solution for inputs, selects, checkboxes, etc. (works not only for react inputs)

/**
 * See [Modify React Component's State using jQuery/Plain Javascript from Chrome Extension](https://stackoverflow.com/q/41166005)
 * See https://github.com/facebook/react/issues/11488#issuecomment-347775628
 * See [How to programmatically fill input elements built with React?](https://stackoverflow.com/q/40894637)
 * See https://github.com/facebook/react/issues/10135#issuecomment-401496776
 *
 * @param {HTMLInputElement | HTMLSelectElement} el
 * @param {string} value
 */
function setNativeValue(el, value) {
  const previousValue = el.value;

  if (el.type === 'checkbox' || el.type === 'radio') {
    if ((!!value && !el.checked) || (!!!value && el.checked)) {
      el.click();
    }
  } else el.value = value;

  const tracker = el._valueTracker;
  if (tracker) {
    tracker.setValue(previousValue);
  }

  // 'change' instead of 'input', see https://github.com/facebook/react/issues/11488#issuecomment-381590324
  el.dispatchEvent(new Event('change', { bubbles: true }));
}

Usage:

setNativeValue(document.getElementById('name'), 'Your name');
document.getElementById('radio').click(); // or setNativeValue(document.getElementById('radio'), true)
document.getElementById('checkbox').click(); // or setNativeValue(document.getElementById('checkbox'), true)
Dmytro Soltusyuk
  • 360
  • 2
  • 10
  • 1
    Thanks, this suits my needs regarding autocomplete and combobox fields that uses ReactJS. :) – Carlos Jaime C. De Leon Sep 10 '21 at 05:54
  • Thanks a bunch. I managed to solve the issue of Jira HelpDesk Widget not seeing that I have pre-populated EMAIL field when submitting the form (validation was failing for that field). With your code I managed to overcome the issue and save dozens of hours writing my own custom widget just because they don't let you pre-populate fields by default. Issue you help me resolve it is https://jira.atlassian.com/browse/JSDCLOUD-7835 – Luka Aug 31 '23 at 18:25
4

I noticed the input element had some property with a name along the lines of __reactEventHandlers$..., which had some functions including an onChange.

This worked for finding that function and triggering it

Update: __reactEventHandlers$... seems to have changed name to __reactProps$..., so try that in the .filter below instead

let getReactEventHandlers = (element) => {
    // the name of the attribute changes, so we find it using a match.
    // It's something like `element.__reactEventHandlers$...`
    let reactEventHandlersName = Object.keys(element)
       .filter(key => key.match('reactEventHandler'));
    return element[reactEventHandlersName];
}

let triggerReactOnChangeEvent = (element) => {
    let ev = new Event('change');
    // workaround to set the event target, because `ev.target = element` doesn't work
    Object.defineProperty(ev, 'target', {writable: false, value: element});
    getReactEventHandlers(element).onChange(ev);
}

input.value = "some value";
triggerReactOnChangeEvent(input);
aljgom
  • 7,879
  • 3
  • 33
  • 28
0

Without element ids:

export default function SomeComponent() {
    const inputRef = useRef();
    const [address, setAddress] = useState("");
    const onAddressChange = (e) => {
        setAddress(e.target.value);
    }
    const setAddressProgrammatically = (newValue) => {
        const event = new Event('change', { bubbles: true });
        const input = inputRef.current;
        if (input) {
            setAddress(newValue);
            input.value = newValue;
            input.dispatchEvent(event);
        }
    }
    return (
        ...
        <input ref={inputRef} type="text" value={address} onChange={onAddressChange}/>
        ...
    );
}
Displee
  • 670
  • 8
  • 20
0

React 17 works with fibers:

function findReact(dom) {
    let key = Object.keys(dom).find(key => key.startsWith("__reactFiber$"));
    let internalInstance = dom[key];
    if (internalInstance == null) return "internalInstance is null: " + key;

    if (internalInstance.return) { // react 16+
        return internalInstance._debugOwner
            ? internalInstance._debugOwner.stateNode
           : internalInstance.return.stateNode;
    } else { // react <16
        return internalInstance._currentElement._owner._instance;
   }
}

then:

findReact(domElement).onChangeWrapper("New value");

the domElement in this is the tr with the data-param-name of the field you are trying to change:

var domElement = ?.querySelectorAll('tr[data-param-name="<my field name>"]')
Tod
  • 2,070
  • 21
  • 27