1

I am new to Javascript and React and have a similar question as this one:

I am following the tutorial for the tic-tac-toe game and am trying to understand mixing JSX and loops and arrays.

I have this which is working:

class Board extends React.Component {
  renderSquare(i) {
    return (
      <Square
       value={this.props.squares[i]}
       onClick={() => this.props.onClick(i)}
      />
    )
  }
  renderRow(row) {
    let ss = [];
    for(let i=0; i<3; i++)
    {
      ss.push(this.renderSquare(i+row));
    }
    return (
      <div className="board-row">
      {ss}
      </div>
    )
  }
  renderBoard() {
    let board = []
    for(let i=0; i<3; i++)
    {
      board.push(this.renderRow(i*3));
    }
    return board;

  }
  render() {
    return (
      <div>
        {this.renderBoard()}
      </div>
    )
  }
}

Basically the idea is to replace the hard coded board which was done with this:

render() {
  return (
    <div>
      <div className="board-row">
        {this.renderSquare(0)}
        {this.renderSquare(1)}
        {this.renderSquare(2)}
      </div>
      <div className="board-row">
        {this.renderSquare(3)}
        {this.renderSquare(4)}
        {this.renderSquare(5)}
      </div>
      <div className="board-row">
        {this.renderSquare(6)}
        {this.renderSquare(7)}
        {this.renderSquare(8)}
      </div>
    </div>
  );
}

What I'd like to do is effectively combine renderRow and renderBoard together in some kind of nested for loop structure. I only have the different functions at all because I don't know how to inline the nested structure of the elements to the array that I am making. Is this possible?

1
  • In this particular example I wouldn't try to force that into some fancy for loops. Also for loops don't play well with jsx and most of the time I find them hard to read. I try to avoid them as much as possible. Actually I haven't used one even a single time while creating react components all day long in a medium sized project for months. I use map() whenever possible (which is in 99% of the cases).
    – trixn
    Commented Aug 6, 2018 at 22:25

4 Answers 4

1

What you often see people do is map over some structure in order to dynamically create an array of elements. In your case, since you only have a flat list of squares instead of a proper hierarchy, you could get around by simply creating one temporarily. And then you don’t actually need all those functions.

class Board extends React.Component {
    render() {
        const indexes = [[0, 1, 2], [3, 4, 5], [6, 7, 8]];

        return (<div>
            {indexes.map(row =>
                <div class="board-row">
                    {row.map(index =>
                        <Square key={index}
                            value={this.props.squares[index]}
                            onClick={() => this.props.onClick(i)}
                            />
                    )}
                </div>
            )}
        </div>);
    }
}

This is btw. a good example where using hyperscript makes this a bit easier on the eyes, compared to JSX. I don’t want to convert you away from JSX here, but I think all that nesting and those curly braces makes it difficult to follow. If you are interested, take a look at the following snippet that uses hyperscript:

const h = React.createElement;

function Square(props) {
  return h('div', { className: 'square', onClick: props.onClick }, props.value);
}

function Board(props) {
    const indexes = [[0, 1, 2], [3, 4, 5], [6, 7, 8]];
    return h('div', null, indexes.map((row, k) =>
        h('div', { key: k, className: 'board-row' }, row.map(index =>
            h(Square, { key: index, value: props.squares[index], onClick: props.onClick.bind(null, index) } )
        ))
    ));
}


const props = {
  squares: [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i' ],
  onClick: (i) => console.log(i),
};
ReactDOM.render(h(Board, props), document.getElementById('target'));
.board-row {
  display: flex;
}
.square {
  width: 40px;
  height: 40px;
  border: 1px solid gray;
}
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="target"></div>

1
  • This is actually pretty clean. I like it. But anyways I would go with the original. It sacrifices a little bit of "don't repeat yourself" to massively improve readability. And it may even be faster. But if I decided to do it different for the sake of using loops that would be the way to do it.
    – trixn
    Commented Aug 6, 2018 at 22:34
0

Sure, try this:

renderBoard() {
  let board = []
  for (let i = 0; i < 3; i++) {
    let inner = []
    for (let j = 0; j < 3; j++) {
      inner.push(this.renderSquare(j + i));
    }
    let row = (
      <div className="board-row">
        {inner}
      </div>
    )
    board.push(row);
  }
  return board;
}

Note the things I had to do, I had to create a variable to store the row, and rather than pushing the function call, I just pushed that row into the board. Also, the inner loop uses a different variable j which replaces the row input of renderRow(row)

1
  • 2
    don't forget to add key attribute to each element in array, like this <Square key={i} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> Commented Aug 6, 2018 at 22:02
0

A shorter version of Board component

  class Board extends React.Component {
    render() {
      let row = [];
      const squaresNumbers = Array.apply(null, Array(9)).map((x, i) => i);

      return (
        <div>
          {squaresNumbers.reduce((result, i) => {
            row.push(
              <Square key={i} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />
            );
            if (row.length === 3) {
              result.push(<div className="board-row">{row}</div>);
              row = [];
            }
            return result;
          })}
        </div>
      );
    }
  }
0

Consider this: you have a range from 0 to 9, excluding the upper boundary. You want to reshape it into a 3 by 3 matrix. And afterwards, you want to map values into React component instances. Let's write the functions.

First, a simplistic range function that takes only one argument, the upper boundary, and outputs a range from 0 to that boundary, not including the latter:

const range = j => [...Array(j).keys()];

so that

> range(9)
[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]

You can look up on MDN why it works like that, but let's consider the "why" out of scope of this question for now.

Next, a very simple reshape function:

const reshape = (mx, my) => array => range(mx).map(x => range(my).map(y => array[mx * x + y]));

which works like this:

> reshape(3, 3)([1, 2, 3, 4, 5, 6, 7, 8, 9])
[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ] ]

And finally, make it render:

class Board extends React.Component {
  constructor(props, ...args) {
    super(props, ...args);

    const {
      rows = 3,
      cols = 3,
    } = props;
    const board = reshape(rows, cols)(range(rows * cols));

    this.state = {
      rows,
      cols,
      board,
    };
  }

  render() {
    const { board } = this.state;

    return (
      <div>
        {board.map((row, i) => (
          <div key={i} className="board-row">
            {row.map(cell => (
              <Square
                key={cell}
                value={cell}
                onClick={() => this.props.onClick(cell)}
              />
            ))}
          </div>
        ))}
      </div>
    );
  }
}

What's going on there?

We are doing some functional programming. We wrote a couple small functions that are not coupled to business logic. We used words like "range" and "reshape" and write function accordingly, so that almost every programmer understands without reading the corresponding source code what these functions do.

And we do some currying in reshape function. Because Javascript isn't quite friendly to variadic function declaration, we go with this weird fn()() syntax. I think it takes time to get used to it, because the upside is awesome: you begin writing little reusable functions that can be composed to create cool business logic out of very simple blocks.

And we made a few assumptions here:

  • the range function only takes one argument,
  • the reshape function only takes two arguments,
  • a lambda function returned by reshape expects an array and the semantics prescribe it to be flat,
  • we never change the board configuration after the Board component was instantiated (its constructor executed).

Either of these may be adjusted separately. You may want to use _.range from lodash, maybe make reshape function a little safer in edge cases, and such. It's up to you to optimize.

So while this answer adds a lot to think about, I do hope that you'll find it useful. Cheers!

Not the answer you're looking for? Browse other questions tagged or ask your own question.