Wednesday, April 8 2015 9:17 PM

React hates me: "Overcontrolled" components

Author: matthias - Last modified: Wednesday, April 8 2015 9:54 PM

en javascript  react  flux  state  checkbox dom  html  events  prevendefault

More accurately: The DOM hates me. I'm currently hacking a lot of React (with fun) and thought that strictly bypassing the default handlers for user input element onChange events in favor for a fully controlled value flow through React components' state and props might be a good idea. Actually, it is not.


Abstract and advice for the impatient: Don't build React "controlled components" in conjunction with preventDefault() in event handlers listening to onChange (which wraps onKeyUp/Down...) events. Silly idea.


Controlled Components

If you found your way to this post, you're potentially in some similar situation to the one I tried to summarize above: You're using React Forms with Controlled Components and for whatever reason you want to take over full control for every type of user input, typically listening to the React wrapped onChange event (good idea!) and preventing default behavior by calling preventDefault() (bad idea!) – meaning that you also have to take care of setting the state of appropriate properties which in turn update the input fields as soon as the render() method is automatically called on state value changes.

Stressing React/Flux

If you're still think this sounds like a somewhat curious approach, you're right. Anyhow, let me just give a short explanation of my motivation: I'm stressing the concept of React/Flux that way that I even want the state of user input fields to be reflected in a related store – and vice-versa. Therefore dispatched actions that update the stores' properties which values flow back through change events emitted thereafter for eventually updating the DOM sounded like a good idea to me. Yes, give me full control over every user input and create a keystroke state! Well, just an idea. Unfortunately not one of my best.

A code fragment to illustrate the situation:

var TestForm = React.createClass({

  getInitialState: function() {
    return {
      message: 'Hello!'
    };
  },

  handleValueChange: function(e) {
    e.preventDefault();
    // Here I'm calling some Flux style action (creator) which in turn
    // emits a change event and updates the components' state
    this.actions.sendMessageToTheWorldViaFluxStore(e.target.value);
    // (instead of directly reflecting the value via this.state:)
    //this.setState({ message: e.target.value });
  },

  render: function() {
    return (
      <div>
          <input 
              type="text"
              onChange={this.handleValueChange} 
              value={this.state.message} />
      </div>
    );
  }
});

Problems...

Basically this approach seems to be possible and behave like expected when using e.g. standard text input fields. You can catch the current value, store or manipulate it and finally assign the value to a state variable. But you'll face two major problems soon, both not directly related to React but to the nature of HTML elements and the DOM, namely:

Checkboxes (and Radio Buttons): Manually updating state fails for checkboxes (and without having tested it: I think for radio controls as well). Why? The problem one will face is that the checkbox onChange event behaves in a special way since the era of HTML (how could I forget!): You may not toggle the state of a checkbox manually via the .checked property. Nor does React. The onChange (onClick) event is fired after the element state changed internally. This may just be reverted based on the return value of the event handler. See this post for a comprehensive examination of this fact.

Performance: It seems to be reasonably fast to capture the user input events for immediately assigning the values back to the state variable, so that the Virtual DOM updates and eventually tiny parts of the actual DOM is re-rendered. But as soon as those values go a longer way through JavaScript objects, event listeners and further manipulation, it significantly slows down Browser performance and kills UX (noticeable lag during typing!).

... and the solution.

"But don't you see", you may say, "that you must not use preventDefault() and just go on like you want with all that Flux crap! State may be updated and reflected (delayed) either way." Yes, absolutely. Call me an idiot. Let the browser do its very basic jobs. (But... unfortunately then it cannot be guaranteed that the captured input fields actually reflect the state of my store... what about race conditions when updating... uhh... ouch! ... Well, who the f--- cares!)

Here's a React fiddle dealing with that stuff.

Comment / Share »