diff --git a/README.md b/README.md index 412fcf9..257c3ad 100644 --- a/README.md +++ b/README.md @@ -283,7 +283,6 @@ [Pull request](https://github.com/nickovchinnikov/minesweeper/pull/50/files) - ## Redux intro ### Pure functions benifits @@ -292,5 +291,10 @@ [Pull request](https://github.com/nickovchinnikov/minesweeper/pull/51/files) -### -### Redux basic example \ No newline at end of file +### Referential transparency + +[Pull request](https://github.com/nickovchinnikov/minesweeper/pull/52/files) + +### Redux basic example + +[Pull request](https://github.com/nickovchinnikov/minesweeper/pull/53/files) diff --git a/examples/Redux/ClickCounter.test.tsx b/examples/Redux/ClickCounter.test.tsx new file mode 100644 index 0000000..a27d2ea --- /dev/null +++ b/examples/Redux/ClickCounter.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; + +import { ClickCounterBasic } from './ClickCounterBasic'; + +describe('ClickCounterBasic component test', () => { + it('should use custom step when incrementing', async () => { + render(); + const decButton = screen.getByTestId('dec'); + const incButton = screen.getByTestId('inc'); + const count = screen.getByTestId('count'); + + expect(count).toHaveTextContent('Count: 0'); + + fireEvent.click(incButton); + + expect(count).toHaveTextContent('Count: 1'); + + fireEvent.click(decButton); + + expect(count).toHaveTextContent('Count: 0'); + }); +}); diff --git a/examples/Redux/ClickCounterBasic.tsx b/examples/Redux/ClickCounterBasic.tsx new file mode 100644 index 0000000..123721d --- /dev/null +++ b/examples/Redux/ClickCounterBasic.tsx @@ -0,0 +1,19 @@ +import React, { FC, useReducer } from 'react'; + +import { reducer, initialState, increment, decrement } from './counter'; + +export const ClickCounterBasic: FC = () => { + const [state, dispatch] = useReducer(reducer, initialState); + + return ( + <> +
Count: {state}
+ + + + ); +}; diff --git a/examples/Redux/counter.test.ts b/examples/Redux/counter.test.ts new file mode 100644 index 0000000..731a621 --- /dev/null +++ b/examples/Redux/counter.test.ts @@ -0,0 +1,10 @@ +import { initialState, reducer, increment, decrement } from './counter'; + +describe('Counter redux module test', () => { + it('Default init with increment action', async () => { + expect(reducer(initialState, increment())).toBe(1); + }); + it('Init with value decrement action', async () => { + expect(reducer(10, decrement())).toBe(9); + }); +}); diff --git a/examples/Redux/counter.ts b/examples/Redux/counter.ts new file mode 100644 index 0000000..cb6b481 --- /dev/null +++ b/examples/Redux/counter.ts @@ -0,0 +1,22 @@ +interface Action { + type: string; +} + +export const initialState = 0; + +const INCREMENT = 'examples/Redux/counter/increment'; +const DECREMENT = 'examples/Redux/counter/decrement'; + +export function reducer(state: number, action: Action): number { + switch (action.type) { + case INCREMENT: + return state + 1; + case DECREMENT: + return state - 1; + default: + throw new Error(); + } +} + +export const increment = (): Action => ({ type: INCREMENT }); +export const decrement = (): Action => ({ type: DECREMENT }); diff --git a/examples/Redux/counterSlice.test.ts b/examples/Redux/counterSlice.test.ts new file mode 100644 index 0000000..1059edd --- /dev/null +++ b/examples/Redux/counterSlice.test.ts @@ -0,0 +1,10 @@ +import { reducer, actions, initialState } from './counterSlice'; + +describe('Counter redux module test', () => { + it('Default init with increment action', async () => { + expect(reducer(initialState, actions.increment())).toBe(1); + }); + it('Init with value decrement action', async () => { + expect(reducer(10, actions.decrement())).toBe(9); + }); +}); diff --git a/examples/Redux/counterSlice.ts b/examples/Redux/counterSlice.ts new file mode 100644 index 0000000..fe2cafe --- /dev/null +++ b/examples/Redux/counterSlice.ts @@ -0,0 +1,14 @@ +import { createSlice } from '@reduxjs/toolkit'; + +export const initialState = 0; + +const { reducer, actions } = createSlice({ + name: 'counter', + initialState, + reducers: { + increment: (state) => state + 1, + decrement: (state) => state - 1, + }, +}); + +export { reducer, actions }; diff --git a/package-lock.json b/package-lock.json index 0abde30..2713d17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,7 @@ "@emotion/styled": "^11.3.0", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-router-dom": "^5.3.0", - "redux": "^4.1.1" + "react-router-dom": "^5.3.0" }, "devDependencies": { "@babel/core": "^7.15.5", @@ -21,6 +20,7 @@ "@babel/preset-react": "^7.14.5", "@babel/preset-typescript": "^7.15.0", "@emotion/jest": "^11.3.0", + "@reduxjs/toolkit": "^1.6.2", "@storybook/addon-actions": "^6.3.9", "@storybook/addon-essentials": "^6.3.9", "@storybook/addon-links": "^6.3.9", @@ -3349,6 +3349,40 @@ "react-dom": "15.x || 16.x || 16.4.0-alpha.0911da3" } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.6.2.tgz", + "integrity": "sha512-HbfI/hOVrAcMGAYsMWxw3UJyIoAS9JTdwddsjlr5w3S50tXhWb+EMyhIw+IAvCVCLETkzdjgH91RjDSYZekVBA==", + "dev": true, + "dependencies": { + "immer": "^9.0.6", + "redux": "^4.1.0", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0", + "react-redux": "^7.2.1" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz", + "integrity": "sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@samverschueren/stream-to-observable": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", @@ -26174,10 +26208,17 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", + "dev": true, "dependencies": { "@babel/runtime": "^7.9.2" } }, + "node_modules/redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==", + "dev": true + }, "node_modules/refractor": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.4.0.tgz", @@ -26706,6 +26747,12 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "node_modules/reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==", + "dev": true + }, "node_modules/resolve": { "version": "2.0.0-next.3", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", @@ -33862,6 +33909,26 @@ "react-lifecycles-compat": "^3.0.4" } }, + "@reduxjs/toolkit": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.6.2.tgz", + "integrity": "sha512-HbfI/hOVrAcMGAYsMWxw3UJyIoAS9JTdwddsjlr5w3S50tXhWb+EMyhIw+IAvCVCLETkzdjgH91RjDSYZekVBA==", + "dev": true, + "requires": { + "immer": "^9.0.6", + "redux": "^4.1.0", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0" + }, + "dependencies": { + "immer": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz", + "integrity": "sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==", + "dev": true + } + } + }, "@samverschueren/stream-to-observable": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", @@ -51850,10 +51917,17 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", + "dev": true, "requires": { "@babel/runtime": "^7.9.2" } }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==", + "dev": true + }, "refractor": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.4.0.tgz", @@ -52272,6 +52346,12 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==", + "dev": true + }, "resolve": { "version": "2.0.0-next.3", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", diff --git a/package.json b/package.json index ee3476f..88cbf3f 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,6 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-router-dom": "^5.3.0", - "redux": "^4.1.1" + "@reduxjs/toolkit": "^1.6.2" } }