58

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 = '[email protected]';

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

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()? Commented Nov 30, 2016 at 17:51
  • 3
    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 Commented Nov 30, 2016 at 18:12
  • Ah, that's interesting - I understand. I'll toss together a codepen and play around. Commented Nov 30, 2016 at 18:34
  • 2
    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? Commented Apr 27, 2019 at 20:02
  • Related: Type text into a React input using javascript (Tampermonkey script)? Commented Jun 15 at 21:33

7 Answers 7

65

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 }));
Sign up to request clarification or add additional context in comments.

4 Comments

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 stackoverflow.com/a/52486921 worked in greasemonkey.
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
It works when run directly in the console but not from an addon (Object.getOwnPropertyDescriptor(element, 'value').set; is failing).
Seems to work for me, but the Typescript compiler is not at all happy with it :)
22

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>

6 Comments

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.
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.
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
Note that if you have a select field, you need to dispatch a change event rather than an input event.
This solution did not work for me on id.atlassian.com, maybe because of the React version they're using ... the solution just below this one ("Here is the cleanest possible...") did work though.
|
19

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)

3 Comments

Thanks, this suits my needs regarding autocomplete and combobox fields that uses ReactJS. :)
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 jira.atlassian.com/browse/JSDCLOUD-7835
This also worked for me on id.atlassian.com while the accepted solution did not work.
8

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);

1 Comment

Thanks, the only really useful answer I found after days of experimenting. As, sadly, usual at SO, the most useful answers are neither marked as accepted nor easy to find. You’ve saved me another week of experiments and made it possible for me to avoid messing with React-related tools (just to workaround React limitations in a web-app not under my control) when I never wanted to.
1

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>"]')

Comments

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}/>
        ...
    );
}

Comments

0

This is not working for me. I looked at the source code and there is a check for document.activeElement that is blocking me. So, I added element.focus(); in the code and it started working.

element.focus();
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);
        }
        element.dispatchEvent(new Event('input', { bubbles: true }));

Comments

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.