Using JQuery plugins that transform the DOM in React Components?

Some JQuery plugins don't just add behavior to DOM nodes, but change them. For example, Bootstrap Switch turns

<input type="checkbox" name="my-checkbox" checked>

into something like

<div class="bootstrap-switch bootstrap-switch-wrapper bootstrap-switch-on bootstrap-switch-large bootstrap-switch-animate">
  <div class="bootstrap-switch-container">
    <span class="bootstrap-switch-handle-on bootstrap-switch-primary">ON</span>
    <label class="bootstrap-switch-label">&nbsp;</label>
    <span class="bootstrap-switch-handle-off bootstrap-switch-default">OFF</span>
    <input type="checkbox" name="download-version" checked="" data-size="large" data-on-text="3" data-off-text="2.0.1">
  </div>
</div>

with

$("[name='my-checkbox']").bootstrapSwitch();

Which doesn't jive with React:

Uncaught Error: Invariant Violation: findComponentRoot(..., .0): Unable to find
element. This probably means the DOM was unexpectedly mutated (e.g., by the
browser), usually due to forgetting a <tbody> when using tables or nesting <p> or
<a> tags. ...<omitted>...`. 

Is there a recommended technique for incorporating these plugins into React components? Or do they fundamentally break the assumptions of React and cannot work with it?


No, react will react (haha) badly to anything that modifies its own component dom structure outside of react. This is something you don't ever want to do. The recommended solution would be to replicate the functionality of whatever you're trying to do with a jquery or similar plugin, in react.

Having said that, there is a reasonable way to do this for specific instances where you just can't do without it, but it essentially means wrapping some non-react dom inside react.

Example:

var Example = React.createClass({
    componentDidMount: function() {
        var $checkboxContainer = $(this.refs.checkboxContainer.getDOMNode());

        var $checkbox = $('<input />').prop('type', 'checkbox');

        $checkboxContainer.append($checkbox);

        $checkbox.bootstrapSwitch({});
    },
    render: function() {
        return (
            <div>
                <div ref="checkboxContainer"></div>
            </div>
        )
    }
});

Now of course you are rendering a component with a nested div. The nested when mounted to the dom for the first time that nested div will get a checkbox appended to it by jquery, which will then also execute our jquery plugin on it.

This particular example component has little point to it, however you can see how this might integrate into a more complex component while still allowing you to re-render and react to state changes etc. You just lose the ability to directly react to events/modify things inside of the checkbox in question which as far as react is concerned, doesn't exist.

Now with the above example if you were to have some react logic to add/remove the nested div, you'd have to have the same logic around that div being inserted be responsible for re-inserting the checkbox and re-initializing it with the jquery plugin. However because react only modifies the dom when needed, this inserted dom content wont be removed unless you do something that modifies the container div in a way that causes it to be removed/re-rendered to the dom. This means you can still access all of the events within react for that container div etc.

You could also make use of the componentDidMount function in react to bind events or callbacks to specific interactions on the checkbox itself. Just make sure to unbind them correctly in componentWillUnmount or wherever it makes sense to do so in the component lifecycle in your specific case.


In this great ryanflorence's tutorial you'll get an idea on how to do this:

Wrapping DOM Libs

Methodology

  1. DOM libs usually manipulate the DOM
  2. React tries to re-render and finds a different DOM than it had last time and freaks out
  3. We hide the DOM manipulation from React by breaking the rendering tree and then reconnecting around the DOM the lib manipulates.
  4. Consumers of our component can stay in React-land.

Sure, there is such a technique. We're doing these things all the time.

  1. You create React component to wrap jQuery plugin.
  2. Inside of your render(), you return an empty <div ref="placeholder" />
  3. In your componentDidMount method, you retrieve this element by its ref, and initialize your jQuery plugin there.
  4. In your componentWillUnmount, you clean it up. Calling 'destroy', or anything else required to avoid memory leaks.

That's it. Fortunately, it's completely safe to modify DOM in this way in React.

If you want this plugin to react on props changes, things get a bit more tricky. You need to override other lifecycle methods, like componentWillReceiveProps, check whenever props actually changed, and call corresponding plugin methods. I can explain in more details, if you will have specific questions, overall topic is too broad for the comment.


This is more of a philosophical question

React was created to optimize DOM manipulations and has a lot of wiring behind the scenes to do so when a component's state changes via setState

Doing so will cause said wiring to traverse its virtual DOM to find the nodes that need to be updated

If you must use React, whether to try to keep a level of consistency in your coding, your best bet is to apply the JQuery DOM manipulation inside the componentDidMount like so...

componentDidMount(){
    this.node = $("#"+this.props.id); // Keep a reference to the node
    this.chart = this.node.easyPieChart(); // Apply JQuery transformation and keep a reference
    this.percentTitle = this.chart.find(".percent"); // Keep a reference to the title
}

Having done so, on whatever your "refresh" method is, do NOT make any calls to setState, instead, call whatever update method your JQuery component may have, like so...

componentWillMount(){
   this.interval = setInterval(this._doRefresh.bind(this), 1500);
}

_doRefresh( percentage ){
    // Note how setState is NOT being called here
    percentage = percentage || Math.floor (Math.random() * 100) // simulate since we're not getting it yet
    this.chart.data('easyPieChart').update(percentage); // call easyPieChart's update
    this.percentTitle.text(percentage);
}

At this point, if you're asking why use React at all, well, in my case, this component is an item in a list of other React components and was used to maintain consistency throughout the application... You may have a similar dilemma

If, unlike me, you are unlucky enough that your component doesn't have an update method, and you can't make one, it might be time to rethink the approach altogether