87

I've written some code to render repeating elements in ReactJS, but I hate how ugly it is.

render: function(){
  var titles = this.props.titles.map(function(title) {
    return <th>{title}</th>;
  });
  var rows = this.props.rows.map(function(row) {
    var cells = [];
    for (var i in row) {
      cells.push(<td>{row[i]}</td>);
    }
    return <tr>{cells}</tr>;
  });
  return (
    <table className="MyClassName">
      <thead>
        <tr>{titles}</tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
} 

Is there a better way to achieve this?

(I would like to embed for loops within the template code, or some similar approach.)

isherwood
  • 58,414
  • 16
  • 114
  • 157
fadedbee
  • 42,671
  • 44
  • 178
  • 308

5 Answers5

133

You can put expressions inside braces. Notice in the compiled JavaScript why a for loop would never be possible inside JSX syntax; JSX amounts to function calls and sugared function arguments. Only expressions are allowed.

(Also: Remember to add key attributes to components rendered inside loops.)

JSX + ES2015:

render() {
  return (
    <table className="MyClassName">
      <thead>
        <tr>
          {this.props.titles.map(title =>
            <th key={title}>{title}</th>
          )}
        </tr>
      </thead>
      <tbody>
        {this.props.rows.map((row, i) =>
          <tr key={i}>
            {row.map((col, j) =>
              <td key={j}>{col}</td>
            )}
          </tr>
        )}
      </tbody>
    </table>
  );
} 

JavaScript:

render: function() {
  return (
    React.DOM.table({className: "MyClassName"}, 
      React.DOM.thead(null, 
        React.DOM.tr(null, 
          this.props.titles.map(function(title) {
            return React.DOM.th({key: title}, title);
          })
        )
      ), 
      React.DOM.tbody(null, 
        this.props.rows.map(function(row, i) {
          return (
            React.DOM.tr({key: i}, 
              row.map(function(col, j) {
                return React.DOM.td({key: j}, col);
              })
            )
          );
        })
      )
    )
  );
} 
isherwood
  • 58,414
  • 16
  • 114
  • 157
Ross Allen
  • 43,772
  • 14
  • 97
  • 95
  • Nice catches. I added the missing parenthesis at the top and switched from `for` to a `map`. Since I don't know what the data is in each of the data structures, I assumed the rows are arrays and used the index of the iteration for the `key`. When data is added/removed from the array, that will cause unnecessary re-rendering. You should switch `key` to a value that uniquely identifies the data and that is not dependent on the order and/or size of the array. – Ross Allen Sep 03 '14 at 17:16
  • @ssorallen Thanks for your answer. I'll leave this question open for a couple of days, in case there is a nicer way of doing this, before accepting your answer. I think my issue is that JSX does not seem to have a non-expression escape, for `for` loops and similar, nor does it have template syntax for loops. – fadedbee Sep 04 '14 at 07:40
  • 1
    @chrisdew JSX is not a template language, it's syntactic sugar for plain old JavaScript. You won't get operators like you expect from template languages. Try pasting the code in this answer into the the live [JSX compiler](https://facebook.github.io/react/jsx-compiler.html) to understand what it's doing and why a `for` loop would never be possible. – Ross Allen Sep 04 '14 at 21:22
  • Could you add onClick event for each row and pass the row item to the function? I am trying something like this but I am getting Uncaught TypeError: Cannot read property 'clickHandler' of undefined clickHandler() { console.log('on click row'); } {this.props.rows.map(function(row, i) { return ( {row.map(function(col, j) { return {col}; })} ); })} – Kiran Mar 09 '17 at 21:09
  • Never mind I have added .bind(this) to the map method then the method is getting involved for each row. Thank you. – Kiran Mar 09 '17 at 22:20
20

Since Array(3) will create an un-iterable array, it must be populated to allow the usage of the map Array method. A way to "convert" is to destruct it inside Array-brackets, which "forces" the Array to be filled with undefined values, same as Array(N).fill(undefined)

<table>
    { [...Array(3)].map((_, index) => <tr key={index}/>) }
</table>

Another way would be via Array fill():

<table>
    { Array(3).fill(<tr/>) }
</table>

⚠️ Problem with above example is the lack of key prop, which is a must.
(Using an iterator's index as key is not recommended)


Nested Nodes:

const tableSize = [3,4]
const Table = (
    <table>
        <tbody>
        { [...Array(tableSize[0])].map((tr, trIdx) => 
            <tr key={trIdx}> 
              { [...Array(tableSize[1])].map((a, tdIdx, arr) => 
                  <td key={trIdx + tdIdx}>
                  {arr.length * trIdx + tdIdx + 1}
                  </td>
               )}
            </tr>
        )}
        </tbody>
    </table>
);

ReactDOM.render(Table, document.querySelector('main'))
td{ border:1px solid silver; padding:1em; }
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<main></main>
vsync
  • 118,978
  • 58
  • 307
  • 400
14

To expand on Ross Allen's answer, here is a slightly cleaner variant using ES6 arrow syntax.

{this.props.titles.map(title =>
  <th key={title}>{title}</th>
)}

It has the advantage that the JSX part is isolated (no return or ;), making it easier to put a loop around it.

Jonathan.
  • 53,997
  • 54
  • 186
  • 290
Jodiug
  • 5,425
  • 6
  • 32
  • 48
3

This is, imo, the most elegant way to do it (with ES6). Instantiate you empty array with 7 indexes and map in one line:

Array.apply(null, Array(7)).map((i)=>
<Somecomponent/>
)

kudos to https://php.quicoto.com/create-loop-inside-react-jsx/

Chris
  • 119
  • 1
  • 3
2

In the spirit of functional programming, let's make our components a bit easier to work with by using abstractions.

// converts components into mappable functions
var mappable = function(component){
  return function(x, i){
    return component({key: i}, x);
  }
}

// maps on 2-dimensional arrays
var map2d = function(m1, m2, xss){
  return xss.map(function(xs, i, arr){
    return m1(xs.map(m2), i, arr);
  });
}

var td = mappable(React.DOM.td);
var tr = mappable(React.DOM.tr);
var th = mappable(React.DOM.th);

Now we can define our render like this:

render: function(){
  return (
    <table>
      <thead>{this.props.titles.map(th)}</thead>
      <tbody>{map2d(tr, td, this.props.rows)}</tbody>
    </table>
  );
}

jsbin


An alternative to our map2d would be a curried map function, but people tend to shy away from currying.

Brigand
  • 84,529
  • 20
  • 165
  • 173
  • Those functions don't give the dev the opportunity to specify the `key` attribute. Using the index means that adding/removing elements will force re-rendering for objects that might not have changed. – Ross Allen Sep 03 '14 at 20:59
  • True, but 95% of the time the index based keys are sufficient. I'd rather go a little out of the way on the somewhat rare exceptions. – Brigand Sep 03 '14 at 22:24
  • Agreed, It would be easy enough to ensure that the keys are unique at any given level if it were necessary, I like it. – Ryan Ore May 02 '16 at 22:04