Skip to content

The React Tutorial written with hooks & reducers.

Notifications You must be signed in to change notification settings

0xnoob/Tic-Tac-Toe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Tic-Tac-Toe

Play with it on CodeSandbox

This repository contains the tic-tac-toe game from the official React Tutorial: Intro to React, but with hooks & reducers.
It's inspired by the YouTube video Trying React Hooks for the first time with Dan Abramov, at the end of which (1:01:36) he mentiones a few things you can implement:

Seperate the game logic and time traveling into two independent parts:

  • move the game logic into a reducer
  • create a custom hook that uses a reducer to manage the history state and takes care of time travel

That's what this repository is for.


There are four implementation variants of the game, each in one of the src/index.*.js files:

  index
.no-reducer
.no-timetravel.js
index
.no-timetravel.js
index
.no-reducer.js
index
.varA
.js
index
.varB
.js
Based on cdpn.io/LyyXgK cdpn.io/LyyXgK cdpn.io/gWWZgR cdpn.io/gWWZgR cdpn.io/gWWZgR
React Hooks ✔️ ✔️ ✔️ ✔️ ✔️
Reducer ✖️ ✔️ ✖️ ✔️ ✔️
Time Travel ✖️ ✖️ ✔️ ✔️ ✔️

The first three are straightforward to write - the last two (index.varA.js and index.varB.js) are of interest, because they include a custom Hook that manages time travel (useTimeTravel).

Difference between index.varA.js and index.varB.js

tl;dr

  • index.varA.js's custom Hook is a wrapper for your game logic: You handle game-state-transitions through it. That saves you from dispatching actions to your game-reducer as well as your timetravel-reducer, but you still have to deal with the history array (e.g. const current = history[history.length - 1];)
  • In index.varB.js's you dispatch actions to your game-reducer and timetravel-reducer, but you don't have to deal with the history anymore.

This variant uses a custom Hook that is a wrapper around the reducer of the game logic.

function Game() {
  [...]
  // custom Hook
  const [{ history, stepNumber }, dispatchTimeTravel] = useTimeTravel(
    gameLogicReducer, 
    initialBoardState
  );
  [...]
} 

To get the current state of the game and to change it (e.g. if someone clicks on a square), you always work with the custom Hook.

function Game() {
  [...]
  const current = history[stepNumber]; // history and stepNumber are returned by your custom hook
  [...]
  function handleClick(i) {
    [...]
    // the payload is the action to the reducer of the game (which is in this case just the index)
    dispatchTimeTravel({ type: "HISTORY_ADD", payload: i });
  }

  function jumpTo(step) {
    dispatchTimeTravel({ type: "HISTORY_JUMPTO", payload: step });
  }
  [...]
}

The gameLogicReducer is only called inside the timeTravelReducer() to get the new state of the game.

function timeTravelReducer(state, action) {
  switch (action.type) {
    case "HISTORY_ADD":
     const gameLogicAction = action.payload;
     let { gameLogicReducer, history, stepNumber } = state;
     let gameState = history[stepNumber];
     gameState = gameLogicReducer(gameState, gameLogicAction);
     [...]
  }
}

That's why the code in our Game() function component is very similar to the final code of the tutorial: We are still working with the history object.

function Game() {
  [...]
  const current = history[stepNumber]; // history and stepNumber are returned by your custom hook
  [...]
  const winner = calculateWinner(current.squares);
  [...]
  status = "Next player: " + (current.xIsNext ? "X" : "O");
  [...]
  <Board squares={current.squares} onClick={i => handleClick(i)} />
  [...]
}

This variant uses a custom Hook that is used additionally to the reducer of the game

function Game() {
  [...]
  const [gameState, dispatchGame] = useReducer(
    gameLogicReducer,
    initialBoardState
  );
  const [stepNumber, dispatchTimeTravel] = useTimeTravel(dispatchGame,
    { type: "PLAY_RESET" } // an action to reset the game is required
  );
  [...]
}

Instead of calling the game reducer inside the time travel reducer, you call both directly in your Game() component.

function Game() {
[...]
  function handleClick(i) {
    [...]
    const gameAction = { type: "PLAY_MOVE", payload: i };
    dispatchGame(gameAction); 
    dispatchTimeTravel({ type: "HISTORY_ADD", payload: gameAction });
  }
}

For this to work, the game reducer has to implement a second type of state change, to reset the game.

function gameLogicReducer(state, action) {
  [...]
    case "PLAY_RESET":
      return {
        squares: new Array(9).fill(null),
        xIsNext: true
      };
    [...]
}

That's because the time travel reducer doesn't have full control over the game state: He can only dispatch action and unlike in index.varA.js can't just replace the state of the game.
When you jump back in time, the time travel reducer has to reset the game and replay it until the chosen move is reached.

function timeTravelReducer(state, action) {
    [...]
    case "HISTORY_JUMPTO":
      [...]
      dispatchGame(actionReset); // actionReset === { type: "PLAY_RESET }
      actionHistory = actionHistory.slice(0, stepNumber);
      actionHistory.forEach(action => dispatchGame(action));
      [...]
  }
}

The upside of this is, that you can just use the game state and don't have to mess with an history array in your Game component.

function Game() {
  [...]
  const winner = calculateWinner(gameState.squares);
  [...]
  const winner = calculateWinner(gameState.squares);
  [...]
  const winner = calculateWinner(gameState.squares);
  [...]
}

About

The React Tutorial written with hooks & reducers.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published