You can move React.js root component around
… and check why 5600+ Rails engineers read also this
You can move React.js root component around
My recent challenge with react was to integrate it with Magnific Popup library. I knew there was going to be a problem because the library is moving DOM elements around. But I had two interesting insights while solving this problem that I think are worth sharing.
Confirming the problem
Here is completely oversimplified version of my problem.
You can press “Button inside popup” (playground below) to change state.
When you press “Show popup” the Magnificent Popup
library however will move the popup content into different
DOM element. When you try change the state one more time
by pressing “Button inside popup” (this time actually
inside popup) it will break with Invariant Violation:
findComponentRoot(..., .0.1.0): Unable to find element. This probably
means the DOM was unexpectedly mutated
. Go ahead. Try
for yourself. Open Developer Console to see the error.
Here is the code for this example:
var clicked = function(){
var node = mountedComponent.refs.popup.getDOMNode();
$.magnificPopup.open({
items: {
src: node,
type: 'inline'
}
});
}
var popupClicked = function(){
mountedComponent.setState({pretendStateChanged: Date.now() })
};
var Component = React.createClass({
getInitialState: function() {
return {pretendStateChanged: Date.now() };
},
render: function(){
return React.DOM.div(null,
React.DOM.a({onClick: clicked, href: "javascript:void(0);"}, "Show popup"),
React.DOM.br(null),
React.DOM.span(null, "State: " + this.state.pretendStateChanged),
Popup({ref: "popup", onClickHandler: popupClicked})
);
}
});
var Popup = React.createClass({
render: function(){
return React.DOM.div(null,
React.DOM.a({
onClick: this.props.onClickHandler,
href: "javascript:void(0);"
}, "Button inside popup")
);
}
});
mountedComponent = React.renderComponent(
Component(),
document.getElementById("reactExampleGoesHere")
);
Here is how the component looks like rendered in DOM before becoming popup.
And after magnificPopup moves it to a different place.
So I did some research and found this interesting React JS - What if the dom changes thread that included some really nice hint: I think React won’t get confused if jQuery moves the root around.
Moving React component
So I decoupled in my little application the Popup
component from my top-level Component
and started
rendering them separately.
If you show popup and click inside it, you will change state of both components. But even though the component rendered insided popup (handled by magnificPopup library) was moved around in DOM, we no longer expierience our problem. Because moving top-level react component around DOM works fine (here I am actually moving the element above the react root node but the concept stays the same).
Here is the code for this example…
var clicked = function(){
var node = mountedPopup.getDOMNode().parentNode;
$.magnificPopup.open({
items: {
src: node,
type: 'inline'
}
});
}
var popupClicked = function(){
mountedComponent.setState({pretendStateChanged: Date.now() }) ;
mountedPopup.setState({pretendStateChanged: Date.now() });
};
var Component = React.createClass({
getInitialState: function() {
return {pretendStateChanged: Date.now() };
},
render: function(){
return React.DOM.div(null,
React.DOM.a({onClick: clicked, href: "javascript:void(0);"}, "Show popup"),
React.DOM.br(null),
React.DOM.span(null, "State: " + this.state.pretendStateChanged)
);
}
});
var Popup = React.createClass({
getInitialState: function() {
return {pretendStateChanged: Date.now() };
},
render: function(){
return React.DOM.div(null,
React.DOM.a({
onClick: this.props.onClickHandler,
href: "javascript:void(0);"
}, "Button inside popup"),
React.DOM.br(null),
React.DOM.span(null, "State: " + this.state.pretendStateChanged)
);
}
});
var mountedComponent = React.renderComponent(
Component(),
document.getElementById("reactExampleGoesHere").childNodes[1]
);
var mountedPopup = React.renderComponent(
Popup({onClickHandler: popupClicked}),
document.getElementById("reactExampleGoesHere").childNodes[3]
);
<div id="reactExampleGoesHere">
<div></div>
<div class="mfp-hide"></div>
</div>
The forum thread that I mentioned show a really nice demo for integrating react with jQuery UI Sortable. You can move the elements around thanks to sortable. But their content is rendered with react. It’s all possible because every element is rendered as separate react root.
So this is a useful trick to know.
Avoide imperative coding
While working with this code I had one more “Aha moment”. I was looking at
my code and thinking Why am I calling show()/hide() on popup library in my
handlers? I didn’t come to React to keep doing that. The idea was to have
props and state and transform it into HTML view. Not to call show()
or
hide()
manually.
I should be setting state and the component should know whether to use 3rd party library to show or hide itself. After all, if I ever want change the popup library (most likely) then such change should be localized to the popup component. I should not change my handlers because I changed my popup library.
So… Move the component behavior of popup inside Popup
. And let it decide
when to show and hide. That’s what it would be doing if it were pure React
component. That’s what is should be doing when it is not so pure, but
coupled with external library.
Here is the code for this example…
var clicked = function(){
mountedPopup.setState({visible: true, pretendStateChanged: Date.now() });
mountedComponent.setState({pretendStateChanged: Date.now() }) ;
}
var popupClicked = function(){
mountedPopup.setState({visible: false, pretendStateChanged: Date.now() });
mountedComponent.setState({pretendStateChanged: Date.now() }) ;
};
var Component = React.createClass({
getInitialState: function() {
return {pretendStateChanged: Date.now() };
},
render: function(){
return React.DOM.div(null,
React.DOM.a({onClick: clicked, href: "javascript:void(0);"}, "Show popup"),
React.DOM.br(null),
React.DOM.span(null, "State: " + this.state.pretendStateChanged)
);
}
});
var Popup = React.createClass({
getInitialState: function() {
return {visible: false, pretendStateChanged: Date.now()};
},
componentWillUpdate: function(nextProps, nextState){
if (!this.state.visible && nextState.visible) {
this.popUp();
}
/* closed by application */
if (this.state.visible && !nextState.visible) {
this.closePopUp();
}
},
popUp: function(){
var self = this;
var parent = this.getDOMNode().parentNode;
$.magnificPopup.open({
items: {
src: parent,
type: 'inline'
},
removalDelay: 30,
callbacks: {
afterClose: function() {
if (self.state.visible){
/* closed by user pressing ESC */
self.setState({visible: false});
}
}
}
});
},
closePopUp: function(){
$.magnificPopup.close();
},
render: function(){
return React.DOM.div(null,
React.DOM.a({
onClick: this.props.onClickHandler,
href: "javascript:void(0);"
}, "Button inside popup"),
React.DOM.br(null),
React.DOM.span(null, "State: " + this.state.pretendStateChanged)
);
}
});
var mountedComponent = React.renderComponent(
Component(),
document.getElementById("reactExampleGoesHere").childNodes[1]
);
var mountedPopup = React.renderComponent(
Popup({onClickHandler: popupClicked}),
document.getElementById("reactExampleGoesHere").childNodes[]
);
<div id="reactExampleGoesHere">
<div></div>
<div class="mfp-hide"></div>
</div>
Summary
- For the purpose of integrating with other libraries that don’t play nicely with React remember that you can isolate react component. And libraries move it around DOM however they want as long as they don’t ingerate inside it.
- Integrate with the external libraries inside a component, not outside them. If you ever want to change your solution to pure react or something else, it will be localized to that one component.