Skip to content

Instantly share code, notes, and snippets.

@busypeoples
Last active September 5, 2017 14:29
Show Gist options
  • Save busypeoples/63c5b93aa920fb3a98baddfad1bb5430 to your computer and use it in GitHub Desktop.
Save busypeoples/63c5b93aa920fb3a98baddfad1bb5430 to your computer and use it in GitHub Desktop.
Chapter 2: Using Flow

Back To The Basics

Chapter 2: Using Flow

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

// @flow
import React from 'react'
import { render } from 'react-dom'
type Circle = {type: 'Circle'}
type Cross = {type: 'Cross'}
type Empty = {type: 'Empty'}
type CellType
= Circle
| Cross
| Empty
type Row = [CellType, CellType, CellType]
type BoardType = [Row, Row, Row]
type Player = 0 | 1
type Just<A> = {type: 'Just', result: A}
type Nothing = {type: 'Nothing'}
type Maybe<A> = Just<A> | Nothing
type Result = Maybe<[Player, Row]>
type Status = Result | {type: 'Running'}
type State = {
board: BoardType,
player: Player,
status: Status
}
const displayCell = (cell: CellType) : string => {
switch(cell.type) {
case 'Circle': return 'O'
case 'Cross': return 'X'
default: return ''
}
}
type CellProps = {
cell: CellType,
onClick: () => void,
}
const Cell = ({cell, onClick} : CellProps) => {
return <div
style={{
float: 'left',
fontSize: '3em',
border: '1px solid #eee',
height: '150px',
width: '150px',
textAlign: 'center',
verticalAlign: '50%',
lineHeight: '150px',
}}
onClick={onClick}
>
{displayCell(cell)}
</div>
}
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>
}
// Helper functions
type ToFlatList = (list: BoardType) => Array<CellType>
const toFlatList : ToFlatList = list =>
list.reduce((xs, x) => {
return xs.concat(x)
}, [])
type ToBoard = (Array<CellType>) => BoardType
const toBaord : ToBoard = ([c1, c2, c3, c4, c5, c6, c7, c8, c9]) => {
return [
[c1, c2, c3],
[c4, c5, c6],
[c7, c8, c9]
]
}
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'}
}
const validate = (row: Maybe<Row>) : Player | null => {
if (row.type === 'Nothing') return null
const { result: [one, two, three] } = row
if ((one.type === two.type) && (one.type === three.type)) {
return one.type === 'Cross' ? 0 : one.type === 'Circle' ? 1 : null
}
return null
}
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)
}
const empty : Empty = {type: 'Empty'}
const emptyRow : Row = [empty, empty, empty]
const board : BoardType = [emptyRow, emptyRow, emptyRow]
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
}
const switchPlayer = (player: Player) : Player => {
switch(player) {
case 0: return 1
case 1: return 0
default: return 0
}
}
const isCell = (board: BoardType, index: number) : boolean => {
const list = toFlatList(board)
return list[index] !== undefined
}
type IsFinished = (board: BoardType) => boolean
const isFinished : IsFinished = board =>
toFlatList(board).reduce((xs, x) => xs && x.type !== 'Empty', true)
class TicTacToe extends React.Component<*, State> {
state = {
board: board,
status: {type: 'Running'},
player: 0,
}
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),
}
}
})
}
render() {
const {board, status} = this.state
return <div style={{ opacity: status.type === 'Running' ? 1 : 0.3 }}>
<Board board={board} updateCell={this.setCell} />
</div>
}
}
render(<TicTacToe />, document.getElementById('root'))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment