简体   繁体   中英

Changing table structure causes React to lose track of the DOM

I am trying to create a table component in React where both the columns and rows are dynamic and change over time. However, when the data changes, React throws an unexpected DOM mutation invariant violation.

Here is a fiddle demonstrating the issue http://jsfiddle.net/69z2wepo/1797/ . As you can see, the data changes after the initial render and React can no longer track the state of the DOM.

Code is here:

var Table = React.createClass({
    getDefaultProps: function() {
      return {
        data: [
          {
            colA: { value: 'foo' },
          },
        ],
      };
    },
    render: function() {
        return (
          <table>
            <thead>
              <tr> {
                Object.keys(this.props.data[0]).map(function(key) {
                  return (<th>{ key }</th>);
                }.bind(this))
              } </tr>
            </thead>
            <tbody> {
              this.props.data.map(function(item) {
                return (<tr> { Object.keys(item).map(function(key) {
                  return (
                    <td>{ item[key] }</td>
                  );
                }.bind(this)) } </tr>);
              }.bind(this))    
            } </tbody>
          </table>
        );
    }
 });

var Main = React.createClass({
  componentWillMount: function() {
    setTimeout(function() {
      var data = [
        {
          colA: {
            value: 'foobar',
          },
        },
      ];
      this.setState({
        data: data,
      });
    }.bind(this), 3000);
  },
  getInitialState: function() {
    var data = [
      {
        colA: {
          value: 'foo',
        },
        colB: {
          value: 'bar',
        }
      },
      {
        colA: {
          value: 'foo',
        },
        colB: {
          value: 'bar',
        }
      }
    ];

    return {
      data: data,
    };
  },
  render: function() {
    return (<Table data={ this.state.data } />);
  },
});

React.render(<Main />, document.body);
console.log(React.renderToString(<Table/>));

I've tried all manner of adding key attributes to track various elements and nothing seems to solve this problem.

Rendering the table component with renderToString shows that React is inserting a bunch of elements at various levels of the table. Is this the possible cause? See the DOM being rendered here:

<table data-reactid=".1" data-react-checksum="1098817301">
  <thead data-reactid=".1.0">
    <tr data-reactid=".1.0.0">
      <span data-reactid=".1.0.0.0"> </span>
      <th data-reactid=".1.0.0.1:0">colA</th>
      <span data-reactid=".1.0.0.2"> </span>
    </tr>
  </thead>
  <tbody data-reactid=".1.1">
    <span data-reactid=".1.1.0"> </span>
    <tr data-reactid=".1.1.1:0">
      <span data-reactid=".1.1.1:0.0"> </span>
      <td data-reactid=".1.1.1:0.1:0"><span data-reactid=".1.1.1:0.1:0.$value:0">foo</span></td>
      <span data-reactid=".1.1.1:0.2"> </span>
    </tr>
    <span data-reactid=".1.1.2"> </span>
  </tbody>
</table>

Turns out the problem is with your indentation. If you start curly braces {} (curly braces to write javascript inside JSX) on a new line, your code works. Not sure why it happens though.

jsfiddle: http://jsfiddle.net/cwn2nebs/

var Table = React.createClass({
    getDefaultProps: function() {
      return {
        data: [
          {
            colA: { value: 'foo' },
          },
        ],
      };
    },
    render: function() {
        return (
          <table>
            <thead>
              <tr> 
              {
                Object.keys(this.props.data[0]).map(function(key, idx) {
                  return (<th key={ idx } >{ key }</th>);
                }.bind(this))
              } 
              </tr>
            </thead>
            <tbody> 
            {
              this.props.data.map(function(item, idx) {
                return (
                <tr key={ idx }> 
                { 
                  Object.keys(item).map(function(key, i) {
                      return (
                        <td key={ i }>{ item[key] }</td>
                      );
                  }.bind(this)) } 
                </tr>);
              }.bind(this))

            } 
            </tbody>
          </table>
        );
    }
});

var Main = React.createClass({
  componentWillMount: function() {
    setTimeout(function() {
      var data = [
        {
          colA: {
            value: 'foobar',
          }
        },
      ];
      this.setState({
        data: data,
      });
    }.bind(this), 3000);
  },
  getInitialState: function() {
    var data = [
      {
        colA: {
          value: 'foo',
        },
        colB: {
          value: 'bar',
        }
      },
      {
        colA: {
          value: 'foo',
        },
        colB: {
          value: 'bar',
        }
      }
    ];

    return {
      data: data,
    };
  },
  render: function() {
    return (<Table data={ this.state.data } />);
  },
});

React.render(<Main />, document.body);

Update

Using JSX compiler I tried to convert part of your code to plain JS:

 render: function() {
        return (
          React.createElement("table", null, 
            React.createElement("thead", null, 
              React.createElement("tr", null, " ", 
                Object.keys(this.props.data[0]).map(function(key, idx) {
                  return (React.createElement("th", {key: idx }, key ));
                }.bind(this)), 
              " ")
            ), 
            React.createElement("tbody", null, " ", 
              this.props.data.map(function(item, idx) {
                return (React.createElement("tr", {key: idx }, " ",  Object.keys(item).map(function(key, i) {
                  return (
                    React.createElement("td", {key: i },  item[key] )
                  );
                }.bind(this)), " "));
              }.bind(this)), 

            " ")
          )
        );
    }

This is how React.createElement works:

React.createElement(type, props, children);

Notice the empty children for tr elements:

React.createElement("tr", null, " ", 
    Object.keys(this.props.data[0]).map(function(key, idx) {
        return (React.createElement("th", {key: idx }, key ));
    }.bind(this)), 
 " ")

But with curly brace on a new line, the compiled code looks like this (there is no empty (" ") children):

React.createElement("tr", null,     
    Object.keys(this.props.data[0]).map(function(key, idx) {
        return (React.createElement("th", {key: idx }, key ));
    }.bind(this))
)

I believe React converts " " children to span elements which is cause of the problem here, as you find out.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM