I am going through the react documentation article of integrating-with-jquery-chosen-plugin, and react gives an example of the Chosen jquery plugin and mentions the following:
Notice how we wrapped
<select>
in an extra<div>
. This is necessary because Chosen will append another DOM element right after the<select>
node we passed to it. However, as far as React is concerned,<div>
always only has a single child. This is how we ensure that React updates won’t conflict with the extra DOM node appended by Chosen. It is important that if you modify the DOM outside of React flow, you must ensure React doesn’t have a reason to touch those DOM nodes.
class Chosen extends React.Component {
render() {
return (
<div>
<select className="Chosen-select" ref={el => this.el = el}>
{this.props.children}
</select>
</div>
);
}
}
I initially thought that when react re renders <App>
with new setState()
call, if we don’t have the extra div
around the select
then there would be trouble for the diffing algorithm and dom
elements appended after select
would be removed. But, apparently this is not whats happening. In terms of code:
<App> | |<App>
<Component1/> | | <Component1/>
<Chosen/> | -- rerender --> | <Chosen/> //gets updated and removes nodes after <select>
<Component2/> | | <Component2/>
</App> | |</App>
I tried to imitate this situation and to my surprise, even if I don’t wrap select
in a div
, it doesn’t get updated and the extra dom nodes added after select
stay intact:
//class component Chosen
class Chosen extends React.Component {
constructor(props) {
super(props);
this.state = {
chosen: this.props.chosen,
};
//I haven't used this state, but even if the option tag has value as this state
//On state update, the appended red input doesn't get deleted
}
render() {
return (
<select id="unique">
<option value="one">one</option>
<option value="two">two</option>
</select>
);
}
}
class App extends React.Component {
//create state for fancybtn1 and 2
constructor(props) {
super(props);
this.state = {
fancyBtn1: "one",
fancyBtn2: "two",
};
//after 5 seconds update state
setTimeout(() => {
this.setState({
fancyBtn1: "three",
fancyBtn2: "four",
});
}, 5000);
}
//componentDidMount
componentDidMount() {
console.log("componentDidMount");
//grab #unique and append an input tag after it
setTimeout(() => { //jquery plugin like dom manipulation //$("#unique").Chosen();
const unique = document.getElementById("unique");
const input = document.createElement("input");
input.type = "text";
input.style.border = "1px solid red";
input.value = "test";
unique.after(input);
}, 2500);
}
render() {
console.log("App render");
return (
<div>
<h1>App {this.state.fancyBtn1} </h1>
<FancyBtn txt={this.state.fancyBtn1} />
<Chosen chosen={this.state.fancyBtn1} />
<FancyBtn txt={this.state.fancyBtn1} />
</div>
);
}
}
let FancyBtnCount = 0;
//create FancyBtn class based component which has state for txt
class FancyBtn extends React.Component {
constructor(props) {
super(props);
this.state = {
txt: this.props.txt,
};
//set counter, which keeps track of how many times the component is instantiated
//I couldn't do it properly atm.
FancyBtnCount++;
}
//on component update
componentWillReceiveProps(nextProps) { // I know gives deprecated warning
console.log("componentWillReceiveProps");
//set state if prev state is different
if (this.state.txt !== nextProps.txt) {
this.setState({
txt: nextProps.txt,
});
}
}
render() {
return (
<div>
<h1>FancyBtn {FancyBtnCount} </h1>
<input type="text" value={this.state.txt} />
<button>{this.state.txt}</button>
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
After 2.5 seconds I add a red input
dom node after select
and after 5 seconds, I re render the whole app. Now why doesn’t with the re render the react diffing algorithm remove the red input
? What difference would it make if I wrap the select
in an extra div
? Basically I am looking for an explanation of the quoted react docs.
2
Answers
This is most likely not a complete answer, but too long to post it as a comment.
In this specific case you are defining a react component named Chosen:
In React, a component can never return multiple "top level" elements, because it wouldn’t make any sense.
So an even simpler component like this:
is "illegal", because a component has to always return only one "top level" element. If for some reason you want your component to return multiple elements like in the example above, you’d have to wrap them into a div:
and now you would be correctly returning only one top-level element, which then in turn has those two divs as a children.
So in your case if you were to do it like this:
it wouldn’t work (even though it looks like there is only one element) because the chosen library apparently works like so that it will turn that select component into something like this for example:
Now React has a problem with this because the component is returning more than one element. However, if you wrap the select into a div, the result would be something like this:
and React will be happy again.
As far as I know though, this div-wrapping method is quite an old way to do things. More modern way is to use React Fragments.
As for why such rule exists, I’ve always thought about it similarly to JavaScript’s ordinary functions, so something like this makes no sense:
If we really want to return 1 and 2 though, we would wrap them into something such as an array:
Wrapping a div is done due to following Reasons :