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

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.


Solution 1:

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:

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 Can we have multiple <tbody> in same <table>?

Solution 3:

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>
    )
  }
}