diff --git a/Chapter1/.gitignore b/Chapter1/.gitignore new file mode 100644 index 0000000..30bc162 --- /dev/null +++ b/Chapter1/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/Chapter2-end/.gitignore b/Chapter2-end/.gitignore new file mode 100644 index 0000000..30bc162 --- /dev/null +++ b/Chapter2-end/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/Chapter2-start/package-lock.json b/Chapter2-start/package-lock.json index af2ec92..7f0a1cd 100644 --- a/Chapter2-start/package-lock.json +++ b/Chapter2-start/package-lock.json @@ -8,17 +8,15 @@ "name": "react-with-redux", "version": "0.1.0", "dependencies": { + "@reduxjs/toolkit": "^1.9.5", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", - "axios": "^1.3.4", + "axios": "^1.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-redux": "^8.0.5", + "react-redux": "^8.1.2", "react-scripts": "5.0.1", - "redux": "^4.2.1", - "redux-logger": "^3.0.6", - "redux-thunk": "^2.4.2", "web-vitals": "^2.1.4" } }, @@ -3096,6 +3094,29 @@ } } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -5076,9 +5097,9 @@ } }, "node_modules/axios": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", - "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -6481,11 +6502,6 @@ "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==" }, - "node_modules/deep-diff": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz", - "integrity": "sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug==" - }, "node_modules/deep-equal": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", @@ -9032,9 +9048,9 @@ } }, "node_modules/immer": { - "version": "9.0.19", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.19.tgz", - "integrity": "sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==", + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -14389,9 +14405,9 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, "node_modules/react-redux": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz", - "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", + "integrity": "sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==", "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", @@ -14406,7 +14422,7 @@ "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0", "react-native": ">=0.59", - "redux": "^4" + "redux": "^4 || ^5.0.0-beta.0" }, "peerDependenciesMeta": { "@types/react": { @@ -14574,14 +14590,6 @@ "@babel/runtime": "^7.9.2" } }, - "node_modules/redux-logger": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz", - "integrity": "sha512-JoCIok7bg/XpqA1JqCqXFypuqBbQzGQySrhFzewB7ThcnysTO30l4VCst86AuB9T9tuT03MAA56Jw2PNhRSNCg==", - "dependencies": { - "deep-diff": "^0.3.5" - } - }, "node_modules/redux-thunk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", @@ -14727,6 +14735,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", diff --git a/Chapter2-start/package.json b/Chapter2-start/package.json index 491884b..45f2357 100644 --- a/Chapter2-start/package.json +++ b/Chapter2-start/package.json @@ -3,12 +3,14 @@ "version": "0.1.0", "private": true, "dependencies": { + "@reduxjs/toolkit": "^1.9.5", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", - "axios": "^1.3.4", + "axios": "^1.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-redux": "^8.1.2", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, diff --git a/Chapter2-start/src/App.js b/Chapter2-start/src/App.js index 79fbb1d..77844cd 100644 --- a/Chapter2-start/src/App.js +++ b/Chapter2-start/src/App.js @@ -1,15 +1,22 @@ +import { useSelector } from 'react-redux'; import './App.css'; import Account from './components/Account'; import Bonus from './components/Bonus'; +import Reward from './components/Reward'; + function App() { + const amount = useSelector(state => state.account.amount); + const bonus = useSelector(state => state.bonus.point) + const pending = useSelector(state => state.account.pending); return (

App

-

Current Amount :

-

Total Bonus :

+

Current Amount : {pending ? 'Loadiiing...' : amount}

+

Total Bonus : {pending ? 'Loading....' : bonus}

+
); } diff --git a/Chapter2-start/src/components/Account.js b/Chapter2-start/src/components/Account.js index 1c76ade..d07a6da 100644 --- a/Chapter2-start/src/components/Account.js +++ b/Chapter2-start/src/components/Account.js @@ -1,20 +1,9 @@ -import { useState } from 'react'; - -function Account() { - const [account, setAccount] = useState({ amount: 0 }); - const [value, setValue] = useState(0); - - const increment = () => { - setAccount({ amount: account.amount + 1 }); - }; - - const decrement = () => { - setAccount({ amount: account.amount - 1 }); - }; +import { useSelector, useDispatch } from 'react-redux'; +import { increment, decrement, incrementByAmount, getUser } from '../slices/accountSlice'; - const incrementByAmount = (value) => { - setAccount({ amount: account.amount + value }); - }; +function Account() { + const amount = useSelector(state => state.account.amount); + const dispatch = useDispatch(); return (
@@ -22,12 +11,14 @@ function Account() {

Account Component

-

Amount:${account.amount}

- - - setValue(+e.target.value)}> - + + +
diff --git a/Chapter2-start/src/components/Bonus.js b/Chapter2-start/src/components/Bonus.js index 2f5b83a..ba0b46c 100644 --- a/Chapter2-start/src/components/Bonus.js +++ b/Chapter2-start/src/components/Bonus.js @@ -1,20 +1,18 @@ -import { useState } from 'react'; +import { useSelector, useDispatch } from "react-redux"; +import { increment } from "../slices/bonusSlice"; function Bonus() { - const [points, setPoints] = useState({ value: 0 }); + const point = useSelector(state => state.bonus.point); + const dispatch = useDispatch(); - const increment = () => { - setPoints({ value: points.value + 1 }); - }; return (

Bonus Component

-

Total Point : ${points.value}

- - +

Total Point : ${point}

+
); diff --git a/Chapter2-start/src/components/Reward.js b/Chapter2-start/src/components/Reward.js new file mode 100644 index 0000000..6f9e513 --- /dev/null +++ b/Chapter2-start/src/components/Reward.js @@ -0,0 +1,21 @@ +import { useSelector, useDispatch } from "react-redux"; +import { increment } from "../reducers/reducer"; + +function Reward() { + const point = useSelector(state => state.reward.rewardPoints); + const dispatch = useDispatch(); + + return ( +
+
+

+ Reward Component +

+

Total Point : ${point}

+ +
+
+ ); +} + +export default Reward; diff --git a/Chapter2-start/src/index.js b/Chapter2-start/src/index.js index ffea4d1..485f39b 100644 --- a/Chapter2-start/src/index.js +++ b/Chapter2-start/src/index.js @@ -3,12 +3,27 @@ import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; +import { configureStore } from '@reduxjs/toolkit'; +import accountReducer from './slices/accountSlice'; +import bonusReducer from './slices/bonusSlice'; +import { rewardReducer } from './reducers/reducer'; +import { Provider } from 'react-redux'; + +const store = configureStore({ + reducer: { + account: accountReducer, + bonus: bonusReducer, + reward: rewardReducer + } +}) const root = ReactDOM.createRoot(document.getElementById('root')); root.render( + + ); diff --git a/Chapter2-start/src/reducers/reducer.js b/Chapter2-start/src/reducers/reducer.js new file mode 100644 index 0000000..6654232 --- /dev/null +++ b/Chapter2-start/src/reducers/reducer.js @@ -0,0 +1,13 @@ +import { createAction, createReducer } from '@reduxjs/toolkit' + +export const increment = createAction('reward/increment') + +const initialState = { rewardPoints: 0 } + +export const rewardReducer = createReducer(initialState, (builder) => { + builder + .addCase(increment, (state, action) => { + state.rewardPoints++ + }) +}) + diff --git a/Chapter2-start/src/slices/accountSlice.js b/Chapter2-start/src/slices/accountSlice.js new file mode 100644 index 0000000..f4dde0e --- /dev/null +++ b/Chapter2-start/src/slices/accountSlice.js @@ -0,0 +1,51 @@ +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' +import axios from 'axios' + +export const getUser = createAsyncThunk( + 'account/getUser', + async (userId, thunkAPI) => { + await new Promise((resolve, rejected) => { + setTimeout(() => { + resolve() + }, 5000); + }) + const { data } = await axios(`http://localhost:8080/accounts/${userId}`); + + return data; + } +) + +const initialState = { + amount: 1, + pending: false +} + +export const accountSlice = createSlice({ + name: 'account', + initialState, + reducers: { + increment: (state) => { + state.amount += 1 + }, + decrement: (state) => { + state.amount -= 1 + }, + incrementByAmount: (state, action) => { + state.amount += action.payload + } + }, + extraReducers: (builder) => { + builder.addCase(getUser.pending, (state, action) => { + state.pending = true; + }).addCase(getUser.fulfilled, (state, action) => { + state.pending = false; + }).addCase(getUser.rejected, (state, action) => { + state.pending = false; + }) + } +}) + +// Action creators are generated for each case reducer function +export const { increment, decrement, incrementByAmount } = accountSlice.actions + +export default accountSlice.reducer \ No newline at end of file diff --git a/Chapter2-start/src/slices/bonusSlice.js b/Chapter2-start/src/slices/bonusSlice.js new file mode 100644 index 0000000..bf51038 --- /dev/null +++ b/Chapter2-start/src/slices/bonusSlice.js @@ -0,0 +1,29 @@ +import { createSlice, createAction } from '@reduxjs/toolkit' + +const incrementByAmount = createAction('account/incrementByAmount') + +const initialState = { + point: 1, +} + +export const bonusSlice = createSlice({ + name: 'bonus', + initialState, + reducers: { + increment: (state) => { + state.point += 1 + }, + }, + extraReducers: (builder) => { + builder.addCase(incrementByAmount, (state, action) => { + if (action.payload > 100) { + state.point += 1 + } + }) + } +}) + +// Action creators are generated for each case reducer function +export const { increment } = bonusSlice.actions + +export default bonusSlice.reducer \ No newline at end of file