Tic Tac Toe Ep 3: Redux Undo

This is a 3 parts serious we will cover from smooth UI/UX, undo step, different ways to build AI, from easy to unbeatable. Complete source code at GitHub, if it help you place a stars.
Introduction
After first 2 parts, we created a playable Tic tac toe game. In part 3, we want want to leverage Redux to make a undo move and history recording.
Final Product in Part 3
Rollback
Everytime the player move, placeMark is dispatched, we also dispatch updateHistory, we will only store the current gameboard and the current player mark, so that our history record wont growth to too large.
History Slice
We will first define a type History and interface HistoryState for the History Slice.
export type History = {
board: Cell[]
}
export interface HistoryState {
past: History[]
}We draft out the reducer
initialHistory: Restart GameupdateHistory: Save game history for each move
export const historySlice = createSlice({
name: "history",
initialState,
reducers: {
initialHistory: (state) => {
state.past = initialState.past
state.rollBackStep = initialState.rollBackStep
},
updateHistory: (state, { payload }: PayloadAction<History>) => {
if (state.rollBackStep !== state.past.length - 1)
state.past = state.past.slice(0, state.rollBackStep + 1)
state.past.push(payload)
state.rollBackStep += 1
},
},
})Recover From History with Thunk
We will export a function for recover from History, We will use Thunk, so that we can do dispatch and getState from the other slice.
// historySlice...
export const recoverFromHistory =
(step: number): AppThunk =>
(dispatch, getState) => {
const historyState = getState().history
if (step < 0 || step >= historyState.past.length) {
console.error("Invalid step number")
return
}
const rollbackRecord = getState().history.past[step]
dispatch(moveHistoryPointer(step))
dispatch(rollbackGameBoard(rollbackRecord))
}rollbackGameBoard is a reducer that help use to replace the history record to current gameboard.
// ticTacToeSlice.ts
// other reducers...
rollbackGameBoard: (
state,
{ payload }: PayloadAction<{ board: Cell[]; currentRound: Marks }>
) => {
state.board = payload.board
state.currentRound = payload.currentRound
},HistoryList
We iterate through the records in the list to RollBackButton. Once we click on the button, it will dispatch the thunk recoverFromHistory(index) and update the gameboard instanly.
import styled from "styled-components"
import { useAppDispatch, useAppSelector } from "../../app/hooks"
import { selectResult } from "../tictactoe/ticTacToeSlice"
import {
recoverFromHistory,
selectHistories,
selectRollBackStep,
} from "./historySlice"
const SectionWrapper = styled.section`
margin-bottom: 0.25rem;
font-weight: 600;
color: var(--text-color);
`
const Heading = styled.p`
margin-bottom: 0.25rem;
font-weight: 600;
text-align: left;
color: var(--text-color);
`
const List = styled.div`
overflow-y: auto;
display: flex;
padding: 0.5em;
/* border: 1px var(--background-color-darker) solid; */
border-radius: 5px;
flex-direction: column;
margin-bottom: 0.25rem;
font-weight: 600;
row-gap: 8px;
height: 280px;
align-items: flex-end;
color: var(--text-color);
`
const RollBackButton = styled(({ ...props }) => <button {...props} />)`
border: 1px var(--background-color-darker) solid;
border-radius: 5px;
font-weight: 600;
height: 32px;
background-color: transparent;
width: 100%;
cursor: pointer;
width: ${(props) => (props.current ? "80%" : "100%")};
color: var(--text-color);
transition: all ease-in-out;
animation-duration: 500ms;
`
function HistoryList() {
const dispatch = useAppDispatch()
const histories = useAppSelector(selectHistories)
const currentStep = useAppSelector(selectRollBackStep)
const result = useAppSelector(selectResult)
const onClickHandler = (index: number) => {
if (result === null) {
dispatch(recoverFromHistory(index))
}
}
return (
<SectionWrapper>
<Heading>History</Heading>
<List>
{histories.map((_, index) => (
<RollBackButton
key={index}
current={currentStep === index}
onClick={() => onClickHandler(index)}
>
{index === 0 ? "Game Start" : `Back to Move #${index}`}
</RollBackButton>
))}
</List>
</SectionWrapper>
)
}
export default HistoryListFinal Product
Project Files
For more information about how to use the plugin and the features it includes, GitHub Repository for Part 3.