Why should dom element be wrapped in an empty div while integrating jquery plugin in react Js?

huangapple go评论65阅读模式
英文:

Why should dom element be wrapped in an empty div while integrating jquery plugin in react Js?

问题

React文档中提到,在使用Chosen jQuery插件时,将<select>包装在额外的<div>中是必要的,因为Chosen会在我们传递给它的<select>节点之后附加另一个DOM元素。但就React而言,<div>始终只有一个子元素,这样可以确保React更新不会与Chosen附加的额外DOM节点冲突。如果您在React流之外修改DOM,必须确保React没有理由触摸那些DOM节点。

在您提供的代码示例中,您注意到即使不包装<select>在额外的<div>中,React重新渲染后也不会删除红色的<input>节点。这是因为React的协调算法会尽量在不删除整个DOM节点的情况下进行更新。如果您将<select>包装在额外的<div>中,React将只更新<div>内部的内容,而不会影响<select>外部的其他DOM节点。

因此,包装<select>在额外的<div>中的主要目的是为了确保React只更新<div>内部的内容,而不影响<select>外部的其他DOM节点。这有助于避免潜在的冲突和不必要的DOM更新。这就是React文档中引用的内容的解释。

英文:

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 &lt;select&gt; in an extra &lt;div&gt;. This is necessary because Chosen will append another DOM element right after the &lt;select&gt; node we passed to it. However, as far as React is concerned, &lt;div&gt; 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 (
      &lt;div&gt;        
        &lt;select className=&quot;Chosen-select&quot; ref={el =&gt; this.el = el}&gt;          
          {this.props.children}
        &lt;/select&gt;
      &lt;/div&gt;
    );
  }
}

I initially thought that when react re renders &lt;App&gt; 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:

&lt;App&gt;                   |                 |&lt;App&gt; 
  &lt;Component1/&gt;         |                 |  &lt;Component1/&gt;   
  &lt;Chosen/&gt;             | -- rerender --&gt; |  &lt;Chosen/&gt;  //gets updated and removes nodes after &lt;select&gt;
  &lt;Component2/&gt;         |                 |  &lt;Component2/&gt; 
&lt;/App&gt;                  |                 |&lt;/App&gt;    

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:

<!-- begin snippet: js hide: false console: true babel: true -->

<!-- language: lang-js -->

//class component Chosen
class Chosen extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      chosen: this.props.chosen,
    };
    //I haven&#39;t used this state, but even if the option tag has value as this state
    //On state update, the appended red input doesn&#39;t get deleted
  }



  render() {
    return (
      &lt;select id=&quot;unique&quot;&gt;
          &lt;option value=&quot;one&quot;&gt;one&lt;/option&gt;
          &lt;option value=&quot;two&quot;&gt;two&lt;/option&gt;
        &lt;/select&gt;
    );
  }
}



class App extends React.Component {
  //create state for fancybtn1 and 2
  constructor(props) {
    super(props);
    this.state = {
      fancyBtn1: &quot;one&quot;,
      fancyBtn2: &quot;two&quot;,
    };

    //after 5 seconds update state
    setTimeout(() =&gt; {
      this.setState({
        fancyBtn1: &quot;three&quot;,
        fancyBtn2: &quot;four&quot;,
      });
    }, 5000);

  }

  //componentDidMount
  componentDidMount() {
    console.log(&quot;componentDidMount&quot;);
    //grab #unique and append an input tag after it
    setTimeout(() =&gt; { //jquery plugin like dom manipulation //$(&quot;#unique&quot;).Chosen();
      const unique = document.getElementById(&quot;unique&quot;);
      const input = document.createElement(&quot;input&quot;);
      input.type = &quot;text&quot;;
      input.style.border = &quot;1px solid red&quot;;
      input.value = &quot;test&quot;;
      unique.after(input);
    }, 2500);
  }




