From bf9c35cc995c25a59b6b827814232cce4253d961 Mon Sep 17 00:00:00 2001 From: Gregory Date: Fri, 11 Jun 2021 09:52:28 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20project:=20add=20redux=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- package.json | 3 + src/App.js | 14 +++-- src/actions/game.js | 13 +++++ src/components/Game.js | 128 +++++++++++++++++++---------------------- src/reducers/game.js | 35 +++++++++++ src/reducers/index.js | 7 +++ src/selectors/game.js | 17 ++++++ yarn.lock | 77 ++++++++++++++++++++++++- 9 files changed, 222 insertions(+), 75 deletions(-) create mode 100644 src/actions/game.js create mode 100644 src/reducers/game.js create mode 100644 src/reducers/index.js create mode 100644 src/selectors/game.js 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)} - /> -
    -
    -
    {status}
    -
      {moves}
    -
    -
    +
  • + +
  • ); + }); + + let status; + if (winner) { + status = 'Winner: ' + winner; + } else { + status = 'Next player: ' + (xIsNext ? 'X' : 'O'); } + + return ( +
    +
    + handleClick(i)} /> +
    +
    +
    {status}
    +
      {moves}
    +
    +
    + ); } 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"