diff --git a/README.md b/README.md index 468e542d..bdb99896 100644 --- a/README.md +++ b/README.md @@ -154,3 +154,10 @@ React Patterns [Pull request](https://github.com/nickovchinnikov/react-js-tutorial/pull/23)
[Presentation](https://drive.google.com/file/d/1VezGwpkXUV38X-pL48j9ZjC58ASPuTPl/view?usp=sharing)
+ + +## Lesson 16: +* React + Redux + +[Pull request](https://github.com/nickovchinnikov/react-js-tutorial/pull/27)
+[Presentation](https://docs.google.com/presentation/d/1kng-DBHU91jQqjWHQVOMNMZjjfe8zawS8zzLP44TnuM/edit?usp=sharing)
diff --git a/package-lock.json b/package-lock.json index 0e8df0b4..ab21ff27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4304,6 +4304,16 @@ "integrity": "sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw==", "dev": true }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dev": true, + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.0.0.tgz", @@ -4445,6 +4455,18 @@ "@types/react": "*" } }, + "@types/react-redux": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.9.tgz", + "integrity": "sha512-mpC0jqxhP4mhmOl3P4ipRsgTgbNofMRXJb08Ms6gekViLj61v1hOZEKWDCyWsdONr6EjEA6ZHXC446wdywDe0w==", + "dev": true, + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/react-router": { "version": "5.1.7", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.7.tgz", @@ -4493,6 +4515,15 @@ "@types/react": "*" } }, + "@types/redux-mock-store": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.2.tgz", + "integrity": "sha512-6LBtAQBN34i7SI5X+Qs4zpTEZO1tTDZ6sZ9fzFjYwTl3nLQXaBtwYdoV44CzNnyKu438xJ1lSIYyw0YMvunESw==", + "dev": true, + "requires": { + "redux": "^4.0.5" + } + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -11827,9 +11858,9 @@ "dev": true }, "http-proxy": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", - "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "requires": { "eventemitter3": "^4.0.0", @@ -14899,6 +14930,12 @@ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", "dev": true }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -15035,9 +15072,9 @@ } }, "loglevel": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", - "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz", + "integrity": "sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==", "dev": true }, "loglevelnext": { @@ -16829,9 +16866,9 @@ "dev": true }, "portfinder": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz", - "integrity": "sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==", + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz", + "integrity": "sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ==", "dev": true, "requires": { "async": "^2.6.2", @@ -18146,6 +18183,18 @@ } } }, + "react-redux": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz", + "integrity": "sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==", + "requires": { + "@babel/runtime": "^7.5.5", + "hoist-non-react-statics": "^3.3.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.9.0" + } + }, "react-router": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", @@ -18524,6 +18573,15 @@ "symbol-observable": "^1.2.0" } }, + "redux-mock-store": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", + "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", + "dev": true, + "requires": { + "lodash.isplainobject": "^4.0.6" + } + }, "reflect.ownkeys": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", @@ -19776,13 +19834,25 @@ } }, "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", + "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==", "dev": true, "requires": { "faye-websocket": "^0.10.0", - "uuid": "^3.0.1" + "uuid": "^3.4.0", + "websocket-driver": "0.6.5" + }, + "dependencies": { + "websocket-driver": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", + "dev": true, + "requires": { + "websocket-extensions": ">=0.1.1" + } + } } }, "sockjs-client": { @@ -19915,9 +19985,9 @@ "dev": true }, "spdy": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.1.tgz", - "integrity": "sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", "dev": true, "requires": { "debug": "^4.1.0", @@ -22317,9 +22387,9 @@ } }, "webpack-dev-server": { - "version": "3.10.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz", - "integrity": "sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz", + "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==", "dev": true, "requires": { "ansi-html": "0.0.7", @@ -22330,65 +22400,71 @@ "debug": "^4.1.1", "del": "^4.1.1", "express": "^4.17.1", - "html-entities": "^1.2.1", + "html-entities": "^1.3.1", "http-proxy-middleware": "0.19.1", "import-local": "^2.0.0", "internal-ip": "^4.3.0", "ip": "^1.1.5", "is-absolute-url": "^3.0.3", "killable": "^1.0.1", - "loglevel": "^1.6.6", + "loglevel": "^1.6.8", "opn": "^5.5.0", "p-retry": "^3.0.1", - "portfinder": "^1.0.25", + "portfinder": "^1.0.26", "schema-utils": "^1.0.0", "selfsigned": "^1.10.7", "semver": "^6.3.0", "serve-index": "^1.9.1", - "sockjs": "0.3.19", + "sockjs": "0.3.20", "sockjs-client": "1.4.0", - "spdy": "^4.0.1", + "spdy": "^4.0.2", "strip-ansi": "^3.0.1", "supports-color": "^6.1.0", "url": "^0.11.0", "webpack-dev-middleware": "^3.7.2", "webpack-log": "^2.0.0", "ws": "^6.2.1", - "yargs": "12.0.5" + "yargs": "^13.3.2" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "dev": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" }, "dependencies": { "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^4.1.0" } } } }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "html-entities": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz", + "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==", "dev": true }, "import-local": { @@ -22407,12 +22483,6 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, "resolve-cwd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", @@ -22446,22 +22516,23 @@ "dev": true }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, "requires": { + "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "strip-ansi": "^5.1.0" }, "dependencies": { "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^4.1.0" } } } @@ -22476,33 +22547,23 @@ } }, "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "dev": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" }, "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "ansi-regex": "^4.1.0" } } } @@ -22517,29 +22578,27 @@ } }, "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "dev": true, "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", + "cliui": "^5.0.0", "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", + "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", + "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^2.0.0", + "string-width": "^3.0.0", "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" } }, "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { "camelcase": "^5.0.0", diff --git a/package.json b/package.json index bcbbbabe..a74fab3a 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,10 @@ "@types/ramda": "^0.27.6", "@types/react": "^16.9.33", "@types/react-dom": "^16.9.6", + "@types/react-redux": "^7.1.9", "@types/react-router-dom": "^5.1.5", "@types/react-test-renderer": "^16.9.2", + "@types/redux-mock-store": "^1.0.2", "@typescript-eslint/eslint-plugin": "^2.25.0", "@typescript-eslint/parser": "^2.25.0", "babel-jest": "^25.2.4", @@ -74,12 +76,13 @@ "prettier": "^2.0.2", "react-docgen-typescript-loader": "^3.7.2", "react-test-renderer": "^16.13.1", + "redux-mock-store": "^1.5.4", "storybook-addon-react-docgen": "^1.2.32", "ts-node": "^8.8.2", "typescript": "^3.8.3", "webpack": "^4.42.1", "webpack-cli": "^3.3.11", - "webpack-dev-server": "^3.10.3" + "webpack-dev-server": "^3.11.0" }, "dependencies": { "@emotion/core": "^10.0.28", @@ -88,6 +91,7 @@ "ramda": "^0.27.0", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-redux": "^7.2.0", "react-router-dom": "^5.2.0", "redux": "^4.0.5" }, diff --git a/src/App.tsx b/src/App.tsx index 78488da5..96fa954e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,35 +5,39 @@ import { FieldScreen } from "@/screens/FieldScreen"; import { NoMatchScreen } from "@/screens/NoMatchScreen"; import { UserScreen } from "@/screens/UserScreen"; import { ReduxScreen } from "@/screens/ReduxScreen"; +import { Provider } from "react-redux"; +import { store } from "@/rdx/store"; export const App: React.FC<{}> = () => ( - - - - - - - } /> - - - - - - - + + + + + + + + } /> + + + + + + + + ); diff --git a/src/components/NextMove.tsx b/src/components/NextMove.tsx index 33414ba3..0d2d7c57 100644 --- a/src/components/NextMove.tsx +++ b/src/components/NextMove.tsx @@ -1,9 +1,15 @@ -import React from 'react'; -import { withRedux } from '@/utils/withRedux'; -import { TicTacToeGameState } from '@/rdx/reducer'; +import React from "react"; +import { TicTacToeGameState } from "@/rdx/reducer"; +import { connect } from "react-redux"; -const RawNextMove: React.FC<{ nextMove: string }> = ({ nextMove }) =>

Next move is for {nextMove}

; +const RawNextMove: React.FC<{ nextMove: string }> = ({ nextMove }) => ( +

Next move is for {nextMove}

+); -export const NextMove = withRedux(RawNextMove, (state: TicTacToeGameState) => ({ - nextMove: state.nextMove -})); +function mapStateToProps(state: TicTacToeGameState) { + return { + nextMove: state.nextMove, + }; +} + +export const NextMove = connect(mapStateToProps)(RawNextMove); diff --git a/src/rdx/actions.ts b/src/rdx/actions.ts new file mode 100644 index 00000000..111ad756 --- /dev/null +++ b/src/rdx/actions.ts @@ -0,0 +1,18 @@ +export const X_MOVE = "X_MOVE"; +export const O_MOVE = "O_MOVE"; + +export type Coordinates = { x: number; y: number }; + +export function xMove(payload: Coordinates) { + return { + type: X_MOVE, + payload, + }; +} + +export function oMove(payload: Coordinates) { + return { + type: O_MOVE, + payload, + }; +} diff --git a/src/rdx/reducer/gameField.ts b/src/rdx/reducer/gameField.ts index 0a06fa70..643dc48c 100644 --- a/src/rdx/reducer/gameField.ts +++ b/src/rdx/reducer/gameField.ts @@ -1,5 +1,5 @@ import { Action } from "redux"; -import * as actionTypes from '@/rdx/types'; +import * as actionTypes from '@/rdx/actions'; type GameFieldState = string[][]; diff --git a/src/rdx/reducer/nextMove.ts b/src/rdx/reducer/nextMove.ts index c2c167c5..701951f0 100644 --- a/src/rdx/reducer/nextMove.ts +++ b/src/rdx/reducer/nextMove.ts @@ -1,5 +1,5 @@ import { Action } from "redux"; -import * as actionTypes from '@/rdx/types'; +import * as actionTypes from '@/rdx/actions'; type nextMoveState = 'x' | 'o'; diff --git a/src/rdx/types.ts b/src/rdx/types.ts deleted file mode 100644 index e0aa9dd4..00000000 --- a/src/rdx/types.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const X_MOVE = 'X_MOVE'; -export const O_MOVE = 'O_MOVE'; \ No newline at end of file diff --git a/src/screens/ReduxScreen.test.tsx b/src/screens/ReduxScreen.test.tsx new file mode 100644 index 00000000..0b2b0ef6 --- /dev/null +++ b/src/screens/ReduxScreen.test.tsx @@ -0,0 +1,107 @@ +import React from "react"; +import { ReduxScreen } from "./ReduxScreen"; +import { Provider } from "react-redux"; +import { mount } from "enzyme"; +import { reducer } from "@/rdx/reducer"; +import { createStore } from "redux"; +import configureStore from "redux-mock-store"; + +describe("ReduxScreen with mocked store", () => { + const mockStore = configureStore([]); + + let store: any; + + beforeEach(() => { + store = mockStore({ + nextMove: "x", + gameField: [[]], + }); + }); + + it("should generate action on click", () => { + const wrapper = mount( + + + + ); + + (wrapper.find("Field").props() as any).onClick(100, 999); + + expect(store.getActions()).toMatchInlineSnapshot(` + Array [ + Object { + "payload": Object { + "x": 100, + "y": 999, + }, + "type": "X_MOVE", + }, + ] + `); + }); +}); + +describe("ReduxScreen with real store", () => { + let store: any; + + beforeEach(() => { + store = createStore(reducer, { + nextMove: "x", + gameField: [ + ["", ""], + ["", ""], + ], + }); + jest.spyOn(store, "dispatch"); + }); + + it("should generate action on click", () => { + const wrapper = mount( + + + + ); + + (wrapper.find("Field").props() as any).onClick(0, 1); + wrapper.update(); // we need this if we're going to compare snapshots + (wrapper.find("Field").props() as any).onClick(1, 1); + + expect(store.getState()).toMatchInlineSnapshot(` + Object { + "gameField": Array [ + Array [ + "", + "", + ], + Array [ + "x", + "o", + ], + ], + "nextMove": "x", + } + `); + expect((store.dispatch as jest.Mock).mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "payload": Object { + "x": 0, + "y": 1, + }, + "type": "X_MOVE", + }, + ], + Array [ + Object { + "payload": Object { + "x": 1, + "y": 1, + }, + "type": "O_MOVE", + }, + ], + ] + `); + }); +}); diff --git a/src/screens/ReduxScreen.tsx b/src/screens/ReduxScreen.tsx index 4e59dc9f..7b51844c 100644 --- a/src/screens/ReduxScreen.tsx +++ b/src/screens/ReduxScreen.tsx @@ -1,40 +1,44 @@ -import React from 'react'; -import * as actionTypes from '@/rdx/types'; -import { Field } from '@/components/InteractiveField/components/Field'; -import { withRedux } from '@/utils/withRedux'; -import { Action } from 'redux'; -import { NextMove } from 'components/NextMove'; -import { TicTacToeGameState } from '@/rdx/reducer'; - -function getReduxScreenState(state: TicTacToeGameState) { - return { - gameField: state.gameField, - nextMove: state.nextMove, - }; -} +import React from "react"; +import { Field } from "@/components/InteractiveField/components/Field"; +import { NextMove } from "components/NextMove"; +import { TicTacToeGameState } from "@/rdx/reducer"; +import { Coordinates, xMove, oMove } from "@/rdx/actions"; +import { connect } from "react-redux"; interface RawReduxScreenProps { nextMove: string; gameField: string[][]; - dispatch: (action: Action & { payload?: any }) => void; + xMove: (coords: Coordinates) => void; + oMove: (coords: Coordinates) => void; } -class RawReduxScreen extends React.Component{ +class RawReduxScreen extends React.Component { onCellClick = (x: number, y: number) => { - this.props.dispatch({ - type: this.props.nextMove === 'x' ? actionTypes.X_MOVE : actionTypes.O_MOVE, - payload: { x, y }, - }) - } + this.props[this.props.nextMove === "x" ? "xMove" : "oMove"]({ x, y }); + }; render() { - return
-

Open console to observe

- - -
{JSON.stringify(this.props, null, 2)}
-
+ return ( +
+

Open console to observe

+ + +
{JSON.stringify(this.props, null, 2)}
+
+ ); } } -export const ReduxScreen = withRedux(RawReduxScreen, getReduxScreenState); \ No newline at end of file +function mapStateToProps(state: TicTacToeGameState) { + return { + gameField: state.gameField, + nextMove: state.nextMove, + }; +} + +const mapDispatchToProps = { xMove, oMove }; + +export const ReduxScreen = connect( + mapStateToProps, + mapDispatchToProps +)(RawReduxScreen);