How do I wrap a React component that returns multiple table rows and avoid the "<tr> cannot appear as a child of <div>" error?

JavascriptReactjs

Javascript Problem Overview


I have a component called OrderItem that takes an object with multiple objects (at least two) inside it, and renders them as multiple rows inside a table. There will be multiple OrderItem components inside the table. The problem is that in the component's render function, I can't return multiple lines. I can only return a single component, and if I wrap them in a div, it says " <tr> cannot appear as a child of <div>"

The code looks something like this (I left some stuff out for easier readability)

Parent() {
  render() {
    return (
      <table>
        <tbody>
          {
            _.map(this.state.orderItems, (value, key) => {
              return <OrderItem value={value} myKey={key}/>
            })
          }
        </tbody>
      </table>
    )
  }
}

class OrderItem extends React.Component {
  render() {
    return (
      <div> // <-- problematic div
	    <tr key={this.props.myKey}>
	      <td> Table {this.props.value[0].table}</td>
	      <td> Item </td>
	      <td> Option </td>
	    </tr>
	    {this.props.value.map((item, index) => {
	      if (index > 0) { // skip the first element since it's already used above
	        return (
		      <tr key={this.props.myKey + index.toString()}>
		        <td><img src={item.image} alt={item.name} width="50"/> {item.name}</td>
		        <td>{item.selectedOption}</td>
		      </tr>
		    )
		  }
        })}
      </div>
    )
  }
}

Is there a way I can return those multiple rows and have them be in the same table without wrapping them in a div and getting an error? I realize I can make a separate table for each component, but that throws my formatting off a bit.

Javascript Solutions


Solution 1 - Javascript

React 16 is now here to rescue, you can now use React.Fragment to render list of elements without wrapping it into a parent element. You can do something like this:

render() {
  return (
    <React.Fragment>
      <tr>
        ...
      </tr>
    </React.Fragment>
  );
}

Solution 2 - Javascript

Yes!! It is possible to map items to multiple table rows inside a table. A solution which doesn't throw console errors and semantically is actually correct, is to use a tbody element as the root component and fill with as many rows as required.

items.map(item => (
   <tbody>
       <tr>...</tr>
       <tr>...</tr>
   </tbody>
))

The following post deals with the ethical questions about it and explains why yes we can use multiple tbody elements https://stackoverflow.com/questions/3076708/can-we-have-multiple-tbody-in-same-table

Solution 3 - Javascript

One approach is to split OrderItem into two components, moving the rendering logic into a method Parent.renderOrderItems:

class Parent extends React.Component {
  renderOrderItems() {
    const rows = []
    for (let orderItem of this.state.orderItems) {
      const values = orderItem.value.slice(0)
      const headerValue = values.shift()
      rows.push(
        <OrderItemHeaderRow table={headerValue.table} key={orderItem.key} />
      )
      values.forEach((item, index) => {
        rows.push(
          <OrderItemRow item={item} key={orderItem.key + index.toString()} />
        )
      })
    }
    return rows
  }
  render() {
    return (
      <table>
        <tbody>
          { this.renderOrderItems() }
        </tbody>
      </table>
    )
  }
}

class OrderItemHeaderRow extends React.Component {
  render() {
    return (
      <tr>
        <td> Table {this.props.table}</td>
        <td> Item </td>
        <td> Option </td>
      </tr>
    )
  }
}

class OrderItemRow extends React.Component {
  render() {
    const { item } = this.props
    return (
      <tr>
        <td>
          <img src={item.image} alt={item.name} width="50"/>
          {item.name}
        </td>
        <td>
          {item.selectedOption}
        </td>
      </tr>
    )
  }
}

Solution 4 - Javascript

It seems there is no way to wrap them cleanly, so the easier solution is to just put the whole table in the component and just have multiple tables and figure out the formatting.

Parent() {
   render() {
       return (
           {_.map(this.state.orderItems, (value, key) => {
               return <OrderItem value={value} myKey={key} key={key}/>
           })}
       )
   }
}


class OrderItem extends React.Component {
    render() {
        return (
            <table>
                <tbody>
                   <tr>
                       <td> Table {this.props.value[0].table}</td>
                       <td> Item </td>
                       <td> Option </td>
                    </tr>
                    {this.props.value.map((item, index) => {
                        if (index > 0) { // skip the first element since it's already used above
                            return (
                                <tr key={this.props.myKey + index.toString()}>
                                    <td> <img src={item.image} alt={item.name} width="50"/> {item.name}</td>  
                                    <td>{item.selectedOption}</td>
                                </tr>
                            )
                        }
                    })}
                </tbody>
            </table>
        )
    }
}

Solution 5 - Javascript

It is an old question, but maybe someone stumbles on it. Since I cannot comment yet, here is a little addition to the answer of @trevorgk:

I used this to render a table with multiple rows per item (about 1000 items resulting in about 2000 rows with 15 columns) and noticed really bad performance with Firefox (even in 57).

I had pure components rendering each item (one <body> per item containing two rows each) and each item contained a (controlled) checkbox.

When clicking the checkbox Firefox took more than ten seconds to update - although only one item was actually updated due to pure components. Chrome's update took at most half a second.

I switched to React 16 and I noticed no difference. Then I used the new AWESOME!!! feature of returning an array from a component's render function and got rid of the 1000 <tbody> elements. Chrome's performance was approximately the same while Firefox's "skyrocketed" to about half a second for an update (no perceived difference to Chrome)

Solution 6 - Javascript

In my case, the solution was to return an array instead of a fragment:

    return [
        <TrHeader />,
        <TrRows />
    ];

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPedramView Question on Stackoverflow
Solution 1 - JavascriptEesaView Answer on Stackoverflow
Solution 2 - JavascripttrevorgkView Answer on Stackoverflow
Solution 3 - JavascriptJomel ImperioView Answer on Stackoverflow
Solution 4 - JavascriptPedramView Answer on Stackoverflow
Solution 5 - JavascriptMarkusView Answer on Stackoverflow
Solution 6 - JavascriptDanielView Answer on Stackoverflow