First time using React.
Here is a demo: https://ostralyan.github.io/flood-fill/
Game Component
import React from 'react';
import Board from './Board';
import Options from './Options';
export default class Game extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.state.widthOfSquare = 10;
this.state.squaresPerRow = 50;
this.state.numberOfColors = 3;
this.state.includeDiagonals = false;
this.state.colors = this.generateColors(this.state.numberOfColors);
this.state.squares = this.generateSquares(
this.state.colors,
this.state.squaresPerRow,
this.state.numberOfColors
);
this.resetBoard = this.resetBoard.bind(this);
}
resetBoard(widthOfSquare, squaresPerRow, numberOfColors, includeDiagonals) {
const colors = this.generateColors(numberOfColors);
const state = {
widthOfSquare,
squaresPerRow,
numberOfColors,
includeDiagonals,
colors: colors,
squares: this.generateSquares(colors, squaresPerRow, numberOfColors)
}
this.setState(state);
}
generateColors(numberOfColors) {
const colors = [];
for (let i = 0; i < numberOfColors; i++) {
colors[i] = '#' + (Math.random() * 0xFFFFFF << 0).toString(16);
}
return colors;
}
generateSquares(colors, squaresPerRow, numberOfColors) {
const squares = []
for(let i = 0; i < squaresPerRow; i++) {
squares[i] = [];
for(let j = 0; j < squaresPerRow; j++) {
squares[i][j] = {
color: this.getColor(colors, numberOfColors),
visited: false
}
}
}
return squares;
}
getColor(colors, numberOfColors) {
const numberBetweenZeroAndFour = Math.floor((Math.random() * numberOfColors));
return colors[numberBetweenZeroAndFour];
}
render() {
return (
<div className="game">
<div className="game-board">
<Options
onReset={this.resetBoard}
widthOfSquare={this.state.widthOfSquare}
squaresPerRow={this.state.squaresPerRow}
numberOfColors={this.state.numberOfColors}
includeDiagonals={this.state.includeDiagonals}
/>
<Board
widthOfSquare={this.state.widthOfSquare}
squaresPerRow={this.state.squaresPerRow}
numberOfColors={this.state.numberOfColors}
includeDiagonals={this.state.includeDiagonals}
squares={this.state.squares}
colors={this.state.colors}
/>
</div>
</div>
);
}
}
Board Component
import React from 'react';
import Square from './Square';
export default class Board extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
floodFillRecursive(i, j) {
const oldColor = this.props.squares[i][j].color;
const newColor = this.getUniqueRandomColor(oldColor);
const squares = this.props.squares.slice();
this.floodFillHelper(squares, i, j, oldColor, newColor);
this.clearVisisted(squares);
this.setState({ squares: squares });
}
floodFillRecursiveHelper(squares, i, j, oldColor, newColor) {
// check out of bounds
if (i < 0 || i > this.props.squaresPerRow - 1) return;
if (j < 0 || j > this.props.squaresPerRow - 1) return;
// check if it's visited
if (squares[i][j].visited) return;
// Indicate node has been visited
squares[i][j].visited = true;
// check if it's same color
if (squares[i][j].color !== oldColor) return;
// set the current color to the new color and mark node as visited.
squares[i][j].color = newColor;
// recurse through up, down, left, right boxes.
this.floodFillRecursiveHelper(squares, i + 1, j, oldColor, newColor);
this.floodFillRecursiveHelper(squares, i - 1, j, oldColor, newColor);
this.floodFillRecursiveHelper(squares, i, j + 1, oldColor, newColor);
this.floodFillRecursiveHelper(squares, i, j - 1, oldColor, newColor);
if (this.props.includeDiagonals) {
this.floodFillRecursiveHelper(squares, i + 1, j + 1, oldColor, newColor);
this.floodFillRecursiveHelper(squares, i - 1, j + 1, oldColor, newColor);
this.floodFillRecursiveHelper(squares, i + 1, j + 1, oldColor, newColor);
this.floodFillRecursiveHelper(squares, i - 1, j - 1, oldColor, newColor);
}
}
floodFillIterative(i, j) {
const oldColor = this.props.squares[i][j].color;
const newColor = this.getUniqueRandomColor(oldColor);
const squares = this.props.squares.slice();
const stack = [
[i, j]
];
while (stack.length) {
const squareCoordinates = stack.pop();
let newI = squareCoordinates[0];
let newJ = squareCoordinates[1];
if (newI < 0 || newI >= this.props.squaresPerRow) continue;
if (newJ < 0 || newJ >= this.props.squaresPerRow) continue;
let nextSquare = squares[newI][newJ];
if (nextSquare.color !== oldColor) continue;
if (nextSquare.visited) continue;
Array.prototype.push.apply(stack, [
[newI - 1, newJ],
[newI + 1, newJ],
[newI, newJ - 1],
[newI, newJ + 1],
]);
if (this.props.includeDiagonals) {
Array.prototype.push.apply(stack, [
[newI - 1, newJ - 1],
[newI + 1, newJ - 1],
[newI - 1, newJ + 1],
[newI + 1, newJ + 1],
]);
}
nextSquare.visited = true;
nextSquare.color = newColor;
}
this.setState({ squares });
this.clearVisisted(squares);
}
getUniqueRandomColor(color) {
const numberBetweenZeroAndFour = Math.floor((Math.random() * this.props.numberOfColors));
if (color === this.props.colors[numberBetweenZeroAndFour]) {
return this.getUniqueRandomColor(color);
} else {
return this.props.colors[numberBetweenZeroAndFour];
}
}
clearVisisted(squares) {
for (let i = 0; i < squares.length; i++) {
for (let j = 0; j < squares[i].length; j++) {
squares[i][j].visited = false;
}
}
}
renderSquare(i, j) {
return <Square
color={this.props.squares[i][j].color}
onClick={() => this.floodFillIterative(i, j)}
widthOfSquare={this.props.widthOfSquare}
key={i + "," + j}
/>;
}
createTable() {
let table = []
for (let i = 0; i < this.props.squaresPerRow; i++) {
let children = []
// Inner loop to create children
for (let j = 0; j < this.props.squaresPerRow; j++) {
children.push(this.renderSquare(i, j))
}
// Create the parent and add the children
table.push(<div className="board-row" key={i}>{children}</div>)
}
return table
}
render() {
return (
<div>
{this.createTable()}
</div>
);
}
}
Options Component
import React from 'react';
export default class Options extends React.Component {
constructor(props) {
super(props)
this.state = {};
this.state.widthOfSquare = this.props.widthOfSquare
this.state.squaresPerRow = this.props.squaresPerRow
this.state.numberOfColors = this.props.numberOfColors
this.state.includeDiagonals = this.props.includeDiagonals
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
handleSubmit(event) {
this.props.onReset(
this.state.widthOfSquare,
this.state.squaresPerRow,
this.state.numberOfColors,
this.state.includeDiagonals,
);
event.preventDefault();
}
render() {
return (
<div>
<p>
Instructions: Click on any square.<br></br>
<a href="https://github.com/ostralyan/flood-fill" target="_blank" rel="noopener noreferrer">Written by Luke Xu.</a>
</p>
<form onSubmit={this.handleSubmit}>
<label>
Width of square:
<input type="number" name="widthOfSquare" value={this.state.widthOfSquare} onChange={this.handleChange} />
</label>
<br></br>
<label>
Squares per row:
<input type="number" name="squaresPerRow" value={this.state.squaresPerRow} onChange={this.handleChange} />
</label>
<br></br>
<label>
Number of colors:
<input type="number" name="numberOfColors" value={this.state.numberOfColors} onChange={this.handleChange} />
</label>
<br></br>
<label>
Include diagonals:
<input
name="includeDiagonals"
type="checkbox"
checked={this.state.includeDiagonals}
onChange={this.handleChange} />
</label>
<br></br>
<input type="submit" value="Reset" />
</form>
<br></br>
</div>
);
}
}
Square Component
import React from 'react';
export default class Square extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
shouldComponentUpdate(nextProps) {
if (nextProps.color !== this.props.color) {
return true;
}
return false;
}
render() {
const divStyle = {
backgroundColor: this.props.color,
height: this.props.widthOfSquare + "px",
width: this.props.widthOfSquare + "px",
lineHeight: this.props.widthOfSquare + "px",
}
return (
<button
className="square"
style={divStyle}
onClick={() => this.props.onClick()}>
</button>
);
}
}
Would love to hear any feedback. Things I'm not sure about. How far up should I lift the state? Pretty much all my state lives at the highest component which kind of doesn't make sense to me. Imagine if this was a super large app then literally all my state would just sit in the Game Component. I guess that might be where redux comes in?