diff --git a/README.md b/README.md
index 64bf9d9..7c0e207 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@ Branches:
| Name | Description | Live |
| ------------- | ------------- | ------------- |
-| **/main** | Starter by [Dan Abramov](https://twitter.com/dan_abramov) rewritten with create-react-app | [codepen](https://codepen.io/gaearon/pen/gWWZgR) |
+| [**/main**](https://github.com/GregoryNative/react-tutorial-tic-tac-toe/tree/main/src) | Starter by [Dan Abramov](https://twitter.com/dan_abramov) rewritten with create-react-app | [codepen](https://codepen.io/gaearon/pen/gWWZgR) |
+| [**/redux+hooks**](https://github.com/GregoryNative/react-tutorial-tic-tac-toe/tree/redux+hooks/src) | Rewritten with [redux](https://redux.js.org) and hooks | [stackblitz](https://stackblitz.com/edit/react-tictactoe-redux?file=src%2FApp.js) |
diff --git a/package.json b/package.json
index 7423eba..16b4d32 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,10 @@
"@testing-library/user-event": "^12.1.10",
"react": "^17.0.2",
"react-dom": "^17.0.2",
+ "react-redux": "^7.2.4",
"react-scripts": "4.0.3",
+ "redux": "^4.1.0",
+ "reselect": "^4.0.0",
"web-vitals": "^1.0.1"
},
"scripts": {
diff --git a/src/App.js b/src/App.js
index 46a3a23..ea257eb 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,9 +1,15 @@
+import { createStore } from 'redux';
+import { Provider } from 'react-redux';
+
import Game from './components/Game';
+import reducers from './reducers';
+
+const store = createStore(reducers);
-function App() {
+export default function App() {
return (
-
+
+
+
);
}
-
-export default App;
diff --git a/src/actions/game.js b/src/actions/game.js
new file mode 100644
index 0000000..9ec3167
--- /dev/null
+++ b/src/actions/game.js
@@ -0,0 +1,13 @@
+export const SET_STATE_TYPE = '@game/SET_STATE_TYPE';
+export const JUMP_TO_TYPE = '@game/JUMP_TO_TYPE';
+
+export const setState = ({ history, squares }) => ({
+ type: SET_STATE_TYPE,
+ history,
+ squares
+});
+
+export const jumpTo = step => ({
+ type: JUMP_TO_TYPE,
+ step
+});
diff --git a/src/components/Game.js b/src/components/Game.js
index 4fd9270..936d179 100644
--- a/src/components/Game.js
+++ b/src/components/Game.js
@@ -1,86 +1,78 @@
import React from 'react';
+import { useSelector, useDispatch } from 'react-redux';
import Board from './Board';
+
+import {
+ setState,
+ jumpTo as jumpToAction,
+} from '../actions/game';
+import {
+ getCurrent,
+ getHistory,
+ getStepNumber,
+ getWinner,
+ getXIsNext
+} from '../selectors/game';
import calculateWinner from '../helpers/calculateWinner';
-class Game extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- history: [
- {
- squares: Array(9).fill(null)
- }
- ],
- stepNumber: 0,
- xIsNext: true
- };
- }
+function Game() {
+ const dispatch = useDispatch();
+
+ const history = useSelector(getHistory);
+ const current = useSelector(getCurrent);
+ const winner = useSelector(getWinner);
+ const stepNumber = useSelector(getStepNumber);
+ const xIsNext = useSelector(getXIsNext);
+
+ const handleClick = i => {
+ const nextHistory = history.slice(0, stepNumber + 1);
+ const nextCurrent = nextHistory[nextHistory.length - 1];
+ const nextSquares = nextCurrent.squares.slice();
- handleClick(i) {
- const history = this.state.history.slice(0, this.state.stepNumber + 1);
- const current = history[history.length - 1];
- const squares = current.squares.slice();
- if (calculateWinner(squares) || squares[i]) {
+ if (calculateWinner(nextSquares) || nextSquares[i]) {
return;
}
- squares[i] = this.state.xIsNext ? "X" : "O";
- this.setState({
- history: history.concat([
- {
- squares: squares
- }
- ]),
- stepNumber: history.length,
- xIsNext: !this.state.xIsNext
- });
- }
- jumpTo(step) {
- this.setState({
- stepNumber: step,
- xIsNext: (step % 2) === 0
- });
- }
+ nextSquares[i] = xIsNext ? 'X' : 'O';
- render() {
- const history = this.state.history;
- const current = history[this.state.stepNumber];
- const winner = calculateWinner(current.squares);
+ dispatch(setState({
+ history: nextHistory,
+ squares: nextSquares
+ }));
+ };
- const moves = history.map((step, move) => {
- const desc = move ?
- 'Go to move #' + move :
- 'Go to game start';
- return (
-
-
-
- );
- });
-
- let status;
- if (winner) {
- status = "Winner: " + winner;
- } else {
- status = "Next player: " + (this.state.xIsNext ? "X" : "O");
- }
+ const jumpTo = step => {
+ dispatch(jumpToAction(step));
+ };
+ const moves = history.map((step, move) => {
+ const desc = move ? 'Go to move #' + move : 'Go to game start';
return (
-
-
- this.handleClick(i)}
- />
-
-
-
+
+
+
);
+ });
+
+ let status;
+ if (winner) {
+ status = 'Winner: ' + winner;
+ } else {
+ status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
+
+ return (
+
+
+ handleClick(i)} />
+
+
+
+ );
}
export default Game;
diff --git a/src/reducers/game.js b/src/reducers/game.js
new file mode 100644
index 0000000..f8bb590
--- /dev/null
+++ b/src/reducers/game.js
@@ -0,0 +1,35 @@
+import { SET_STATE_TYPE, JUMP_TO_TYPE } from '../actions/game';
+
+const initialValues = {
+ history: [
+ {
+ squares: Array(9).fill(null)
+ }
+ ],
+ stepNumber: 0,
+ xIsNext: true
+};
+
+export default function gameReducer(state = initialValues, action) {
+ switch (action.type) {
+ case SET_STATE_TYPE:
+ return {
+ ...state,
+ history: action.history.concat([
+ {
+ squares: action.squares
+ }
+ ]),
+ stepNumber: action.history.length,
+ xIsNext: !state.xIsNext
+ };
+ case JUMP_TO_TYPE:
+ return {
+ ...state,
+ stepNumber: action.step,
+ xIsNext: action.step % 2 === 0
+ };
+ default:
+ return state;
+ }
+}
diff --git a/src/reducers/index.js b/src/reducers/index.js
new file mode 100644
index 0000000..543526c
--- /dev/null
+++ b/src/reducers/index.js
@@ -0,0 +1,7 @@
+import { combineReducers } from 'redux';
+
+import gameReducer from './game';
+
+export default combineReducers({
+ game: gameReducer
+});
diff --git a/src/selectors/game.js b/src/selectors/game.js
new file mode 100644
index 0000000..b2f707a
--- /dev/null
+++ b/src/selectors/game.js
@@ -0,0 +1,17 @@
+import { createSelector } from 'reselect';
+
+import calculateWinner from '../helpers/calculateWinner';
+
+export const getHistory = state => state.game.history;
+export const getStepNumber = state => state.game.stepNumber;
+export const getXIsNext = state => state.game.xIsNext;
+export const getCurrent = createSelector(
+ [getHistory, getStepNumber],
+ (history, stepNumber) => {
+ return history[stepNumber];
+ }
+);
+export const getWinner = createSelector(
+ [getCurrent],
+ current => calculateWinner(current.squares)
+);
diff --git a/yarn.lock b/yarn.lock
index 6cb0c56..ea8aca2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1368,7 +1368,7 @@
dependencies:
regenerator-runtime "^0.13.4"
-"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.9.2":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.5.tgz#665450911c6031af38f81db530f387ec04cd9a98"
integrity sha512-121rumjddw9c3NCQ55KGkyE1h/nzWhU/owjhw0l4mQrkzz4x9SGS1X8gFLraHwX7td3Yo4QTL+qj0NcIzN87BA==
@@ -2035,6 +2035,14 @@
dependencies:
"@types/node" "*"
+"@types/hoist-non-react-statics@^3.3.0":
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
+ integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
+ dependencies:
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+
"@types/html-minifier-terser@^5.0.0":
version "5.1.1"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#3c9ee980f1a10d6021ae6632ca3e79ca2ec4fb50"
@@ -2112,11 +2120,35 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.3.tgz#ef65165aea2924c9359205bf748865b8881753c0"
integrity sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==
+"@types/prop-types@*":
+ version "15.7.3"
+ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
+ integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
+
"@types/q@^1.5.1":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
+"@types/react-redux@^7.1.16":
+ version "7.1.16"
+ resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21"
+ integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==
+ dependencies:
+ "@types/hoist-non-react-statics" "^3.3.0"
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+ redux "^4.0.0"
+
+"@types/react@*":
+ version "17.0.11"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451"
+ integrity sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA==
+ dependencies:
+ "@types/prop-types" "*"
+ "@types/scheduler" "*"
+ csstype "^3.0.2"
+
"@types/resolve@0.0.8":
version "0.0.8"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
@@ -2124,6 +2156,11 @@
dependencies:
"@types/node" "*"
+"@types/scheduler@*":
+ version "0.16.1"
+ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
+ integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
+
"@types/source-list-map@*":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
@@ -4218,6 +4255,11 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
+csstype@^3.0.2:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
+ integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==
+
cyclist@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
@@ -5839,6 +5881,13 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
+hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
+ integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
+ dependencies:
+ react-is "^16.7.0"
+
hoopy@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d"
@@ -9535,7 +9584,7 @@ react-error-overlay@^6.0.9:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
-react-is@^16.8.1:
+react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -9545,6 +9594,18 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+react-redux@^7.2.4:
+ version "7.2.4"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
+ integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==
+ dependencies:
+ "@babel/runtime" "^7.12.1"
+ "@types/react-redux" "^7.1.16"
+ hoist-non-react-statics "^3.3.2"
+ loose-envify "^1.4.0"
+ prop-types "^15.7.2"
+ react-is "^16.13.1"
+
react-refresh@^0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
@@ -9713,6 +9774,13 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
+redux@^4.0.0, redux@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4"
+ integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==
+ dependencies:
+ "@babel/runtime" "^7.9.2"
+
regenerate-unicode-properties@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
@@ -9869,6 +9937,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+reselect@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
+ integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
+
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"