  render() {
    console.log(&quot;App render&quot;);
    return (
      &lt;div&gt;
        &lt;h1&gt;App {this.state.fancyBtn1} &lt;/h1&gt;
        &lt;FancyBtn txt={this.state.fancyBtn1} /&gt;
        &lt;Chosen chosen={this.state.fancyBtn1} /&gt;
        &lt;FancyBtn txt={this.state.fancyBtn1} /&gt;
      &lt;/div&gt;
    );
  }
}

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&#39;t do it properly atm.
    FancyBtnCount++;
  }

  //on component update
  componentWillReceiveProps(nextProps) { // I know gives deprecated warning 
    console.log(&quot;componentWillReceiveProps&quot;);
    //set state if prev state is different
    if (this.state.txt !== nextProps.txt) {
      this.setState({
        txt: nextProps.txt,
      });
    }
  }



  render() {
    return (
      &lt;div&gt;
        &lt;h1&gt;FancyBtn {FancyBtnCount} &lt;/h1&gt;
        &lt;input type=&quot;text&quot; value={this.state.txt} /&gt;
        &lt;button&gt;{this.state.txt}&lt;/button&gt;
      &lt;/div&gt;
    );
  }
}







ReactDOM.render(&lt;App /&gt;, document.querySelector(&quot;#app&quot;))

<!-- language: lang-html -->

&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js&quot;&gt;&lt;/script&gt;


&lt;div id=&quot;app&quot;&gt;&lt;/div&gt;

<!-- end snippet -->

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.

答案1

得分: 6

这很可能不是一个完整的答案,但太长了,无法将其作为评论发布。

在这个特定的情况下,您正在定义一个名为Chosen的React组件:

class Chosen extends React.Component {
  render() {
    return (
      <div>        
        <select className="Chosen-select" ref={el => this.el = el}>          
          {this.props.children}
        </select>
      </div>
    );
  }
}

在React中,组件绝不能返回多个"顶级"元素,因为这没有任何意义。

因此,像这样更简单的组件也是"非法"的,因为组件必须始终只返回一个"顶级"元素。如果出于某种原因,您希望您的组件返回多个元素,就像上面的示例一样,您必须将它们包装到一个div中:

class Chosen extends React.Component {
    render() {
        return (
            <div>
                <div></div>
                <div></div>
            <div>
        );
    }
}

现在,您将正确返回一个顶级元素,然后这个元素又有这两个div作为子元素。

所以在您的情况下,如果您这样做:

class Chosen extends React.Component {
  render() {
    return (     
        <select className="Chosen-select" ref={el => this.el = el}>          
          {this.props.children}
        </select>
    );
  }
}

它将无法工作(即使看起来只有一个元素),因为选择的库显然是这样工作的,它会将该选择组件转换为类似于以下示例的内容:

class Chosen extends React.Component {
  render() {
    return (  
        <div class="chosen-select-label">My select</div>   
        <div class="chosen-select">
          <select>
            <option></option>
            <option></option>
          </select>
        </div>
        <div class="some-other-chosen-select-shenanigans">
          ...
        </div>
    );
  }
}

现在React对此有问题,因为该组件返回的不止一个元素。但是,如果将选择包装到一个div中,结果将类似于以下内容:

class Chosen extends React.Component {
  render() {
    return (
      <div>
        <div class="chosen-select-label">My select</div>   
        <div class="chosen-select">
          <select>
            <option></option>
            <option></option>
          </select>
        </div>
        <div class="some-other-chosen-select-shenanigans">
          ...
        </div>
      </div>
    );
  }
}

然后React会再次正常工作。

就我所知,这种包装div的方法相当古老。更现代的方法是使用React片段

至于为什么存在这样的规则,我一直认为它类似于JavaScript的普通函数,所以这样做是没有意义的:

function func() {
  return 1;
  return 2;
}

但如果我们真的想要返回1和2,我们会将它们包装成数组之类的东西:

function func() {
  return [1, 2];
}
英文:

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:

class Chosen extends React.Component {
  render() {
    return (
      &lt;div&gt;        
        &lt;select className=&quot;Chosen-select&quot; ref={el =&gt; this.el = el}&gt;          
          {this.props.children}
        &lt;/select&gt;
      &lt;/div&gt;
    );
  }
}

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:

class Chosen extends React.Component {
  render() {
    return (
      &lt;div&gt;        
      &lt;/div&gt;
      &lt;div&gt;
      &lt;/div&gt;
    );
  }
}

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:

class Chosen extends React.Component {
    render() {
        return (
            &lt;div&gt;
                &lt;div&gt;
                &lt;/div&gt;
                &lt;div&gt;
                &lt;/div&gt;
            &lt;div&gt;
        );
    }
}

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:

class Chosen extends React.Component {
  render() {
    return (     
        &lt;select className=&quot;Chosen-select&quot; ref={el =&gt; this.el = el}&gt;          
          {this.props.children}
        &lt;/select&gt;
    );
  }
}

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:

class Chosen extends React.Component {
  render() {
    return (  
        &lt;div class = &quot;chosen-select-label&quot;&gt;My select&lt;/div&gt;   
        &lt;div class = &quot;chosen-select&quot;&gt;
          &lt;select&gt;
            &lt;option&gt;&lt;/option&gt;
            &lt;option&gt;&lt;/option&gt;
          &lt;/select&gt;
        &lt;/div&gt;
        &lt;div class = &quot;some-other-chosen-select-shenanigans&quot;&gt;
          ...
        &lt;/div&gt;
    );
  }
}

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:

class Chosen extends React.Component {
  render() {
    return (
      &lt;div&gt;
        &lt;div class = &quot;chosen-select-label&quot;&gt;My select&lt;/div&gt;   
        &lt;div class = &quot;chosen-select&quot;&gt;
          &lt;select&gt;
            &lt;option&gt;&lt;/option&gt;
            &lt;option&gt;&lt;/option&gt;
          &lt;/select&gt;
        &lt;/div&gt;
        &lt;div class = &quot;some-other-chosen-select-shenanigans&quot;&gt;
          ...
        &lt;/div&gt;
      &lt;/div&gt;
    );
  }
}

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:

function func() {
  return 1;
  return 2;
}

If we really want to return 1 and 2 though, we would wrap them into something such as an array:

function func() {
  return [1, 2];
}

答案2

得分: -1

React 不知道在 React 之外对 DOM 所做的更改。它根据其内部表示确定更新,如果另一个库操作相同的 DOM 节点,React 会感到困惑,无法恢复。

这并不意味着将 React 与其他影响 DOM 的方式组合是不可能的,甚至不一定是具有挑战性的,但您必须注意每个库正在做什么。

避免冲突的最简单方法是阻止 React 组件更新。您可以通过渲染 React 没有理由编辑的元素来实现这一点,比如一个空的 <div />

解决问题的方法

为了演示这一点,让我们草拟一个用于通用 jQuery 插件的包装器。

我们将附加一个 ref 到根 DOM 元素。在 componentDidMount 中,我们将获取对它的引用,以便可以将它传递给 jQuery 插件。

为了在挂载后阻止 React 触摸 DOM,我们将从 render() 方法返回一个空的 <div /><div /> 元素没有属性或子元素,因此 React 没有理由更新它,从而使 jQuery 插件可以自由地管理 DOM 的那部分:

class SomePlugin extends React.Component {
  componentDidMount() {
    this.$el = $(this.el);
    this.$el.somePlugin();
  }

  componentWillUnmount() {
    this.$el.somePlugin('destroy');
  }

  render() {
    return <div ref={el => this.el = el} />;
  }
}

解决方案摘自 React 官方文档:
React 文档

英文:

React is unaware of changes made to the DOM outside of React. It determines updates based on its internal representation, and if the same DOM nodes are manipulated by another library, React gets confused and has no way to recover.
This does not mean it is impossible or even necessarily challenging to combine React with other ways of affecting the DOM, you have to be mindful of what each is doing.
The easiest way to avoid conflicts is to prevent the React component from updating. You can do this by rendering elements that React has no reason to edit, like an empty <div />.

How to Approach the Problem

To demonstrate this, let’s sketch out a wrapper for a generic jQuery plugin.

We will attach a ref to the root DOM element. Inside componentDidMount, we will get a reference to it so we can pass it to the jQuery plugin.

To prevent React from touching the DOM after mounting, we will return an empty <div /> from the render() method. The <div /> element has no properties or children, so React has no reason to update it, leaving the jQuery plugin free to manage that part of the DOM:

class SomePlugin extends React.Component {
componentDidMount() {
  this.$el = $(this.el);
  this.$el.somePlugin();
}

componentWillUnmount() {
  this.$el.somePlugin(&#39;destroy&#39;);
}

render() {
  return &lt;div ref={el =&gt; this.el = el} /&gt;;
}

}

Solution Copied from React Official Documentation:
React Documentation

huangapple
  • 本文由 发表于 2023年6月6日 01:11:35
  • 转载请务必保留本文链接:https://go.coder-hub.com/76408639.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定