Back To The Basics: Using React + Flow Pt.2

Chapter 2: Using React + Flow

A. Sharif
JavaScript Inside
9 min readSep 4, 2017

--

This is a brand new series aimed at focusing on advanced concepts when using React or React-Like libraries. Planned topics include testing, advanced state handling and other topics relevant to building modern Front-End Applications. The reader should have a basic understanding of the fundamental React principles and sufficient knowledge of modern Front-End Development basics.

Introduction

Why does it make sense to use FlowType or TypeScript when working with JavaScript? To give an appropriate answer, the best idea would be to build a small game or application to make the benefits clear.

This is part 2 of “Flow with React”. In part 1 we were able to build a basic board that renders 3 rows containing 3 cells each. Now it’s time to build upon what we have, and start structuring our TicTacToe game and add interactivity.

Refactoring

It’s time to refactor our single component and create a Board and Cell component.

The refactoring is nothing special or noteworthy, but for the sake of completeness here’s how our components are structured now.

const Cell = ({ cell: CellType }) => {
return <div style={{
float: 'left',
textAlign: 'center',
border: '1px solid #eee',
padding: '75px'
}}>
cell
</div>
}
const Board = ({ board: BoardType }) : React$Element<any> => {
return <div>
{board.map((row, i) => {
return <div style={{width: '600px', height: '150px'}} key={i}>
{row.map((cell, j) => <Cell key={j} cell={cell} /> )}
</div>
})}
</div>
}
class TicTacToe extends React.Component<*, State> {
state = {
board: board,
status: {type: 'Running'},
player: 0,
}
render() {
const {board} = this.state
return <div>{
<Board board={board} />
}</div>
}
}

Advanced

So we have broken our app into multiple components and did a little renamng to avoid some name clashing. We renamed Board type to simply BoardType and the Cell to CellType. Now that we have manually restructured our game, it's time to move on to the more interesting tasks. We're still rendering 'cell' to the screen. But what we actually want to do, is render the correct representation, i.e. a circle or a cross.

Because we know about the type that will be passed in, we can display appropriate visual representation. Let’s write a small function that recieves the Cell and returns a string.

const displayCell = (cell: CellType) : string => {
switch(cell.type) {
case ' Circle': return 'O'
case 'Cross': return 'X'
default: return ''
}
}

We can quickly test our displayCell function to verify it works as expected.

console.log('X' === displayCell({type: 'Cross'}))

And for clarity, this is how our Cell component looks like now:

type CellProps = {
cell: CellType,
}
const Cell = ({cell} : CellProps) => {
return <div style={{
float: 'left',
textAlign: 'center',
border: '1px solid #eee',
padding: '75px'
}}>
{displayCell(cell)}
</div>
}

Our next task is to add interactivity, otherwise the game is unusable. Let’s also recap what we actually need to do:

  • User can click on a cell, if the cell is empty we either render a circle or a cross.
  • Everytime a cell is updated, users switch.
  • If a row or a column or a diagonal has the same type (cirlce or cross), there is a winner and the game ends.
  • If all cells are filled and there is no winner up to this point, then the game is a tie.

What we can see is that there are a number of possible combinations we need to keep track of.

To get things going, we’ll focus on the player switching part.

const switchPlayer = (player: Player) : Player => {
switch(player) {
case 0: return 1
case 1: return 0
default: return 0
}
}

We pass in a Player and we return a Player. Continuing, we will need to implement an update function that will update a cell. So how can tackle this in a sane manner?

We have a Board type, which is modelled as 3x3 cells, that means if we wanted to update the top left cell, we could access it via board[0][0] and the right bottom cell via board[3][3]. Another uproach is transform between the 3x3 board and a flat list.

// Helper functions
const toFlatList : (list: BoardType) => Array<CellType> = list =>
list.reduce((xs, x) => {
return xs.concat(x)
}, [])
const toBaord : (Array<CellType>) => BoardType = ([c1, c2, c3, c4, c5, c6, c7, c8, c9]) => {
return [
[c1, c2, c3],
[c4, c5, c6],
[c7, c8, c9]
]
}

