Implement the classic Windows game Minesweeper with React.
- Clicking a mine ends the game.
- Clicking a square with an adjacent mine clears that square and shows the number of mines touching it.
- Clicking a square with no adjacent mine clears that square and clicks all adjacent squares.
- The first click will never be a mine.
- It will clear the map and place numbers on the grid.
- The numbers reflect the number of mines touching a square.
- Open Live Demo
- Adjust Board Config
- Click
Start
Left Click
to open a cellRight Click
to flag a cell (suspicious mine)
- Board Width: how many columns of a board
- Board Height: how many rows of a board
- Bomb Probability: the probability of whether a cell is a mine
- Show Log: show the log of function execution in Devtool console panel
- Purpose: a component maintaining the state of game.
- Lifecycle: uncontrolled component, remount on each round.
- State
- boardState: 2D Array of cellState, recording the current board status.
- cellState: Object type, basic unit of boardState, recording the cell status
DEFAULT_CELL_STATE = { opened: false, isBomb: false, adjBombNum: 0, flagged: false, };
- bombCount: number type, recording the mine(bomb) number.
- openedCount: number type, recording the opened cell number.
- boardState: 2D Array of cellState, recording the current board status.
- Purpose: a direct visual representation of boardState.
- Lifecycle: controlled component, re-render on props change
- State: stateless component
- Generate 2D array of board state based on
Board Width
andBoard Height
. - Each element contains a least basic properties of a cell:
opened
,isBomb
,adjBombNum
andflagged
.
iterate through every cell of board state and performs following actions respectively.
- randomly set
isBomb
totrue
based onBomb Probability
. - if set bomb, update
adjBombNum
of adjacent cells.
Flag / Unflag a cell
- Performs
setState
only once. - The corresponding new state is generated by pipeline of pure function:
handleFirstBomb
,openCell
,openAdjacentSafeCells
,openBomb
,doSideEffect
,getState
in helper.js.- handleFirstBomb: Given a boardState and cell location, modify the cell to normal cell and update adjBombNum of adjacent cells.
- openAdjacentSafeCells: Given a boardState and cell location, Using Depth-First-Search get all adjacent cells of
adjBombNum===0
, then set these cell stateopened=true
. - openCell: Given a boardState and cell location, set the given cell state
opened=true
- openBomb: Given a boardState and cell location, set the given cell state
opened=true
&background="red"
- doSideEffect: Use the information of previous function, do something then return as input. Here use to get count of openAdjacentSafeCells.
- getState: return boardState
-
Win condition
board width * board height - bombCount === opened count
-
Lose condition:
clicked a bomb && not first step
I have tried several ways of handleClickCell
for updating boardState
(list in chronological order)
-
Multiple steps of setState (not work)
Since I maintain the boardState with
useState
, the first and naive implementation ofhandleClickCell
is performing multiple steps ofsetState
(ex. handleFirstBomb, openAdjacentSafeCells...). It did not worked because each step relies on the result of previous step andsetState
of React does not work synchronously. -
Single setState with pipeline of pure functions (commits after: refactor: functions into pure function)
Instead of multiple
setState
, I refactoredhandleClickCell
into single setState function with pipeline of pure functions executed inside updater function of setState.It is the first working version
. -
Performance Optimization: State management with useReducer (commits after refactor: useReducer)
Since
render
time increase as board size grows. I thought about the native characteristics of React functional component, the Re-render of each Cell happenswhenever boardState change
, even for the unchanged cells. Unnecessary re-render can slow down the re-render process. React provideuseCallback
hook andmemo
HOC for performance optimization. I expected performance optimization by reducing the unnecessary re-render.To utilize
memo
HOC, the goal here is to distinguish and compare if the props ofCell
unchanged.The primitive type of props (ex.
isBomb
: boolean,adjBombNum
: number) can be directly compared using equal operator. The trickiest of the part ishandleClickCell
because function recreate whenever state update and it use boardState directly, which means it must be recreate to get the latest boardState on each re-render.dispatch
won't change between re-renders (reference)In order to remove the dependency of boardState inside
handleClickCell
. I replaced theuseState
withuseReducer
and perform state change inside reducer. In that way thehandleClickCell
only depends on staticdispatch
andactions
, which means all theonClick
of Cell propsis essentially same
anddoes not change on re-render
. Then I can easily memoize the same reference of it withuseCallback
hook and wrapCell
withmemo
HOC for preventing unnecessary re-render of Cell.After the refactoring, I inspected the performance with Chrome devtools. I found out that performance was not improved and the bottleneck actually lies in
unstable_runWithPriority
ofReact scheduler
, not the render process ofCell
. TheCell
itself maybe too simple to affect the performance. I should have noticed that before refactoring!It would be interesting to compare React project with other examples made with pure javascript (without framework). For example: Hedronium/minesweeper, it directly manipulate DOM tree. Only with rough comparison, under the same Board configuration, performance does not have significant difference compared to this project.
When bombProbability
is low (ex.0.01) , e.g. lots of safe cells, the recursive method of findAdjacentSafeCells
is prone to Maximum call stack size exceeded error
. Common technique to prevent recursion from call stack size exceed is to push recursion into macro task using setTimeout
. Since it is called inside setState, which should be synchronous and pure, setTimeout
does not work here.
To solve the Maximum call stack exceed
error encountered above, I refactored the recursion method into iterative BFS.