At the beginning of my Hacker School batch I paired with Jake on a Javascript implementation of Conway’s Game of Life, as a way to learn Javascript and HTML5 canvas:
But our code felt a bit hacky, using global state all over the place. For example, the draw
function here calls exports.game.update()
, which relies entirely on side effects to update the game state:
var draw = function() {
var update = function(){
exports.game.update();
renderGrid(exports.context, exports.game.grid, exports.WIDTH, exports.HEIGHT);
};
exports.intervalId = setInterval(update, 100);
};
So after doing a code review with Mary I
refactored
it into a more functional style to
eliminate as much global state as I could. Instead of exports.game.update()
, I created a stepGame
function that takes the current game state and returns a new one:
var startAnimate = function() {
var update = function(){
gameState = stepGame(gameState);
renderGrid(ctx, gameState, canvas.width, canvas.height);
};
intervalId = setInterval(update, 100);
};
Then I wondered how you’d implement the same interface using D3.js, so Damian and I refactored it again using SVG graphics managed by D3:
With D3 you treat the DOM kind of like a database, selecting groups of SVG elements and transformations on them:
base.render = function() {
_svg
.selectAll('g')
.selectAll('rect')
.attr('fill', function(d,i) {
if (d[2]) return 'black';
else return 'white';
})
return base;
}
Around that time I heard about Facebook’s React.js framework, and Thomas, a Hacker School alum who knew React, happened to be visiting that week. So I paired with him and refactored the code yet again to use React for managing the SVG elements:
Now the GOL grid was a React component (a Board
),
containing individual Cell
components. Each step the board is created entirely anew with fresh
cells whose color is determined by the state of the Board
component:
var Board = React.createClass({
...
render: function() {
var a = [];
for (var row=0; row<50; row++) {
for (var col=0; col<50; col++) {
a.push(<Cell
dim='10' col={col} row={row}
key={row + ',' + col}
fill={this.state[[row, col]].val === 1 ? 'black': 'white'}
handleCellClicked={this.handleCellClicked}
/>);
}
}
return (
<div>
<input id="startButton" type="button" value="start" onClick={this.handleStart}/>
<input id="stopButton" type="button" value="stop" onClick={this.handleStop}/>
<svg>
{a}
</svg>
</div>
)
},
...
});
Every time the Board
component’s render
method is called, React sends the returned elements to
the virtual DOM it maintains internally. The vDOM is inspected for the actual differences that
warrant DOM mutations between invocations of getNextAnimationFrame
, using an optimized algorithm
for diffing the vDOM tree.
I was really curious to see the performance difference between a naive Javascript implementation and the React implementation. So I wrote a Chrome browser extension that highlights DOM elements being dynamically changed on any web page:
For an apples-to-apples comparison of pure Javascript and React, I re-implemented the pure Javascript version again, this time
using divs for each cell on the grid. The updateGrid
function now adds a fresh div of the appropriate color for all the cells
on the grid:
var updateGrid = function(gameState) {
newGrid();
var row, col, cellId, rowColDiv;
for (row = 0; row < gameState.dim; row ++) {
for (col = 0; col < gameState.dim; col ++) {
cellId = "r" + row + "c" + col;
rowColDiv = document.getElementById(cellId);
if (gameState.grid[row][col].val === 1) {
rowColDiv.style.backgroundColor = "black";
} else if (gameState.grid[row][col].val === 0) {
rowColDiv.style.backgroundColor = "white";
}
}
}
};
… where the newGrid
function wipes the old grid and re-adds fresh divs each time:
var newGrid = function() {
var row, col, rowDiv;
gameGrid = document.getElementById("grid");
// remove existing rows
if (document.getElementById("r" + 0)) {
for (row = 0; row < gridSize; row ++) {
rowDiv = document.getElementById("r" + row);
gameGrid.removeChild(rowDiv);
}
}
...
gameGrid.style.width = divSize * gridSize;
// add 50 child divs for each row, with row ids 0-49
for (row = 0; row < gridSize; row ++) {
rowDiv = document.createElement('div');
...
rowDiv.appendChild(rowColDiv);
}
gameGrid.appendChild(rowDiv);
}
};
Then I did conceptually the same implementation in React, only I’m adding fresh Cell
components to the vDOM
each game step, where the Cell
component is really just a wrapper around a div (as before it was just a
wrapper around an svg rect
):
var Cell = React.createClass({
render: function() {
var dim = this.props.dim;
var col = this.props.col;
var row = this.props.row;
var leftAlignQ = col === 0 ? true : false;
var style = {
width: 10,
height: 10,
backgroundColor: this.props.color,
border: "solid gray 1px",
"float": "left",
clear: leftAlignQ ? "both" : "none"
};
return (
<div width = {dim} height = {dim} style = {style} >
</div>
);
}
});
At each step, the Board
component is created anew with a fresh set of Cell
components:
var Board = React.createClass({
startAnimate: function() {
var update = function(){
this.setState(stepGame(this.state));
}.bind(this);
intervalId = setInterval(update, 100);
},
stopAnimate: function() {
clearInterval(intervalId);
},
getInitialState: function () {
return {grid: randomGameGrid(gridSize)};
},
render: function() {
var a = [];
for (var row=0; row<50; row++) {
for (var col=0; col<50; col++) {
a.push(<Cell
dim = '10' col = {col} row = {row}
color = {this.state.grid[row][col].val === 1 ? "black" : "white"}
/>);
}
}
return (
<div>
<input id="startButton" type="button" value="start" onClick={this.startAnimate}/>
<input id="stopButton" type="button" value="stop" onClick={this.stopAnimate}/>
<div width= {divSize * gridSize}>
{a}
</div>
</div>
)
}
});
Since I’m creating a new div for each cell on each time step in the pure Javascript version, the DOM mutation visualizer highlights every cell on every step:
But in the React version, only the Cell
components whose data actually change from step to step
are updated in the DOM:
This is an apples-to-apples comparison between Javascript and React, because I’m re-creating a fresh set of cells at each game step in both cases. In the pure JS version, I’m dumping a whole new set of divs into the DOM on each step. In the React version, I’m dumping a whole new set of React components in the vDOM on each step.
I remember Brian explaining this idea to me, that with React you just dump all new components into the vDOM on every step, and let React take care of figuring out what needs to change in the DOM. That was when I felt like I was starting to get the point of React, and why it’s powerful. I hope this little Game of Life demo makes that idea clear for others as well.