We will leverage these two functions and transform the data forth and back when needed. For example we can now update a cell by just knowing about the index. This will simplify things significantly. Ofcourse we could also take the other route, mainly being having to define a column type, and switching over the row and then the column, but this can come with some significant overhead. Our current implementation should be suitable. Let’s implement a function that updates a cell.

const updateCell = (board: BoardType, player: Player, index: number) : BoardType => {
const cells = toFlatList(board)
const cell = cells[index]
if (cell && cell.type === 'Empty') {
const updatedCell : Circle | Cross = player === 0 ? {type: 'Cross'} : {type: 'Circle'}
return toBaord([...cells.slice(0, index), updatedCell, ...cells.slice(index + 1)])
}
return board
}

We convert the passed in board to a flat list and access the passed in index. If the Cell type is `Èmpty' we update the cell with the right type depending on the defined player. There is no magic involved here. Only a function that always returns a board, and updates the board if an update is possible. Further more we can easily test this function, but will leave this as a task to the interested reader.

Our next step is to create a function that is triggered when the player clicks on the cell. Also, we should keep in mind that if a player clicks a filled cell, nothing should happen.

const isCell = (board: BoardType, index: number) : boolean => {
const list = toFlatList(board)
return list[index] !== undefined
}

isCell checks if the actual cell exists on the board, which will get called when wanting to update the actual state. Only when valid, will we a actually call setState with the updated board and player. Adding a setCell method to our TicTacToe class and passing this method down to the actual cell should be enough to display the correct cell state.

class TicTacToe extends React.Component<*, State> {
...
setCell = (index: number) : void => {
this.setState(state => {
const {board, player} = state
return isCell(board, index)
? {
player: player === 0 ? 1 : 0,
board: updateCell(board, player, index),
}
: {}
})
}
render() {
const {board} = this.state
return <div>{
<Board board={board} updateCell={this.setCell} />
}</div>
}
}

Now we all need to do, is pass the newly defined method via the Board component to the Cell. One important aspect to note is that we’re calculating the cell index on the fly here onClick={() => updateCell(i*3 + j). As earlier mentioned, we could also change the implementation and define column types as well, accessing the cells via board[0][0] i.e. If you have time and interest and try to implement in this way.

type BoardProps = {
board: BoardType,
updateCell: (i: number) => void
}
const Board = ({board, updateCell} : BoardProps) : React$Element<any> => {
return <div>
{board.map((row, i) => {
return <div style={{width: '600px', height: '150px'}} key={i}>
{row.map((cell: CellType, j) =>
<Cell
key={j}
cell={cell}
onClick={() => updateCell(i*3 + j)}
/>
)}
</div>
})}
</div>
}

Finally, our Cell component calls this function via onClick. Here is our updated Cell component, including some minor style changes.

type CellProps = {
cell: CellType,
onClick: () => void,
}
const Cell = ({cell, onClick} : CellProps) => {
return <div
style={{
float: 'left',
textAlign: 'center',
fontSize: '3em',
border: '1px solid #eee',
height: '150px',
width: '150px',
textAlign: 'center',
verticalAlign: '50%',
lineHeight: '150px',
}}
onClick={onClick}
>
{displayCell(cell)}
</div>
}

Clicking on a cell will update the cell incase it’s empty.

We’re getting closer to finalizing this game. What is left to do? Up untill now, we don’t know if the game has ended and if there is an actual winner. Checking if the game is over, can be achieved by checking if there is an Empty cell left.

type IsFinished = (board: BoardType) => boolean
const isFinished : IsFinished = board =>
toFlatList(board).reduce((xs, x) => xs && x.type !== 'Empty', true)

All we need to do is reduce over the flatted list and check if there is an empty cell left.

Continuing to the validation part: we want to know if a row, or a column or a diagonal contain the same type, eihter being a cross or a cirlce.

Because we choose to convert between the 3 x 3 board and flat list, we can convert any combination of indexes to a Row.

Let’s define the possible combinations we need to check for:

const row1 = [0, 1, 2]
const row2 = [3, 4, 5]
const row3 = [6, 7, 8]
const col1 = [0, 3, 6]
const col2 = [1, 4, 7]
const col3 = [2, 5, 8]
const diag1 = [0, 4, 8]
const diag2 = [2, 4, 6]
const rows = [row1, row2, row3, col1, col2, col3, diag1, diag2]

So our rows contains all possibele combinations. Everything converted into row, no matter if it's an actual row or not.

Next, we need a function that can pick any values from a given list and return a Row.

const pick = (selection: Array<number>, board: BoardType) : Maybe<Row> => {
const flatlist : Array<CellType> = toFlatList(board)
const result = selection.reduce((xs, x) => {
const cell : ?CellType = flatlist[x]
return cell ? [...xs, cell] : xs
}, [])
const [c1, c2, c3] = result
if (c3.length === 3) {
return {type: 'Just', result: [c1, c2, c3]}
}
return {type: 'Nothing'}
}

Our pick function will take care of returning a Row. Once we have a row we can validate the cells by checking if the share the same type.

const validateRow = (row: Maybe<Row>) : Player | null => {
if (row.type === 'Nothing') return null
const [one, two, three] = row.result
if ((one.type === two.type) && (one.type === three.type)) {
return one.type === 'Cross' ? 0 : one.type === 'Circle' ? 1 : null
}
return null
}

There is not really too much to say about our validateRow function, except that we return a player or null as a result. Which means that we can now which player won, by checking if the same type is a cross or a cirlce and mapping it back to the player.

To wrap this all up we need to connect our validateRow function with the previously defined possible row combinations. We can write an isWinner function that accepts the board and runs all the possible combinations against the validateRow function. As soon as we have a validRow, we also have a winner. Technically reducing over the row combinations should suffice. By simply returning a player and winning row tuple, we can later display this information on the screen.

type IsWinner = (board: BoardType) => [Player, Row] | false
const isWinner: IsWinner = (board, player) => {
const row1 = [0, 1, 2]
const row2 = [3, 4, 5]
const row3 = [6, 7, 8]
const col1 = [0, 3, 6]
const col2 = [1, 4, 7]
const col3 = [2, 5, 8]
const diag1 = [0, 4, 8]
const diag2 = [2, 4, 6]
const rows : Array<Array<number>> =
[row1, row2, row3, col1, col2, col3, diag1, diag2]

return rows.reduce((selected, selection) => {
if (selected) return selected
const row : Maybe<Row> = pick(selection, board)
if (row.type === 'Nothing') return selected
const winner = validate(row)
if (winner !== null) {
return [winner, row.result]
}
return false
}, false)
}

Finally, we will also need to call the ìsFinishedandisWinnerfunctions at the appropriate place. Let's update our previously definedsetCell` method.

setCell = (index: number) : void => {
this.setState(state => {
const {board, player} = state
if (!isCell(board, index)) return {}
const updatedBoard = updateCell(board, player, index)
const winner = isWinner(updatedBoard)
if (winner) {
return {
board: updatedBoard,
status: {type: 'Just', result: winner},
}
} else if (isFinished(updatedBoard)) {
return {
board: updatedBoard,
status: {type: 'Nothing'}
}
} else {
return {
board: updatedBoard,
player: switchPlayer(player),
}
}
})
}

There is alot going on here. We go through several steps: first we check if the move is valid. If it is valid, we then check if we have a winner, and if not, we check if the game has a winner. You might refacor this, or move the isWinner and isFinished checks to the ComponentDidUpdate method. Feel free to experimen.

We have an actual TicTacToe game now. There are still some more refinements needed, but out of scope of this write up. If you’re interested in finalizing the game, here are some ideas:

  • prevent any clicks after the game has ended or in case there is a winner.
  • Display the current player.
  • Display the game status.
  • Highlight the winning combination.

If you have any further questions or insights please provide feedback via Twitter

--

--

A. Sharif
JavaScript Inside

Focusing on quality. Software Development. Product Management.