├── .env
├── .env.test
├── .gitignore
├── LICENSE
├── README.md
├── mocks
├── data
│ └── users.json
├── handlers
│ └── index.ts
└── server
│ └── index.ts
├── package.json
├── public
└── index.html
├── src
├── App.tsx
├── api
│ ├── getUsers.ts
│ └── gif.ts
├── components
│ ├── common
│ │ ├── Gif.tsx
│ │ └── Header.tsx
│ ├── coreRedux
│ │ ├── CatGame.tsx
│ │ ├── TodoList.css
│ │ ├── TodoList.tsx
│ │ └── UsersList.tsx
│ ├── onlyRQ
│ │ ├── CatGame.tsx
│ │ ├── UsersList.tsx
│ │ └── containers.test.tsx
│ ├── reduxToolkit
│ │ ├── CatGame.tsx
│ │ ├── TodoList.css
│ │ ├── TodoList.tsx
│ │ ├── UsersList.tsx
│ │ └── containers.test.tsx
│ ├── toolkitWithRQ
│ │ ├── CatGame.tsx
│ │ ├── UsersList.tsx
│ │ └── containers.test.tsx
│ └── toolkitWithRTKQ
│ │ ├── CatGame.tsx
│ │ └── UsersList.tsx
├── containers
│ └── coreRedux
│ │ ├── CatGameContainer.ts
│ │ ├── TodosContainer.ts
│ │ ├── UsersContainer.ts
│ │ └── containers.test.tsx
├── hoc
│ └── withReduxStore.tsx
├── index.tsx
├── pages
│ ├── CoreRedux.tsx
│ ├── NoMatch.tsx
│ ├── RQOnly.tsx
│ ├── ReduxTodo.tsx
│ ├── ReduxToolkit.tsx
│ ├── ToolKitAndRQ.tsx
│ ├── ToolKitAndRTKQ.tsx
│ └── ToolkitTodo.tsx
├── react-app-env.d.ts
├── setupTests.ts
├── stores
│ ├── coreRedux
│ │ ├── gif
│ │ │ ├── actions.ts
│ │ │ ├── gifReducers.ts
│ │ │ └── sagas.ts
│ │ ├── index.ts
│ │ ├── reducers.ts
│ │ ├── sagas.ts
│ │ ├── todo
│ │ │ ├── actions.ts
│ │ │ ├── counterReducers.ts
│ │ │ ├── selectedTodosReducers.ts
│ │ │ └── todoReducers.ts
│ │ └── users
│ │ │ ├── actions.ts
│ │ │ ├── sagas.ts
│ │ │ └── usersReducers.ts
│ ├── reduxToolkit
│ │ ├── features
│ │ │ ├── gif
│ │ │ │ ├── reducers.ts
│ │ │ │ └── sagas.ts
│ │ │ ├── todo
│ │ │ │ ├── counterReducers.ts
│ │ │ │ ├── selectedTodosReducers.ts
│ │ │ │ └── todoReducers.ts
│ │ │ └── users
│ │ │ │ ├── reducers.ts
│ │ │ │ └── sagas.ts
│ │ ├── index.ts
│ │ └── sagas.ts
│ ├── toolkitWithRQ
│ │ ├── features
│ │ │ ├── gif
│ │ │ │ └── reducers.ts
│ │ │ └── users
│ │ │ │ └── reducers.ts
│ │ └── index.ts
│ ├── toolkitWithRTKQ
│ │ ├── features
│ │ │ ├── gif
│ │ │ │ └── reducers.ts
│ │ │ └── users
│ │ │ │ └── reducers.ts
│ │ ├── index.ts
│ │ └── services
│ │ │ ├── gif.ts
│ │ │ └── users.ts
│ └── types.ts
└── styles.css
├── tsconfig.json
└── yarn.lock
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_GIF_API_URL=https://ruddy-mail.glitch.me/api/gacha
2 | REACT_APP_USERS_API_URL=https://jsonplaceholder.typicode.com/users
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | REACT_APP_GIF_API_URL=/gacha
2 | REACT_APP_USERS_API_URL=/users
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Tarun Kumar Sukhu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # redux-and-react-query
2 |
3 | Created with CodeSandbox
4 |
5 | - This project showcases two redux examples that also use redux saga for async API (Core Redux)
6 |
7 | - The same examples are reimplemented using different techniques
8 | 1. Pure Redux + Redux Saga
9 | 2. Redux Toolkit instead of Pure Redux (with Redux Saga)
10 | 3. Redux Toolkit with React Query (without Redux Saga)
11 | 4. Redux Toolkit with experimental RTK Query (without Redux Sage)
12 | 5. Pure React Query
13 | 6. Redux and Redux Toolkit complex state management comparison (Todo App)
14 |
15 |
16 | ## Code Structure
17 | - **pages**: Page Routes for the app
18 | - **hoc**: Higher Order Components
19 | - **containers**: Redux Containers
20 | - **components**: React components organized by store types
21 | - **stores**: Store code organized by store types
22 | - **api**: API helpers
23 |
24 | ### Store Type Folders
25 |
26 | - `coreRedux` : Traditional Redux Code + Saga
27 | - `reduxtToolkit` : Redux Toolkit Based Store + Saga
28 | - `toolkitWithRQ` : Redux as the global store with `react-query` for handling the FE server state
29 | - `toolkitWithRTKQ` : Redux as the global store with `Redux Toolkit Query` for handling the FE server state
30 | - `onlyRQ` : All state managed within the components with server state handled with `react-query`
31 |
32 | ## Testing
33 |
34 | - Focus is on testing the application and features rather than the implementation details.
35 | - This is where the `testing-library` comes in and helped achieve this.
36 | - Also as Kent C Dodds mentions in his [blog](https://kentcdodds.com/blog/write-tests) where the focus should be more on the integration level tests where we get the most confidence on the application , rather than focusing on areas which are known to work (like action creators or low level implementation of the state management). Test these at the application level.
37 | - To compliment this also adding in the project is `msw` or Mock Service Worker [https://mswjs.io/](https://mswjs.io/) , so that we mock the APIs at the network level rather than changing the internal implemention of client API libraries. This will ensure the test and the actual application code are being tested under the same configuration and code without any extra testing intercepts and the client API level.
38 |
39 |
40 | ## Setup
41 |
42 | ```
43 | yarn
44 | ```
45 |
46 | ## Run the App
47 |
48 | ```
49 | yarn start
50 | ```
51 |
52 | ## Run the tests
53 |
54 | ```
55 | yarn test
56 | ```
57 |
58 | ## Findings
59 |
60 | - While this is a small subset , here are the findings
61 |
62 | 1. `Redux Toolkit` helps to reduce the boilerplate code and make the code more effecient with OOTB `immer` and `reselect` making the code cleaner and simple to understand
63 |
64 | 2. By using React Query adds additional benefit of further reducing the code as well as bringing the all the features of `react-query`. Also we can get rid of all our `redux-saga` related code. There are advanced features from `react-query` like `pre-fetching` ,`depended queries` , `parrallel queries` , `background fetching indicators` that can also be leveraged for more advanced use cases.
65 |
66 | 3. The amount of global state maintaned can be further reduces with `Frontend Server State` maintained with `react-query` , making the overall code more efficient.
67 |
68 | 4. RTK Query once integrated (and a product release is available) with RTK is likely to be become a defacto standard for async , `Frontend Server State` if redux is being as part of the application.
69 |
70 | ## Lines of Code Differences
71 |
72 | | Technique | LOC | % Savings |
73 | |-----------|-----|-----------|
74 | |Pure Redux| 339 | |
75 | |Redux Toolkit| 276| 18|
76 | |React Query with Redux Toolkit| 212 | 37|
77 | |Redux Toolkit with RTK Query| 231 | 31|
78 | |Only React Query| 122| 64|
79 | |Todo (Pure Redux)| 437 | |
80 | |Todo (Redux Toolkit)| 306 | 29|
81 |
82 |
83 | ## Credits
84 |
85 | Todo app is an adaption from the [redux-toolkit-comparison](https://github.com/angle943/redux-toolkit-comparison) project.
--------------------------------------------------------------------------------
/mocks/data/users.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "name": "Leanne Graham",
5 | "username": "Bret",
6 | "email": "Sincere@april.biz",
7 | "address": {
8 | "street": "Kulas Light",
9 | "suite": "Apt. 556",
10 | "city": "Gwenborough",
11 | "zipcode": "92998-3874",
12 | "geo": {
13 | "lat": "-37.3159",
14 | "lng": "81.1496"
15 | }
16 | },
17 | "phone": "1-770-736-8031 x56442",
18 | "website": "hildegard.org",
19 | "company": {
20 | "name": "Romaguera-Crona",
21 | "catchPhrase": "Multi-layered client-server neural-net",
22 | "bs": "harness real-time e-markets"
23 | }
24 | },
25 | {
26 | "id": 2,
27 | "name": "Ervin Howell",
28 | "username": "Antonette",
29 | "email": "Shanna@melissa.tv",
30 | "address": {
31 | "street": "Victor Plains",
32 | "suite": "Suite 879",
33 | "city": "Wisokyburgh",
34 | "zipcode": "90566-7771",
35 | "geo": {
36 | "lat": "-43.9509",
37 | "lng": "-34.4618"
38 | }
39 | },
40 | "phone": "010-692-6593 x09125",
41 | "website": "anastasia.net",
42 | "company": {
43 | "name": "Deckow-Crist",
44 | "catchPhrase": "Proactive didactic contingency",
45 | "bs": "synergize scalable supply-chains"
46 | }
47 | },
48 | {
49 | "id": 3,
50 | "name": "Clementine Bauch",
51 | "username": "Samantha",
52 | "email": "Nathan@yesenia.net",
53 | "address": {
54 | "street": "Douglas Extension",
55 | "suite": "Suite 847",
56 | "city": "McKenziehaven",
57 | "zipcode": "59590-4157",
58 | "geo": {
59 | "lat": "-68.6102",
60 | "lng": "-47.0653"
61 | }
62 | },
63 | "phone": "1-463-123-4447",
64 | "website": "ramiro.info",
65 | "company": {
66 | "name": "Romaguera-Jacobson",
67 | "catchPhrase": "Face to face bifurcated interface",
68 | "bs": "e-enable strategic applications"
69 | }
70 | },
71 | {
72 | "id": 4,
73 | "name": "Patricia Lebsack",
74 | "username": "Karianne",
75 | "email": "Julianne.OConner@kory.org",
76 | "address": {
77 | "street": "Hoeger Mall",
78 | "suite": "Apt. 692",
79 | "city": "South Elvis",
80 | "zipcode": "53919-4257",
81 | "geo": {
82 | "lat": "29.4572",
83 | "lng": "-164.2990"
84 | }
85 | },
86 | "phone": "493-170-9623 x156",
87 | "website": "kale.biz",
88 | "company": {
89 | "name": "Robel-Corkery",
90 | "catchPhrase": "Multi-tiered zero tolerance productivity",
91 | "bs": "transition cutting-edge web services"
92 | }
93 | },
94 | {
95 | "id": 5,
96 | "name": "Chelsey Dietrich",
97 | "username": "Kamren",
98 | "email": "Lucio_Hettinger@annie.ca",
99 | "address": {
100 | "street": "Skiles Walks",
101 | "suite": "Suite 351",
102 | "city": "Roscoeview",
103 | "zipcode": "33263",
104 | "geo": {
105 | "lat": "-31.8129",
106 | "lng": "62.5342"
107 | }
108 | },
109 | "phone": "(254)954-1289",
110 | "website": "demarco.info",
111 | "company": {
112 | "name": "Keebler LLC",
113 | "catchPhrase": "User-centric fault-tolerant solution",
114 | "bs": "revolutionize end-to-end systems"
115 | }
116 | },
117 | {
118 | "id": 6,
119 | "name": "Mrs. Dennis Schulist",
120 | "username": "Leopoldo_Corkery",
121 | "email": "Karley_Dach@jasper.info",
122 | "address": {
123 | "street": "Norberto Crossing",
124 | "suite": "Apt. 950",
125 | "city": "South Christy",
126 | "zipcode": "23505-1337",
127 | "geo": {
128 | "lat": "-71.4197",
129 | "lng": "71.7478"
130 | }
131 | },
132 | "phone": "1-477-935-8478 x6430",
133 | "website": "ola.org",
134 | "company": {
135 | "name": "Considine-Lockman",
136 | "catchPhrase": "Synchronised bottom-line interface",
137 | "bs": "e-enable innovative applications"
138 | }
139 | },
140 | {
141 | "id": 7,
142 | "name": "Kurtis Weissnat",
143 | "username": "Elwyn.Skiles",
144 | "email": "Telly.Hoeger@billy.biz",
145 | "address": {
146 | "street": "Rex Trail",
147 | "suite": "Suite 280",
148 | "city": "Howemouth",
149 | "zipcode": "58804-1099",
150 | "geo": {
151 | "lat": "24.8918",
152 | "lng": "21.8984"
153 | }
154 | },
155 | "phone": "210.067.6132",
156 | "website": "elvis.io",
157 | "company": {
158 | "name": "Johns Group",
159 | "catchPhrase": "Configurable multimedia task-force",
160 | "bs": "generate enterprise e-tailers"
161 | }
162 | },
163 | {
164 | "id": 8,
165 | "name": "Nicholas Runolfsdottir V",
166 | "username": "Maxime_Nienow",
167 | "email": "Sherwood@rosamond.me",
168 | "address": {
169 | "street": "Ellsworth Summit",
170 | "suite": "Suite 729",
171 | "city": "Aliyaview",
172 | "zipcode": "45169",
173 | "geo": {
174 | "lat": "-14.3990",
175 | "lng": "-120.7677"
176 | }
177 | },
178 | "phone": "586.493.6943 x140",
179 | "website": "jacynthe.com",
180 | "company": {
181 | "name": "Abernathy Group",
182 | "catchPhrase": "Implemented secondary concept",
183 | "bs": "e-enable extensible e-tailers"
184 | }
185 | },
186 | {
187 | "id": 9,
188 | "name": "Glenna Reichert",
189 | "username": "Delphine",
190 | "email": "Chaim_McDermott@dana.io",
191 | "address": {
192 | "street": "Dayna Park",
193 | "suite": "Suite 449",
194 | "city": "Bartholomebury",
195 | "zipcode": "76495-3109",
196 | "geo": {
197 | "lat": "24.6463",
198 | "lng": "-168.8889"
199 | }
200 | },
201 | "phone": "(775)976-6794 x41206",
202 | "website": "conrad.com",
203 | "company": {
204 | "name": "Yost and Sons",
205 | "catchPhrase": "Switchable contextually-based project",
206 | "bs": "aggregate real-time technologies"
207 | }
208 | },
209 | {
210 | "id": 10,
211 | "name": "Clementina DuBuque",
212 | "username": "Moriah.Stanton",
213 | "email": "Rey.Padberg@karina.biz",
214 | "address": {
215 | "street": "Kattie Turnpike",
216 | "suite": "Suite 198",
217 | "city": "Lebsackbury",
218 | "zipcode": "31428-2261",
219 | "geo": {
220 | "lat": "-38.2386",
221 | "lng": "57.2232"
222 | }
223 | },
224 | "phone": "024-648-3804",
225 | "website": "ambrose.net",
226 | "company": {
227 | "name": "Hoeger LLC",
228 | "catchPhrase": "Centralized empowering task-force",
229 | "bs": "target end-to-end models"
230 | }
231 | }
232 | ]
--------------------------------------------------------------------------------
/mocks/handlers/index.ts:
--------------------------------------------------------------------------------
1 | // src/mocks/handlers.js
2 | import { rest } from "msw";
3 | import users from "../data/users.json";
4 | export const handlers = [
5 | // Handles a GET /gif request
6 | rest.get(`${process.env.REACT_APP_GIF_API_URL}`, (_req, res, ctx) => {
7 | return res(
8 | ctx.json({
9 | url: "https://media.giphy.com/media/Is0AJv4CEj9hm/giphy.gif",
10 | })
11 | );
12 | }),
13 | // Handles a GET /users request
14 | rest.get(`${process.env.REACT_APP_USERS_API_URL}`, (_req, res, ctx) => {
15 | return res(ctx.json(users));
16 | }),
17 | ];
18 |
--------------------------------------------------------------------------------
/mocks/server/index.ts:
--------------------------------------------------------------------------------
1 | // src/mocks/server.ts
2 | import { setupServer } from "msw/node";
3 | import { handlers } from "../handlers";
4 | // This configures a request mocking server with the given request handlers.
5 | export const server = setupServer(...handlers);
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-saga-sample",
3 | "version": "1.0.0",
4 | "description": "",
5 | "keywords": [],
6 | "main": "src/index.js",
7 | "dependencies": {
8 | "@reduxjs/toolkit": "1.5.0",
9 | "@rtk-incubator/rtk-query": "^0.2.0",
10 | "@types/jest": "^26.0.20",
11 | "@types/node": "14.14.25",
12 | "@types/react": "17.0.1",
13 | "@types/react-dom": "17.0.0",
14 | "@types/react-router-dom": "^5.1.7",
15 | "@types/uuid": "^8.3.0",
16 | "bulma": "0.9.2",
17 | "react": "17.0.1",
18 | "react-dom": "17.0.1",
19 | "react-query": "3.8.2",
20 | "react-redux": "7.2.2",
21 | "react-router-dom": "5.2.0",
22 | "react-scripts": "4.0.2",
23 | "redux": "^4.0.5",
24 | "redux-devtools-extension": "2.13.8",
25 | "redux-saga": "1.1.3",
26 | "typescript": "4.1.5",
27 | "uuid": "^8.3.2"
28 | },
29 | "scripts": {
30 | "start": "react-scripts start",
31 | "build": "react-scripts build",
32 | "test": "react-scripts test --env=jsdom",
33 | "eject": "react-scripts eject"
34 | },
35 | "browserslist": [
36 | ">0.2%",
37 | "not dead",
38 | "not ie <= 11",
39 | "not op_mini all"
40 | ],
41 | "devDependencies": {
42 | "@babel/preset-typescript": "^7.12.16",
43 | "@testing-library/dom": "^7.29.4",
44 | "@testing-library/jest-dom": "^5.11.9",
45 | "@testing-library/react": "^11.2.5",
46 | "@testing-library/user-event": "^12.7.1",
47 | "@types/react-redux": "^7.1.16",
48 | "cross-env": "^7.0.3",
49 | "msw": "^0.26.2",
50 | "sass": "^1.32.7",
51 | "ts-node": "^9.1.1"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
23 | React App
24 |
25 |
26 |
27 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Route, Switch } from "react-router-dom";
3 | import Header from "./components/common/Header";
4 | import CoreRedux from "./pages/CoreRedux";
5 | import ReduxToolkit from "./pages/ReduxToolkit";
6 | import ToolKitAndRQ from "./pages/ToolKitAndRQ";
7 | import ToolKitAndRTKQ from "./pages/ToolKitAndRTKQ";
8 | import ReduxTodo from "./pages/ReduxTodo";
9 | import ToolkitTodo from "./pages/ToolkitTodo";
10 | import RQOnly from "./pages/RQOnly";
11 | import NoMatch from "./pages/NoMatch";
12 |
13 | /**
14 | * Main App
15 | */
16 | const App: React.FC = () => {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/src/api/getUsers.ts:
--------------------------------------------------------------------------------
1 |
2 | export const getUsers = () => {
3 | return fetch(`${process.env.REACT_APP_USERS_API_URL}`)
4 | .then((response) => response.json())
5 | .catch((error) => ({ error }));
6 | };
7 |
--------------------------------------------------------------------------------
/src/api/gif.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | random() {
3 | return fetch(`${process.env.REACT_APP_GIF_API_URL}`).then(response => response.json());
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/common/Gif.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | interface GifProps {
4 | imageUrl: string;
5 | }
6 |
7 | const Gif: React.FC = ({ imageUrl }) => {
8 | return (
9 |
10 |
11 |
12 | GIFs by GIPHY
13 |
14 |
15 | );
16 | };
17 |
18 | export default Gif;
19 |
--------------------------------------------------------------------------------
/src/components/common/Header.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Link, useLocation } from "react-router-dom";
3 |
4 | const Header = () => {
5 | const location = useLocation();
6 | return (
7 |
8 |
9 |
10 | -
11 | Core Redux
12 |
13 | -
16 | Redux Toolkit
17 |
18 | -
23 | Toolkit & React Query
24 |
25 | -
30 | Toolkit & RTK Query
31 |
32 | -
33 | React Query Only
34 |
35 |
36 |
37 |
38 | -
39 | Core Redux Todo
40 |
41 | -
42 | Reduxtoolkit Todo
43 |
44 |
45 |
46 |
47 | );
48 | };
49 | export default Header;
50 |
--------------------------------------------------------------------------------
/src/components/coreRedux/CatGame.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Gif from "../common/Gif";
3 |
4 | function renderGif({ imageUrl, loading, error } : { imageUrl: string, loading: boolean, error: boolean}) {
5 | if (error) {
6 | return Error!!
;
7 | }
8 |
9 | if (loading) {
10 | return Loading...
;
11 | }
12 |
13 | return imageUrl ? (
14 |
15 | ) : (
16 | Welcome to the cat game!
17 | );
18 | }
19 |
20 | interface CatGameProps {
21 | imageUrl: string;
22 | loading: boolean;
23 | error: boolean;
24 | play: () => void;
25 | clear: () => void;
26 | }
27 |
28 | const CatGame: React.FC = ({
29 | imageUrl,
30 | loading,
31 | error,
32 | play,
33 | clear
34 | }) => {
35 | return (
36 |
37 | Cat Game
38 | {renderGif({ imageUrl, loading, error })}
39 |
40 |
41 |
44 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default CatGame;
53 |
--------------------------------------------------------------------------------
/src/components/coreRedux/TodoList.css:
--------------------------------------------------------------------------------
1 | .App__counter {
2 | height: 60px;
3 | display: flex;
4 | align-items: center;
5 | margin-left: 64px;
6 | font-size: 18px;
7 | }
8 |
9 | .App__header {
10 | align-items: center;
11 | background: rgb(118, 74, 188);
12 | color: white;
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: center;
16 | padding: 64px 0;
17 | }
18 |
19 | h1 {
20 | font-size: 48px;
21 | font-weight: 700;
22 | }
23 |
24 | input {
25 | background: rgb(235, 237, 240);
26 | border-radius: 32px;
27 | border: none;
28 | color: rgb(68, 73, 80);
29 | cursor: text;
30 | font-size: 14.4px;
31 | font-weight: 400;
32 | line-height: 32px;
33 | margin: 0 16px;
34 | padding: 0 16px;
35 | width: 200px;
36 | }
37 |
38 | input:focus {
39 | outline: none;
40 | }
41 |
42 | button {
43 | background: rgb(249, 250, 251);
44 | border-radius: 6px;
45 | color: rgb(28, 30, 33);
46 | cursor: pointer;
47 | font-size: 17.28px;
48 | font-weight: 700;
49 | line-height: 25.92px;
50 | margin-right: 8px;
51 | transition: all 0.1s ease-in;
52 | }
53 |
54 | button:focus {
55 | outline: none;
56 | }
57 |
58 | button:hover {
59 | background: white;
60 | color: rgb(92, 61, 143);
61 | }
62 |
63 | button:active {
64 | background: rgb(200, 201, 204);
65 | color: rgb(28, 30, 33);
66 | }
67 |
68 | .App__body {
69 | display: grid;
70 | grid-template-columns: repeat(2, 1fr);
71 | grid-row-gap: 16px;
72 | margin: 32px 78px;
73 | }
74 |
75 | .App__list {
76 | list-style-type: none;
77 | font-size: 17px;
78 | color: rgb(28, 30, 33);
79 | line-height: 25.5px;
80 | margin-block-start: 0;
81 | margin-block-end: 0;
82 | }
83 |
84 | h2 {
85 | margin-block-start: 0;
86 | }
87 |
88 | li {
89 | cursor: pointer;
90 | margin: 6px 0;
91 | }
92 |
93 | li:hover {
94 | color: rgb(118, 74, 188);
95 | }
96 |
97 | li.active {
98 | font-weight: bold;
99 | }
100 |
101 | .done {
102 | text-decoration: line-through;
103 | }
104 |
105 | .list-number {
106 | display: inline-block;
107 | width: 24px;
108 | }
109 |
110 | .empty-state {
111 | color: rgb(200, 201, 204);
112 | font-style: italic;
113 | }
114 |
115 | .todo-desc {
116 | display: block;
117 | font-weight: bold;
118 | }
119 |
120 | .todo-actions {
121 | margin-top: 16px;
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/coreRedux/TodoList.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | ChangeEvent,
3 | FormEvent,
4 | useEffect,
5 | useRef,
6 | useState,
7 | } from "react";
8 | import { Todo } from "../../stores/types";
9 | import './TodoList.css';
10 | interface TodoListProps {
11 | todos: Todo[];
12 | selectedTodoId: string;
13 | counter: number;
14 | createTodo: (todo: { desc: string }) => void;
15 | deleteTodo: (todo: { id: string }) => void;
16 | editTodo: (todo: { id: string; desc: string }) => void;
17 | toggleTodo: (todo: { id: string; isComplete: boolean }) => void;
18 | selectTodo: (todo: { id: string }) => void;
19 | }
20 |
21 | const TodoList: React.FC = ({
22 | todos,
23 | selectedTodoId,
24 | counter,
25 | createTodo,
26 | deleteTodo,
27 | editTodo,
28 | toggleTodo,
29 | selectTodo,
30 | }) => {
31 | const [newTodoInput, setNewTodoInput] = useState("");
32 | const [editTodoInput, setEditTodoInput] = useState("");
33 | const [isEditMode, setIsEditMode] = useState(false);
34 | const editInput = useRef(null);
35 |
36 | const selectedTodo =
37 | (selectedTodoId && todos.find((todo) => todo.id === selectedTodoId)) ||
38 | null;
39 |
40 | const handleNewInputChange = (e: ChangeEvent): void => {
41 | setNewTodoInput(e.target.value);
42 | };
43 |
44 | const handleEditInputChange = (e: ChangeEvent): void => {
45 | setEditTodoInput(e.target.value);
46 | };
47 |
48 | const handleCreateNewTodo = (e: FormEvent): void => {
49 | e.preventDefault();
50 | if (!newTodoInput.length) return;
51 |
52 | createTodo({ desc: newTodoInput });
53 | setNewTodoInput("");
54 | };
55 |
56 | const handleSelectTodo = (todoId: string) => (): void => {
57 | selectTodo({ id: todoId });
58 | };
59 |
60 | const handleEdit = (): void => {
61 | if (!selectedTodo) return;
62 |
63 | setEditTodoInput(selectedTodo.desc);
64 | setIsEditMode(true);
65 | };
66 |
67 | useEffect(() => {
68 | if (isEditMode) {
69 | editInput?.current?.focus();
70 | }
71 | }, [isEditMode]);
72 |
73 | const handleUpdate = (e: FormEvent): void => {
74 | e.preventDefault();
75 |
76 | if (!editTodoInput.length || !selectedTodoId) {
77 | handleCancelUpdate();
78 | return;
79 | }
80 |
81 | editTodo({ id: selectedTodoId, desc: editTodoInput });
82 | setIsEditMode(false);
83 | setEditTodoInput("");
84 | };
85 |
86 | const handleCancelUpdate = (
87 | e?: React.MouseEvent
88 | ): void => {
89 | e?.preventDefault();
90 | setIsEditMode(false);
91 | setEditTodoInput("");
92 | };
93 |
94 | const handleToggle = (): void => {
95 | if (!selectedTodoId || !selectedTodo) return;
96 |
97 | toggleTodo({
98 | id: selectedTodoId,
99 | isComplete: !selectedTodo.isComplete,
100 | });
101 | };
102 |
103 | const handleDelete = (): void => {
104 | if (!selectedTodoId) return;
105 |
106 | deleteTodo({ id: selectedTodoId });
107 | };
108 |
109 | return (
110 |
111 |
Todos Updated Count: {counter}
112 |
113 |
Todo: Redux vs RTK Edition
114 |
123 |
124 |
125 |
126 | My Todos:
127 | {todos.map((todo, i) => (
128 | -
135 | {i + 1}) {todo.desc}
136 |
137 | ))}
138 |
139 |
140 |
Selected Todo:
141 | {selectedTodo === null ? (
142 |
No Todo Selected
143 | ) : !isEditMode ? (
144 | <>
145 |
150 | {selectedTodo.desc}
151 |
152 |
153 |
154 |
155 |
156 |
157 | >
158 | ) : (
159 |
169 | )}
170 |
171 |
172 |
173 | );
174 | };
175 |
176 | export default TodoList;
177 |
--------------------------------------------------------------------------------
/src/components/coreRedux/UsersList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | interface UsersListProps {
4 | users: any[];
5 | loading: boolean;
6 | error: boolean;
7 | load: () => void;
8 | }
9 |
10 | const UsersList: React.FC = ({
11 | users,
12 | loading,
13 | error,
14 | load
15 | }) => {
16 | React.useEffect(() => {
17 | load();
18 | }, []);
19 | return (
20 | <>
21 |
22 | Users List
23 | {error && Error!!
}
24 | {loading && Loading...
}
25 |
26 |
27 |
28 |
29 | Name
30 | |
31 |
32 |
33 |
34 | {users &&
35 | users.map(({ id, name }) => (
36 |
37 | {name} |
38 |
39 | ))}
40 |
41 |
42 |
43 | >
44 | );
45 | };
46 |
47 | export default UsersList;
48 |
--------------------------------------------------------------------------------
/src/components/onlyRQ/CatGame.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Gif from "../common/Gif";
3 | import { useQuery } from "react-query";
4 | import gifApi from "../../api/gif";
5 |
6 | function renderGif({ data, isFetching, error }: {data: {url: string}, isFetching: boolean, error: any}) {
7 | if (error) {
8 | return Error!!
;
9 | }
10 |
11 | if (isFetching) {
12 | return Loading...
;
13 | }
14 |
15 | return data && data.url ? (
16 |
17 | ) : (
18 | Welcome to the cat game!
19 | );
20 | }
21 |
22 | const CatGameRQ: React.FC = () => {
23 | const [play, setPlay] = React.useState(false);
24 | const [clear, setClear] = React.useState(false);
25 | const { data, error, isFetching } = useQuery(
26 | ["gifs"],
27 | async () => await gifApi.random(),
28 | {
29 | enabled: !!play
30 | }
31 | );
32 |
33 | React.useEffect(() => {
34 | if (data) {
35 | setPlay(false);
36 | }
37 | }, [data]);
38 |
39 | return (
40 |
41 | Cat Game
42 | {!clear && renderGif({ data, isFetching, error })}
43 |
44 |
45 |
54 |
57 |
58 |
59 | );
60 | };
61 |
62 | export default CatGameRQ;
63 |
--------------------------------------------------------------------------------
/src/components/onlyRQ/UsersList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useQuery } from "react-query";
3 | import { getUsers } from "../../api/getUsers";
4 | const UsersListOnlyRQ: React.FC = () => {
5 | const { data, error, isFetching } = useQuery(
6 | ["users"],
7 | async () => await getUsers()
8 | );
9 | return (
10 | <>
11 |
12 | Users List
13 | {error && Error!!
}
14 | {isFetching && Loading...
}
15 |
16 |
17 |
18 |
19 | Name
20 | |
21 |
22 |
23 |
24 | {data &&
25 | data.map(({ id, name }: { id: any; name: string }) => (
26 |
27 | {name} |
28 |
29 | ))}
30 |
31 |
32 |
33 | >
34 | );
35 | };
36 |
37 | export default UsersListOnlyRQ;
38 |
--------------------------------------------------------------------------------
/src/components/onlyRQ/containers.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { QueryClient, QueryClientProvider } from "react-query";
3 | import { render, waitFor, screen } from "@testing-library/react";
4 | import userEvent from "@testing-library/user-event";
5 | import UsersList from "../onlyRQ/UsersList";
6 | import CatGame from "../onlyRQ/CatGame";
7 |
8 | const queryClient = new QueryClient();
9 |
10 | function renderWithRQ(component: React.ReactNode) {
11 | return {
12 | ...render(
13 |
14 | {component}
15 |
16 | ),
17 | };
18 | }
19 |
20 | it("renders with redux", async () => {
21 | renderWithRQ();
22 | const elem = await waitFor(() => screen.getByTestId(3));
23 | expect(elem).toHaveTextContent("Clementine Bauch");
24 | });
25 |
26 | it("fires play and renders with react-query", async () => {
27 | renderWithRQ();
28 | userEvent.click(screen.getByText("Play"));
29 | const elem = await waitFor(() => screen.getByTestId("cat-game"));
30 | expect(elem).toBeTruthy();
31 | });
32 |
--------------------------------------------------------------------------------
/src/components/reduxToolkit/CatGame.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Gif from "../common/Gif";
3 | import { useSelector, useDispatch } from "react-redux";
4 | import {
5 | clear,
6 | selectError,
7 | selectLoading,
8 | selectGif,
9 | fetchGifAsync
10 | } from "../../stores/reduxToolkit/features/gif/reducers";
11 |
12 | function renderGif({ imageUrl, loading, error }: {imageUrl: string, loading: boolean, error: any}) {
13 | if (error) {
14 | return Error!!
;
15 | }
16 |
17 | if (loading) {
18 | return Loading...
;
19 | }
20 |
21 | return imageUrl ? (
22 |
23 | ) : (
24 | Welcome to the cat game!
25 | );
26 | }
27 |
28 | const CatGameTK: React.FC = () => {
29 | const imageUrl = useSelector(selectGif);
30 | const loading = useSelector(selectLoading);
31 | const error = useSelector(selectError);
32 | const dispatch = useDispatch();
33 | return (
34 |
35 | Cat Game
36 | {renderGif({ imageUrl, loading, error })}
37 |
38 |
39 |
45 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default CatGameTK;
54 |
--------------------------------------------------------------------------------
/src/components/reduxToolkit/TodoList.css:
--------------------------------------------------------------------------------
1 | .App__counter {
2 | height: 60px;
3 | display: flex;
4 | align-items: center;
5 | margin-left: 64px;
6 | font-size: 18px;
7 | }
8 |
9 | .App__header {
10 | align-items: center;
11 | background: rgb(118, 74, 188);
12 | color: white;
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: center;
16 | padding: 64px 0;
17 | }
18 |
19 | h1 {
20 | font-size: 48px;
21 | font-weight: 700;
22 | }
23 |
24 | input {
25 | background: rgb(235, 237, 240);
26 | border-radius: 32px;
27 | border: none;
28 | color: rgb(68, 73, 80);
29 | cursor: text;
30 | font-size: 14.4px;
31 | font-weight: 400;
32 | line-height: 32px;
33 | margin: 0 16px;
34 | padding: 0 16px;
35 | width: 200px;
36 | }
37 |
38 | input:focus {
39 | outline: none;
40 | }
41 |
42 | button {
43 | background: rgb(249, 250, 251);
44 | border-radius: 6px;
45 | color: rgb(28, 30, 33);
46 | cursor: pointer;
47 | font-size: 17.28px;
48 | font-weight: 700;
49 | line-height: 25.92px;
50 | margin-right: 8px;
51 | transition: all 0.1s ease-in;
52 | }
53 |
54 | button:focus {
55 | outline: none;
56 | }
57 |
58 | button:hover {
59 | background: white;
60 | color: rgb(92, 61, 143);
61 | }
62 |
63 | button:active {
64 | background: rgb(200, 201, 204);
65 | color: rgb(28, 30, 33);
66 | }
67 |
68 | .App__body {
69 | display: grid;
70 | grid-template-columns: repeat(2, 1fr);
71 | grid-row-gap: 16px;
72 | margin: 32px 78px;
73 | }
74 |
75 | .App__list {
76 | list-style-type: none;
77 | font-size: 17px;
78 | color: rgb(28, 30, 33);
79 | line-height: 25.5px;
80 | margin-block-start: 0;
81 | margin-block-end: 0;
82 | }
83 |
84 | h2 {
85 | margin-block-start: 0;
86 | }
87 |
88 | li {
89 | cursor: pointer;
90 | margin: 6px 0;
91 | }
92 |
93 | li:hover {
94 | color: rgb(118, 74, 188);
95 | }
96 |
97 | li.active {
98 | font-weight: bold;
99 | }
100 |
101 | .done {
102 | text-decoration: line-through;
103 | }
104 |
105 | .list-number {
106 | display: inline-block;
107 | width: 24px;
108 | }
109 |
110 | .empty-state {
111 | color: rgb(200, 201, 204);
112 | font-style: italic;
113 | }
114 |
115 | .todo-desc {
116 | display: block;
117 | font-weight: bold;
118 | }
119 |
120 | .todo-actions {
121 | margin-top: 16px;
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/reduxToolkit/TodoList.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | ChangeEvent,
3 | FormEvent,
4 | useEffect,
5 | useRef,
6 | useState,
7 | } from "react";
8 | import { Todo } from "../../stores/types";
9 | import { useSelector, useDispatch } from "react-redux";
10 | import {
11 | selectTodo,
12 | selectSelectedTodoId,
13 | } from "../../stores/reduxToolkit/features/todo/selectedTodosReducers";
14 | import {
15 | selectTodos,
16 | deleteTodo,
17 | editTodo,
18 | createTodo,
19 | toggleTodo,
20 | } from "../../stores/reduxToolkit/features/todo/todoReducers";
21 | import { selectCounter } from "../../stores/reduxToolkit/features/todo/counterReducers";
22 | import "./TodoList.css";
23 |
24 | const TodoList: React.FC = () => {
25 | const dispatch = useDispatch();
26 | const selectedTodoId = useSelector(selectSelectedTodoId);
27 | const todos = useSelector(selectTodos);
28 | const counter = useSelector(selectCounter);
29 | const [newTodoInput, setNewTodoInput] = useState("");
30 | const [editTodoInput, setEditTodoInput] = useState("");
31 | const [isEditMode, setIsEditMode] = useState(false);
32 | const editInput = useRef(null);
33 |
34 | const selectedTodo =
35 | (selectedTodoId &&
36 | todos.find((todo: Todo) => todo.id === selectedTodoId)) ||
37 | null;
38 |
39 | const handleNewInputChange = (e: ChangeEvent): void => {
40 | setNewTodoInput(e.target.value);
41 | };
42 |
43 | const handleEditInputChange = (e: ChangeEvent): void => {
44 | setEditTodoInput(e.target.value);
45 | };
46 |
47 | const handleCreateNewTodo = (e: FormEvent): void => {
48 | e.preventDefault();
49 | if (!newTodoInput.length) return;
50 |
51 | dispatch(createTodo({ desc: newTodoInput }));
52 | setNewTodoInput("");
53 | };
54 |
55 | const handleSelectTodo = (todoId: string) => (): void => {
56 | dispatch(selectTodo({ id: todoId }));
57 | };
58 |
59 | const handleEdit = (): void => {
60 | if (!selectedTodo) return;
61 |
62 | setEditTodoInput(selectedTodo.desc);
63 | setIsEditMode(true);
64 | };
65 |
66 | useEffect(() => {
67 | if (isEditMode) {
68 | editInput?.current?.focus();
69 | }
70 | }, [isEditMode]);
71 |
72 | const handleUpdate = (e: FormEvent): void => {
73 | e.preventDefault();
74 |
75 | if (!editTodoInput.length || !selectedTodoId) {
76 | handleCancelUpdate();
77 | return;
78 | }
79 |
80 | dispatch(editTodo({ id: selectedTodoId, desc: editTodoInput }));
81 | setIsEditMode(false);
82 | setEditTodoInput("");
83 | };
84 |
85 | const handleCancelUpdate = (
86 | e?: React.MouseEvent
87 | ): void => {
88 | e?.preventDefault();
89 | setIsEditMode(false);
90 | setEditTodoInput("");
91 | };
92 |
93 | const handleToggle = (): void => {
94 | if (!selectedTodoId || !selectedTodo) return;
95 |
96 | dispatch(
97 | toggleTodo({
98 | id: selectedTodoId,
99 | isComplete: !selectedTodo.isComplete,
100 | })
101 | );
102 | };
103 |
104 | const handleDelete = (): void => {
105 | if (!selectedTodoId) return;
106 |
107 | dispatch(deleteTodo({ id: selectedTodoId }));
108 | };
109 |
110 | return (
111 |
112 |
Todos Updated Count: {counter}
113 |
114 |
Todo: Redux vs RTK Edition
115 |
124 |
125 |
126 |
127 | My Todos:
128 | {todos.map((todo: Todo, i: number) => (
129 | -
136 | {i + 1}) {todo.desc}
137 |
138 | ))}
139 |
140 |
141 |
Selected Todo:
142 | {selectedTodo === null ? (
143 |
No Todo Selected
144 | ) : !isEditMode ? (
145 | <>
146 |
151 | {selectedTodo.desc}
152 |
153 |
154 |
155 |
156 |
157 |
158 | >
159 | ) : (
160 |
170 | )}
171 |
172 |
173 |
174 | );
175 | };
176 |
177 | export default TodoList;
178 |
--------------------------------------------------------------------------------
/src/components/reduxToolkit/UsersList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import {
4 | fetchUsersAsync,
5 | selectError,
6 | selectLoading,
7 | selectUsers
8 | } from "../../stores/reduxToolkit/features/users/reducers";
9 | const UsersListTK: React.FC = () => {
10 | const users = useSelector(selectUsers);
11 | const loading = useSelector(selectLoading);
12 | const error = useSelector(selectError);
13 | const dispatch = useDispatch();
14 |
15 | React.useEffect(() => {
16 | dispatch(fetchUsersAsync());
17 | }, [dispatch]);
18 | return (
19 | <>
20 |
21 | Users List
22 | {error && Error!!
}
23 | {loading && Loading...
}
24 |
25 |
26 |
27 |
28 | Name
29 | |
30 |
31 |
32 |
33 | {users &&
34 | users.map(({ id, name }: {id: any, name: string}) => (
35 |
36 | {name} |
37 |
38 | ))}
39 |
40 |
41 |
42 | >
43 | );
44 | };
45 |
46 | export default UsersListTK;
47 |
--------------------------------------------------------------------------------
/src/components/reduxToolkit/containers.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Provider } from "react-redux";
3 | import { render, waitFor, screen } from "@testing-library/react";
4 | import userEvent from "@testing-library/user-event";
5 | import store from "../../stores/reduxToolkit";
6 | import UsersList from "../reduxToolkit/UsersList";
7 | import CatGame from "../reduxToolkit/CatGame";
8 |
9 | function renderWithRedux(component: React.ReactNode, store = {} as any) {
10 | return { ...render({component}) };
11 | }
12 |
13 | it("renders with redux", async () => {
14 | renderWithRedux(, store);
15 | const elem = await waitFor(() => screen.getByTestId(3));
16 | expect(elem).toHaveTextContent("Clementine Bauch");
17 | });
18 |
19 | it("fires play and renders with redux", async () => {
20 | renderWithRedux(, store);
21 | userEvent.click(screen.getByText("Play"));
22 | const elem = await waitFor(() => screen.getByTestId("cat-game"));
23 | expect(elem).toBeTruthy();
24 | });
25 |
--------------------------------------------------------------------------------
/src/components/toolkitWithRQ/CatGame.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Gif from "../common/Gif";
3 | import { useQuery } from "react-query";
4 | import { useSelector, useDispatch } from "react-redux";
5 | import { clear, selectGif, setGif } from "../../stores/toolkitWithRQ/features/gif/reducers";
6 | import gifApi from "../../api/gif";
7 |
8 | function renderGif({ imageUrl, isFetching, error }: {imageUrl: string, isFetching: boolean, error: any}) {
9 | if (error) {
10 | return Error!!
;
11 | }
12 |
13 | if (isFetching) {
14 | return Loading...
;
15 | }
16 |
17 | return imageUrl ? (
18 |
19 | ) : (
20 | Welcome to the cat game!
21 | );
22 | }
23 |
24 | const CatGameRQ: React.FC = () => {
25 | const imageUrl = useSelector(selectGif);
26 | const dispatch = useDispatch();
27 | const [play, setPlay] = React.useState(false);
28 |
29 | const { data, error, isFetching, refetch } = useQuery(
30 | ["gifs"],
31 | async () => await gifApi.random(),
32 | {
33 | enabled: !!play
34 | }
35 | );
36 |
37 | React.useEffect(() => {
38 | if (data) {
39 | dispatch(setGif(data.url));
40 | setPlay(false);
41 | }
42 | }, [data]);
43 |
44 | return (
45 |
46 | Cat Game
47 | {renderGif({ imageUrl, isFetching, error })}
48 |
49 |
50 |
56 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default CatGameRQ;
65 |
--------------------------------------------------------------------------------
/src/components/toolkitWithRQ/UsersList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useQuery } from "react-query";
3 | import { useSelector, useDispatch } from "react-redux";
4 | import { selectUsers, addUsers } from "../../stores/toolkitWithRQ/features/users/reducers";
5 |
6 | import { getUsers } from "../../api/getUsers";
7 | const UsersListRQ: React.FC = () => {
8 | const users = useSelector(selectUsers);
9 | const dispatch = useDispatch();
10 | const { data, error, isFetching } = useQuery(
11 | ["users"],
12 | async () => await getUsers()
13 | );
14 |
15 | React.useEffect(() => {
16 | if (data) {
17 | dispatch(addUsers(data));
18 | }
19 | }, [data]);
20 |
21 | return (
22 | <>
23 |
24 | Users List
25 | {error && Error!!
}
26 | {isFetching && Loading...
}
27 |
28 |
29 |
30 |
31 | Name
32 | |
33 |
34 |
35 |
36 | {users &&
37 | users.map(({ id, name }: {id: any, name: string}) => (
38 |
39 | {name} |
40 |
41 | ))}
42 |
43 |
44 |
45 | >
46 | );
47 | };
48 |
49 | export default UsersListRQ;
50 |
--------------------------------------------------------------------------------
/src/components/toolkitWithRQ/containers.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Provider } from "react-redux";
3 | import { QueryClient, QueryClientProvider } from "react-query";
4 | import { render, waitFor, screen } from "@testing-library/react";
5 | import userEvent from "@testing-library/user-event";
6 | import store from "../../stores/toolkitWithRQ";
7 | import UsersList from "../toolkitWithRQ/UsersList";
8 | import CatGame from "../toolkitWithRQ/CatGame";
9 |
10 | const queryClient = new QueryClient();
11 |
12 | function renderWithReduxAndRQ(component: React.ReactNode, store = {} as any) {
13 | return {
14 | ...render(
15 |
16 |
17 | {component}
18 |
19 |
20 | ),
21 | };
22 | }
23 |
24 | it("renders with redux", async () => {
25 | renderWithReduxAndRQ(, store);
26 | const elem = await waitFor(() => screen.getByTestId(3));
27 | expect(elem).toHaveTextContent("Clementine Bauch");
28 | });
29 |
30 | it("fires play and renders with react-query", async () => {
31 | renderWithReduxAndRQ(, store);
32 | userEvent.click(screen.getByText("Play"));
33 | const elem = await waitFor(() => screen.getByTestId("cat-game"));
34 | expect(elem).toBeTruthy();
35 | });
36 |
--------------------------------------------------------------------------------
/src/components/toolkitWithRTKQ/CatGame.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Gif from "../common/Gif";
3 | import { useGetNextGifQuery } from "../../stores/toolkitWithRTKQ/services/gif";
4 | import { useSelector, useDispatch } from "react-redux";
5 | import {
6 | clear,
7 | selectGif,
8 | setGif,
9 | } from "../../stores/toolkitWithRTKQ/features/gif/reducers";
10 |
11 | function renderGif({
12 | imageUrl,
13 | isFetching,
14 | error,
15 | }: {
16 | imageUrl: string;
17 | isFetching: boolean;
18 | error: any;
19 | }) {
20 | if (error) {
21 | return Error!!
;
22 | }
23 |
24 | if (isFetching) {
25 | return Loading...
;
26 | }
27 |
28 | return imageUrl ? (
29 |
30 | ) : (
31 | Welcome to the cat game!
32 | );
33 | }
34 |
35 | const CatGameRQ: React.FC = () => {
36 | const imageUrl = useSelector(selectGif);
37 | const dispatch = useDispatch();
38 | const [play, setPlay] = React.useState(false);
39 |
40 | const { data, error, isFetching, refetch } = useGetNextGifQuery({});
41 |
42 | React.useEffect(() => {
43 | if (data) {
44 | dispatch(setGif(data.url));
45 | setPlay(false);
46 | }
47 | }, [data]);
48 |
49 | return (
50 |
51 | Cat Game
52 | {renderGif({ imageUrl, isFetching, error })}
53 |
54 |
55 |
64 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default CatGameRQ;
77 |
--------------------------------------------------------------------------------
/src/components/toolkitWithRTKQ/UsersList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useGetUsersQuery as useQuery } from "../../stores/toolkitWithRTKQ/services/users";
3 | import { useSelector, useDispatch } from "react-redux";
4 | import {
5 | selectUsers,
6 | addUsers,
7 | } from "../../stores/toolkitWithRTKQ/features/users/reducers";
8 |
9 | const UsersListRQ: React.FC = () => {
10 | const users = useSelector(selectUsers);
11 | const dispatch = useDispatch();
12 | const { data, error, isFetching } = useQuery({});
13 |
14 | React.useEffect(() => {
15 | if (data) {
16 | dispatch(addUsers(data));
17 | }
18 | }, [data]);
19 |
20 | return (
21 | <>
22 |
23 | Users List
24 | {error && Error!!
}
25 | {isFetching && Loading...
}
26 |
27 |
28 |
29 |
30 | Name
31 | |
32 |
33 |
34 |
35 | {users &&
36 | users.map(({ id, name }: { id: any; name: string }) => (
37 |
38 | {name} |
39 |
40 | ))}
41 |
42 |
43 |
44 | >
45 | );
46 | };
47 |
48 | export default UsersListRQ;
49 |
--------------------------------------------------------------------------------
/src/containers/coreRedux/CatGameContainer.ts:
--------------------------------------------------------------------------------
1 | import { connect } from "react-redux";
2 | import { gifActions } from "../../stores/coreRedux/gif/actions";
3 | import CatGame from "../../components/coreRedux/CatGame";
4 |
5 | const mapStateToProps = ({ gif }: { gif: any}) => ({
6 | imageUrl: gif.url,
7 | loading: gif.loading,
8 | error: gif.error
9 | });
10 |
11 | const mapDispatchToProps = (dispatch: any) => ({
12 | play: () => dispatch(gifActions.asyncFetch()),
13 | clear: () => dispatch(gifActions.clearGif())
14 | });
15 |
16 | export default connect(mapStateToProps, mapDispatchToProps)(CatGame);
17 |
--------------------------------------------------------------------------------
/src/containers/coreRedux/TodosContainer.ts:
--------------------------------------------------------------------------------
1 | import { connect } from "react-redux";
2 | import {
3 | createTodoActionCreator,
4 | deleteTodoActionCreator,
5 | editTodoActionCreator,
6 | toggleTodoActionCreator,
7 | selectTodoActionCreator,
8 | } from "../../stores/coreRedux/todo/actions";
9 | import TodoList from "../../components/coreRedux/TodoList";
10 |
11 | const mapStateToProps = ({ todos, selectedTodoId, counter }: any) => ({
12 | todos,
13 | selectedTodoId,
14 | counter,
15 | });
16 |
17 | const mapDispatchToProps = (dispatch: any) => ({
18 | createTodo: (todo: { desc: string }) =>
19 | dispatch(createTodoActionCreator(todo)),
20 | deleteTodo: (todo: { id: string }) => dispatch(deleteTodoActionCreator(todo)),
21 | editTodo: (todo: { id: string; desc: string }) =>
22 | dispatch(editTodoActionCreator(todo)),
23 | toggleTodo: (todo: { id: string; isComplete: boolean }) =>
24 | dispatch(toggleTodoActionCreator(todo)),
25 | selectTodo: (todo: { id: string }) => dispatch(selectTodoActionCreator(todo)),
26 | });
27 |
28 | export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
29 |
--------------------------------------------------------------------------------
/src/containers/coreRedux/UsersContainer.ts:
--------------------------------------------------------------------------------
1 | import { connect } from "react-redux";
2 | import { usersActions } from "../../stores/coreRedux/users/actions";
3 | import UsersList from "../../components/coreRedux/UsersList";
4 |
5 | const mapStateToProps = ({ users }: any) => ({
6 | users: users.users,
7 | loading: users.loading,
8 | error: users.error
9 | });
10 |
11 | const mapDispatchToProps = (dispatch: any) => ({
12 | load: () => dispatch(usersActions.asyncFetch()),
13 | clear: () => dispatch(usersActions.clearUsers())
14 | });
15 |
16 | export default connect(mapStateToProps, mapDispatchToProps)(UsersList);
17 |
--------------------------------------------------------------------------------
/src/containers/coreRedux/containers.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { createStore } from "redux";
3 | import { Provider } from "react-redux";
4 | import { applyMiddleware } from "redux";
5 | import createSagaMiddleware from "redux-saga";
6 | import { render, waitFor, screen } from "@testing-library/react";
7 | import userEvent from "@testing-library/user-event";
8 | import reducers from "../../stores/coreRedux/reducers";
9 | import rootSaga from "../../stores/coreRedux/sagas";
10 | import UsersContainer from "./UsersContainer";
11 | import CatGameContainer from "./CatGameContainer";
12 | const sagaMiddleware = createSagaMiddleware();
13 |
14 | function renderWithRedux(
15 | component: React.ReactNode,
16 | {
17 | initialState,
18 | store = createStore(
19 | reducers,
20 | initialState,
21 | applyMiddleware(sagaMiddleware)
22 | ),
23 | } = {} as any
24 | ) {
25 | sagaMiddleware.run(rootSaga);
26 | return { ...render({component}) };
27 | }
28 |
29 | it("renders with redux", async () => {
30 | renderWithRedux();
31 | const elem = await waitFor(() => screen.getByTestId(3));
32 | expect(elem).toHaveTextContent("Clementine Bauch");
33 | });
34 |
35 | it("fires play and renders with redux", async () => {
36 | renderWithRedux();
37 | userEvent.click(screen.getByText("Play"));
38 | const elem = await waitFor(() => screen.getByTestId("cat-game"));
39 | expect(elem).toBeTruthy();
40 | });
41 |
--------------------------------------------------------------------------------
/src/hoc/withReduxStore.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Provider } from "react-redux";
3 |
4 | const withReduxStore = (WrappedComponent: any, store: any) => {
5 | return class extends React.Component {
6 | render() {
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 | };
14 | };
15 |
16 | export default withReduxStore;
17 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { BrowserRouter } from "react-router-dom";
4 |
5 | import "bulma";
6 | import App from "./App";
7 |
8 | const rootElement = document.getElementById("root");
9 |
10 | ReactDOM.render(
11 |
12 |
13 |
14 |
15 | ,
16 | rootElement
17 | );
18 |
--------------------------------------------------------------------------------
/src/pages/CoreRedux.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import CatGameContainer from "../containers/coreRedux/CatGameContainer";
3 | import UsersContainer from "../containers/coreRedux/UsersContainer";
4 | import store from "../stores/coreRedux";
5 | import withReduxStore from "../hoc/withReduxStore";
6 |
7 | const CoreRedux = () => {
8 | return (
9 | <>
10 | Core Redux
11 |
12 |
13 |
14 | >
15 | );
16 | };
17 |
18 | export default withReduxStore(CoreRedux, store);
19 |
--------------------------------------------------------------------------------
/src/pages/NoMatch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | const NoMatch = () => {
3 | return (
4 | <>
5 | Wrong path !!!!
6 | >
7 | );
8 | };
9 |
10 | export default NoMatch;
11 |
--------------------------------------------------------------------------------
/src/pages/RQOnly.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { QueryClient, QueryClientProvider } from "react-query";
3 | import { ReactQueryDevtools } from "react-query/devtools";
4 | import UsersList from "../components/onlyRQ/UsersList";
5 | import CatGame from "../components/onlyRQ/CatGame";
6 |
7 | const ReactQueryOnly = () => {
8 | const queryClientRef: any = React.useRef();
9 | if (!queryClientRef.current) {
10 | queryClientRef.current = new QueryClient();
11 | }
12 | return (
13 |
14 | Redux Toolkit
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default ReactQueryOnly;
24 |
--------------------------------------------------------------------------------
/src/pages/ReduxTodo.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import TodosContainer from "../containers/coreRedux/TodosContainer";
3 | import store from "../stores/coreRedux";
4 | import withReduxStore from "../hoc/withReduxStore";
5 |
6 | const ReduxTodo = () => {
7 | return (
8 | <>
9 |
10 | Redux Todo
11 |
12 |
13 | >
14 | );
15 | };
16 |
17 | export default withReduxStore(ReduxTodo, store);
18 |
--------------------------------------------------------------------------------
/src/pages/ReduxToolkit.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import UsersList from "../components/reduxToolkit/UsersList";
3 | import CatGame from "../components/reduxToolkit/CatGame";
4 | import store from "../stores/reduxToolkit";
5 | import withReduxStore from "../hoc/withReduxStore";
6 |
7 | const ReduxToolkit = () => {
8 | return (
9 | <>
10 | Redux Toolkit
11 |
12 |
13 |
14 | >
15 | );
16 | };
17 |
18 | export default withReduxStore(ReduxToolkit, store);
19 |
--------------------------------------------------------------------------------
/src/pages/ToolKitAndRQ.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { QueryClient, QueryClientProvider } from "react-query";
3 | import { ReactQueryDevtools } from "react-query/devtools";
4 | import UsersList from "../components/toolkitWithRQ/UsersList";
5 | import CatGame from "../components/toolkitWithRQ/CatGame";
6 | import store from "../stores/toolkitWithRQ";
7 | import withReduxStore from "../hoc/withReduxStore";
8 |
9 | const ReactQuery = () => {
10 | const queryClientRef: any = React.useRef();
11 | if (!queryClientRef.current) {
12 | queryClientRef.current = new QueryClient();
13 | }
14 | return (
15 |
16 | Redux Toolkit
17 |
18 |
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default withReduxStore(ReactQuery, store);
26 |
--------------------------------------------------------------------------------
/src/pages/ToolKitAndRTKQ.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import UsersList from "../components/toolkitWithRTKQ/UsersList";
3 | import CatGame from "../components/toolkitWithRTKQ/CatGame";
4 | import store from "../stores/toolkitWithRTKQ";
5 | import withReduxStore from "../hoc/withReduxStore";
6 |
7 | const ToolkitWithRTKQ = () => {
8 | return (
9 | <>
10 | Redux Toolkit
11 |
12 |
13 |
14 | >
15 | );
16 | };
17 |
18 | export default withReduxStore(ToolkitWithRTKQ, store);
19 |
--------------------------------------------------------------------------------
/src/pages/ToolkitTodo.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import TodoList from "../components/reduxToolkit/TodoList";
3 | import store from "../stores/reduxToolkit";
4 | import withReduxStore from "../hoc/withReduxStore";
5 |
6 | const ToolkitTodo = () => {
7 |
8 | return (
9 | <>
10 | Redux Toolkit Todo
11 |
12 | >
13 | );
14 | };
15 |
16 | export default withReduxStore(ToolkitTodo, store);
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | // src/setupTests.ts
3 | import { server } from "../mocks/server";
4 | // Establish API mocking before all tests.
5 | beforeAll(() => server.listen());
6 | // Reset any request handlers that we may add during the tests,
7 | // so they don't affect other tests.
8 | afterEach(() => server.resetHandlers());
9 | // Clean up after the tests are finished.
10 | afterAll(() => server.close());
11 |
12 |
--------------------------------------------------------------------------------
/src/stores/coreRedux/gif/actions.ts:
--------------------------------------------------------------------------------
1 | export const GIF_FETCH_ASYNC = "GIF_FETCH_ASYNC";
2 | export const GIF_FETCH_START = "GIF_FETCH_START";
3 | export const GIF_FETCH_SUCCEED = "GIF_FETCH_SUCCEED";
4 | export const GIF_FETCH_FAILED = "GIF_FETCH_FAILED";
5 | export const GIF_CLEAR = "GIF_CLEAR";
6 |
7 | export const gifActions = {
8 | asyncFetch() {
9 | return { type: GIF_FETCH_ASYNC };
10 | },
11 |
12 | startFetch() {
13 | return { type: GIF_FETCH_START };
14 | },
15 |
16 | successFetch(payload: any) {
17 | return { type: GIF_FETCH_SUCCEED, payload };
18 | },
19 |
20 | failFetch(payload: any) {
21 | return { type: GIF_FETCH_FAILED, payload };
22 | },
23 |
24 | clearGif() {
25 | return { type: GIF_CLEAR };
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/stores/coreRedux/gif/gifReducers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GIF_FETCH_START,
3 | GIF_FETCH_SUCCEED,
4 | GIF_FETCH_FAILED,
5 | GIF_CLEAR
6 | } from "./actions";
7 |
8 | const gifInitialState = {
9 | url: "",
10 | loading: false,
11 | error: false
12 | };
13 |
14 | export const gif = (state = gifInitialState, action: any) => {
15 | switch (action.type) {
16 | case GIF_FETCH_START:
17 | return { url: "", loading: true };
18 | case GIF_FETCH_SUCCEED:
19 | return { url: action.payload, loading: false, error: false };
20 | case GIF_FETCH_FAILED:
21 | console.error(action.payload);
22 | return { ...state, loading: false, error: true };
23 | case GIF_CLEAR:
24 | return { url: "", loading: false, error: false };
25 | default:
26 | return state;
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/src/stores/coreRedux/gif/sagas.ts:
--------------------------------------------------------------------------------
1 | import { call, put, takeLatest } from "redux-saga/effects";
2 | import { gifActions, GIF_FETCH_ASYNC } from "./actions";
3 | import gifApi from "../../../api/gif";
4 |
5 | function* fetchGif() {
6 | yield put(gifActions.startFetch());
7 |
8 | try {
9 | const response = yield call(gifApi.random);
10 | yield put(gifActions.successFetch(response.url));
11 | } catch (err) {
12 | yield put(gifActions.failFetch(err));
13 | }
14 | }
15 |
16 | export function* watchFetchGif() {
17 | yield takeLatest(GIF_FETCH_ASYNC, fetchGif);
18 | }
19 |
--------------------------------------------------------------------------------
/src/stores/coreRedux/index.ts:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from "redux";
2 | import { composeWithDevTools } from "redux-devtools-extension";
3 | import createSagaMiddleware from "redux-saga";
4 | import reducer from "./reducers";
5 | import rootSaga from "./sagas";
6 | const sagaMiddleware = createSagaMiddleware();
7 | const composeEnhancers = composeWithDevTools({
8 | // Specify name here, actionsBlacklist, actionsCreators and other options if needed
9 | });
10 |
11 | const store = createStore(
12 | reducer,
13 | composeEnhancers(applyMiddleware(sagaMiddleware))
14 | );
15 | sagaMiddleware.run(rootSaga);
16 |
17 | export default store;
18 |
--------------------------------------------------------------------------------
/src/stores/coreRedux/reducers.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import { gif } from "./gif/gifReducers";
3 | import { users } from "./users/usersReducers";
4 | import { counterReducer } from "./todo/counterReducers";
5 | import { selectedTodoReducer } from "./todo/selectedTodosReducers";
6 | import { todosReducer } from "./todo/todoReducers";
7 |
8 | export default combineReducers({
9 | gif,
10 | users,
11 | todos: todosReducer,
12 | selectedTodoId: selectedTodoReducer,
13 | counter: counterReducer,
14 | });
15 |
--------------------------------------------------------------------------------
/src/stores/coreRedux/sagas.ts:
--------------------------------------------------------------------------------
1 | import { fork, all } from "redux-saga/effects";
2 | import { watchFetchGif } from "./gif/sagas";
3 | import { watchFetchUsers } from "./users/sagas";
4 |
5 | export default function* rootSaga() {
6 | yield all([fork(watchFetchGif), fork(watchFetchUsers)]);
7 | }
8 |
--------------------------------------------------------------------------------
/src/stores/coreRedux/todo/actions.ts:
--------------------------------------------------------------------------------
1 | import { v1 as uuid } from "uuid";
2 | import { Todo } from "../../types";
3 |
4 | export const CREATE_TODO = "CREATE_TODO";
5 | export const EDIT_TODO = "EDIT_TODO";
6 | export const TOGGLE_TODO = "TOGGLE_TODO";
7 | export const DELETE_TODO = "DELETE_TODO";
8 | export const SELECT_TODO = "SELECT_TODO";
9 |
10 | export type TodoActionTypes =
11 | | CreateTodoActionType
12 | | EditTodoActionType
13 | | ToggleTodoActionType
14 | | DeleteTodoActionType;
15 |
16 | // Actions & Action Type
17 | export interface CreateTodoActionType {
18 | type: typeof CREATE_TODO;
19 | payload: Todo;
20 | }
21 |
22 | export const createTodoActionCreator = ({
23 | desc,
24 | }: {
25 | desc: string;
26 | }): CreateTodoActionType => {
27 | return {
28 | type: CREATE_TODO,
29 | payload: {
30 | id: uuid(),
31 | desc,
32 | isComplete: false,
33 | },
34 | };
35 | };
36 |
37 | export interface EditTodoActionType {
38 | type: typeof EDIT_TODO;
39 | payload: { id: string; desc: string };
40 | }
41 |
42 | export const editTodoActionCreator = ({
43 | id,
44 | desc,
45 | }: {
46 | id: string;
47 | desc: string;
48 | }): EditTodoActionType => {
49 | return {
50 | type: EDIT_TODO,
51 | payload: { id, desc },
52 | };
53 | };
54 |
55 | export interface ToggleTodoActionType {
56 | type: typeof TOGGLE_TODO;
57 | payload: { id: string; isComplete: boolean };
58 | }
59 |
60 | export const toggleTodoActionCreator = ({
61 | id,
62 | isComplete,
63 | }: {
64 | id: string;
65 | isComplete: boolean;
66 | }): ToggleTodoActionType => {
67 | return {
68 | type: TOGGLE_TODO,
69 | payload: { id, isComplete },
70 | };
71 | };
72 |
73 | export interface DeleteTodoActionType {
74 | type: typeof DELETE_TODO;
75 | payload: { id: string };
76 | }
77 |
78 | export const deleteTodoActionCreator = ({
79 | id,
80 | }: {
81 | id: string;
82 | }): DeleteTodoActionType => {
83 | return {
84 | type: DELETE_TODO,
85 | payload: { id },
86 | };
87 | };
88 |
89 | export interface SelectTodoActionType {
90 | type: typeof SELECT_TODO;
91 | payload: { id: string };
92 | }
93 |
94 | export const selectTodoActionCreator = ({
95 | id,
96 | }: {
97 | id: string;
98 | }): SelectTodoActionType => {
99 | return {
100 | type: SELECT_TODO,
101 | payload: { id },
102 | };
103 | };
104 |
--------------------------------------------------------------------------------
/src/stores/coreRedux/todo/counterReducers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CREATE_TODO,
3 | DELETE_TODO,
4 | EDIT_TODO,
5 | TOGGLE_TODO,
6 | TodoActionTypes,
7 | } from "./actions";
8 |
9 | export const counterReducer = (state: number = 0, action: TodoActionTypes) => {
10 | switch (action.type) {
11 | case CREATE_TODO: {
12 | return state + 1;
13 | }
14 | case EDIT_TODO: {
15 | return state + 1;
16 | }
17 | case TOGGLE_TODO: {
18 | return state + 1;
19 | }
20 | case DELETE_TODO: {
21 | return state + 1;
22 | }
23 | default: {
24 | return state;
25 | }
26 | }
27 | };
28 |
29 | export default counterReducer;
30 |
--------------------------------------------------------------------------------
/src/stores/coreRedux/todo/selectedTodosReducers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | SELECT_TODO,
3 | SelectTodoActionType
4 | } from "./actions";
5 |
6 | type SelectedTodoActionTypes = SelectTodoActionType;
7 | export const selectedTodoReducer = (
8 | state: string | null = null,
9 | action: SelectedTodoActionTypes
10 | ) => {
11 | switch (action.type) {
12 | case SELECT_TODO: {
13 | const { payload } = action;
14 | return payload.id;
15 | }
16 | default: {
17 | return state;
18 | }
19 | }
20 | };
21 |
22 | export default selectedTodoReducer;
--------------------------------------------------------------------------------
/src/stores/coreRedux/todo/todoReducers.ts:
--------------------------------------------------------------------------------
1 | import { v1 as uuid } from "uuid";
2 | import { Todo } from "../../types";
3 | import {
4 | CREATE_TODO,
5 | DELETE_TODO,
6 | EDIT_TODO,
7 | TOGGLE_TODO,
8 | TodoActionTypes,
9 | } from "./actions";
10 |
11 | const todosInitialState: Todo[] = [
12 | {
13 | id: uuid(),
14 | desc: "Learn React",
15 | isComplete: true,
16 | },
17 | {
18 | id: uuid(),
19 | desc: "Learn Redux",
20 | isComplete: true,
21 | },
22 | {
23 | id: uuid(),
24 | desc: "Learn Redux-ToolKit",
25 | isComplete: false,
26 | },
27 | ];
28 |
29 | export const todosReducer = (
30 | state: Todo[] = todosInitialState,
31 | action: TodoActionTypes
32 | ) => {
33 | switch (action.type) {
34 | case CREATE_TODO: {
35 | const { payload } = action;
36 | return [...state, payload];
37 | }
38 | case EDIT_TODO: {
39 | const { payload } = action;
40 | return state.map((todo) =>
41 | todo.id === payload.id ? { ...todo, desc: payload.desc } : todo
42 | );
43 | }
44 | case TOGGLE_TODO: {
45 | const { payload } = action;
46 | return state.map((todo) =>
47 | todo.id === payload.id
48 | ? { ...todo, isComplete: payload.isComplete }
49 | : todo
50 | );
51 | }
52 | case DELETE_TODO: {
53 | const { payload } = action;
54 | return state.filter((todo) => todo.id !== payload.id);
55 | }
56 | default: {
57 | return state;
58 | }
59 | }
60 | };
61 |
62 | export default todosReducer;
63 |
--------------------------------------------------------------------------------
/src/stores/coreRedux/users/actions.ts:
--------------------------------------------------------------------------------
1 | export const USERS_FETCH_ASYNC = "USERS_FETCH_ASYNC";
2 | export const USERS_FETCH_START = "USERS_FETCH_START";
3 | export const USERS_FETCH_SUCCEED = "USERS_FETCH_SUCCEED";
4 | export const USERS_FETCH_FAILED = "USERS_FETCH_FAILED";
5 | export const USERS_CLEAR = "USERS_CLEAR";
6 |
7 | export const usersActions = {
8 | asyncFetch() {
9 | return { type: USERS_FETCH_ASYNC };
10 | },
11 |
12 | startFetch() {
13 | return { type: USERS_FETCH_START };
14 | },
15 |
16 | successFetch(payload: any) {
17 | return { type: USERS_FETCH_SUCCEED, payload };
18 | },
19 |
20 | failFetch(payload: any) {
21 | return { type: USERS_FETCH_FAILED, payload };
22 | },
23 |
24 | clearUsers() {
25 | return { type: USERS_CLEAR };
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/stores/coreRedux/users/sagas.ts:
--------------------------------------------------------------------------------
1 | import { call, put, takeLatest } from "redux-saga/effects";
2 | import { usersActions, USERS_FETCH_ASYNC } from "./actions";
3 | import { getUsers } from "../../../api/getUsers";
4 |
5 | function* fetchUsers() {
6 | yield put(usersActions.startFetch());
7 |
8 | try {
9 | const response = yield call(getUsers);
10 | yield put(usersActions.successFetch(response));
11 | } catch (err) {
12 | yield put(usersActions.failFetch(err));
13 | }
14 | }
15 |
16 | export function* watchFetchUsers() {
17 | yield takeLatest(USERS_FETCH_ASYNC, fetchUsers);
18 | }
19 |
--------------------------------------------------------------------------------
/src/stores/coreRedux/users/usersReducers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | USERS_FETCH_START,
3 | USERS_FETCH_SUCCEED,
4 | USERS_FETCH_FAILED,
5 | USERS_CLEAR
6 | } from "./actions";
7 |
8 | export const usersInitialState = {
9 | users: [],
10 | loading: false,
11 | error: false
12 | };
13 |
14 | export const users = (state = usersInitialState, action: any) => {
15 | switch (action.type) {
16 | case USERS_FETCH_START:
17 | return { users: [], loading: true };
18 | case USERS_FETCH_SUCCEED:
19 | return { users: action.payload, loading: false, error: false };
20 | case USERS_FETCH_FAILED:
21 | console.error(action.payload);
22 | return { ...state, loading: false, error: true };
23 | case USERS_CLEAR:
24 | return { users: [], loading: false, error: false };
25 | default:
26 | return state;
27 | }
28 | };
29 |
30 | export default users;
31 |
--------------------------------------------------------------------------------
/src/stores/reduxToolkit/features/gif/reducers.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, createAction } from "@reduxjs/toolkit";
2 |
3 | const gifInitialState = {
4 | data: "",
5 | loading: false,
6 | error: false
7 | };
8 |
9 | const gifSlice = createSlice({
10 | name: "gif",
11 | initialState: gifInitialState,
12 | reducers: {
13 | fetch: (state) => {
14 | state.loading = true;
15 | state.error = false;
16 | state.data = "";
17 | },
18 | clear: (state) => {
19 | state.loading = false;
20 | state.error = false;
21 | state.data = "";
22 | },
23 | fetchSuccess: (state, action) => {
24 | state.data = action.payload;
25 | state.loading = false;
26 | },
27 | fetchFailure: (state, action) => {
28 | state.error = action.payload.error;
29 | state.loading = false;
30 | }
31 | }
32 | });
33 |
34 | export default gifSlice.reducer;
35 | export const { fetch, clear, fetchSuccess, fetchFailure } = gifSlice.actions;
36 | export const selectGif = (state: { gif: { data: any } }) => state.gif.data;
37 | export const selectLoading = (state: { gif: { loading: any } }) =>
38 | state.gif.loading;
39 | export const selectError = (state: { gif: { error: any } }) => state.gif.error;
40 |
41 | export const fetchGifAsync = createAction("GIF_FETCH_ASYNC");
42 |
--------------------------------------------------------------------------------
/src/stores/reduxToolkit/features/gif/sagas.ts:
--------------------------------------------------------------------------------
1 | import { call, put, takeLatest } from "redux-saga/effects";
2 | import { fetch, fetchSuccess, fetchFailure, fetchGifAsync } from "./reducers";
3 | import gifApi from "../../../../api/gif";
4 |
5 | function* fetchGif() {
6 | yield put(fetch());
7 |
8 | try {
9 | const response = yield call(gifApi.random);
10 | yield put(fetchSuccess(response.url));
11 | } catch (err) {
12 | yield put(fetchFailure(err));
13 | }
14 | }
15 |
16 | export function* watchFetchGif() {
17 | yield takeLatest(fetchGifAsync, fetchGif);
18 | }
19 |
--------------------------------------------------------------------------------
/src/stores/reduxToolkit/features/todo/counterReducers.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { todosSlice } from "./todoReducers";
3 |
4 | export const counterSlice = createSlice({
5 | name: "counter",
6 | initialState: 0,
7 | reducers: {},
8 | extraReducers: {
9 | [todosSlice.actions.createTodo.type]: (state) => state + 1,
10 | [todosSlice.actions.editTodo.type]: (state) => state + 1,
11 | [todosSlice.actions.toggleTodo.type]: (state) => state + 1,
12 | [todosSlice.actions.deleteTodo.type]: (state) => state + 1,
13 | },
14 | });
15 | export default counterSlice.reducer;
16 |
17 | export const selectCounter = (state: { counter: any }) => state.counter;
18 |
--------------------------------------------------------------------------------
/src/stores/reduxToolkit/features/todo/selectedTodosReducers.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | export const selectedTodoIdSlice = createSlice({
4 | name: "selectedTodo",
5 | initialState: "",
6 | reducers: {
7 | selectTodo: (state, { payload }: PayloadAction<{ id: string }>) =>
8 | payload.id,
9 | },
10 | });
11 |
12 | export const selectSelectedTodoId = (state: { selectedTodoId: any }) =>
13 | state.selectedTodoId;
14 |
15 | export const { selectTodo } = selectedTodoIdSlice.actions;
16 | export default selectedTodoIdSlice.reducer;
17 |
--------------------------------------------------------------------------------
/src/stores/reduxToolkit/features/todo/todoReducers.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 | import { v1 as uuid } from "uuid";
3 |
4 | import { Todo } from "../../../types";
5 |
6 | const todosInitialState: Todo[] = [
7 | {
8 | id: uuid(),
9 | desc: "Learn React",
10 | isComplete: true,
11 | },
12 | {
13 | id: uuid(),
14 | desc: "Learn Redux",
15 | isComplete: true,
16 | },
17 | {
18 | id: uuid(),
19 | desc: "Learn Redux-ToolKit",
20 | isComplete: false,
21 | },
22 | ];
23 |
24 | export const todosSlice = createSlice({
25 | name: "todos",
26 | initialState: todosInitialState,
27 | reducers: {
28 | createTodo: {
29 | reducer: (
30 | state,
31 | {
32 | payload,
33 | }: PayloadAction<{ id: string; desc: string; isComplete: boolean }>
34 | ) => {
35 | state.push(payload);
36 | },
37 | prepare: ({ desc }: { desc: string }) => ({
38 | payload: {
39 | id: uuid(),
40 | desc,
41 | isComplete: false,
42 | },
43 | }),
44 | },
45 | editTodo: (
46 | state,
47 | { payload }: PayloadAction<{ id: string; desc: string }>
48 | ) => {
49 | const index = state.findIndex((todo) => todo.id === payload.id);
50 | if (index !== -1) {
51 | state[index].desc = payload.desc;
52 | }
53 | },
54 | toggleTodo: (
55 | state,
56 | { payload }: PayloadAction<{ id: string; isComplete: boolean }>
57 | ) => {
58 | const index = state.findIndex((todo) => todo.id === payload.id);
59 | if (index !== -1) {
60 | state[index].isComplete = payload.isComplete;
61 | }
62 | },
63 | deleteTodo: (state, { payload }: PayloadAction<{ id: string }>) => {
64 | const index = state.findIndex((todo) => todo.id === payload.id);
65 | if (index !== -1) {
66 | state.splice(index, 1);
67 | }
68 | },
69 | },
70 | });
71 |
72 | export const {
73 | createTodo,
74 | editTodo,
75 | toggleTodo,
76 | deleteTodo,
77 | } = todosSlice.actions;
78 |
79 | export default todosSlice.reducer;
80 | export const selectTodos = (state: { todos: any }) => state.todos;
81 |
--------------------------------------------------------------------------------
/src/stores/reduxToolkit/features/users/reducers.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, createAction } from "@reduxjs/toolkit";
2 |
3 | const usersInitialState = {
4 | data: [],
5 | loading: false,
6 | error: false
7 | };
8 |
9 | const usersSlice = createSlice({
10 | name: "users",
11 | initialState: usersInitialState,
12 | reducers: {
13 | fetch: (state) => {
14 | state.loading = true;
15 | state.error = false;
16 | state.data = [];
17 | },
18 | fetchSuccess: (state, action) => {
19 | state.data = action.payload;
20 | state.loading = false;
21 | },
22 | fetchFailure: (state, action) => {
23 | state.error = action.payload.error;
24 | state.loading = false;
25 | }
26 | }
27 | });
28 |
29 | export default usersSlice.reducer;
30 | export const { fetch, fetchSuccess, fetchFailure } = usersSlice.actions;
31 | export const selectUsers = (state: { users: { data: any } }) =>
32 | state.users.data;
33 | export const selectLoading = (state: { users: { loading: any } }) =>
34 | state.users.loading;
35 | export const selectError = (state: { users: { error: any } }) =>
36 | state.users.error;
37 |
38 | export const fetchUsersAsync = createAction("USERS_FETCH_ASYNC");
39 |
--------------------------------------------------------------------------------
/src/stores/reduxToolkit/features/users/sagas.ts:
--------------------------------------------------------------------------------
1 | import { call, put, takeLatest } from "redux-saga/effects";
2 | import { fetch, fetchSuccess, fetchFailure, fetchUsersAsync } from "./reducers";
3 | import { getUsers } from "../../../../api/getUsers";
4 |
5 | function* fetchUsers() {
6 | yield put(fetch());
7 |
8 | try {
9 | const response = yield call(getUsers);
10 | yield put(fetchSuccess(response));
11 | } catch (err) {
12 | yield put(fetchFailure(err));
13 | }
14 | }
15 |
16 | export function* watchFetchUsers() {
17 | yield takeLatest(fetchUsersAsync, fetchUsers);
18 | }
19 |
--------------------------------------------------------------------------------
/src/stores/reduxToolkit/index.ts:
--------------------------------------------------------------------------------
1 | import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
2 | import createSagaMiddleware from "redux-saga";
3 | import userReducer from "./features/users/reducers";
4 | import gifReducer from "./features/gif/reducers";
5 | import todosReducer from "./features/todo/todoReducers";
6 | import selectedTodoReducer from "./features/todo/selectedTodosReducers";
7 | import counterReducer from "./features/todo/counterReducers";
8 | import rootSaga from "./sagas";
9 |
10 | let sagaMiddleware = createSagaMiddleware();
11 |
12 | const middleware = [...getDefaultMiddleware({ thunk: false }), sagaMiddleware];
13 |
14 | const store = configureStore({
15 | reducer: {
16 | users: userReducer,
17 | gif: gifReducer,
18 | todos: todosReducer,
19 | selectedTodoId: selectedTodoReducer,
20 | counter: counterReducer,
21 | },
22 | middleware,
23 | });
24 |
25 | sagaMiddleware.run(rootSaga);
26 | export default store;
27 |
--------------------------------------------------------------------------------
/src/stores/reduxToolkit/sagas.ts:
--------------------------------------------------------------------------------
1 | import { fork, all } from "redux-saga/effects";
2 | import { watchFetchGif } from "./features/gif/sagas";
3 | import { watchFetchUsers } from "./features/users/sagas";
4 |
5 | export default function* rootSaga() {
6 | yield all([fork(watchFetchGif), fork(watchFetchUsers)]);
7 | }
8 |
--------------------------------------------------------------------------------
/src/stores/toolkitWithRQ/features/gif/reducers.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const gifInitialState = {
4 | data: ""
5 | };
6 |
7 | const gifSlice = createSlice({
8 | name: "gif",
9 | initialState: gifInitialState,
10 | reducers: {
11 | setGif: (state, { payload }) => {
12 | state.data = payload;
13 | },
14 | clear: (state) => {
15 | state.data = "";
16 | }
17 | }
18 | });
19 |
20 | export default gifSlice.reducer;
21 | export const { setGif, clear } = gifSlice.actions;
22 | export const selectGif = (state: { gif: { data: any } }) => state.gif.data;
23 |
--------------------------------------------------------------------------------
/src/stores/toolkitWithRQ/features/users/reducers.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | const usersInitialState = {
4 | data: []
5 | };
6 |
7 | const usersSlice = createSlice({
8 | name: "users",
9 | initialState: usersInitialState,
10 | reducers: {
11 | addUsers: (state, { payload }) => {
12 | state.data = payload;
13 | }
14 | }
15 | });
16 |
17 | export default usersSlice.reducer;
18 | export const { addUsers } = usersSlice.actions;
19 | export const selectUsers = (state: { users: { data: any } }) =>
20 | state.users.data;
21 |
--------------------------------------------------------------------------------
/src/stores/toolkitWithRQ/index.ts:
--------------------------------------------------------------------------------
1 | import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
2 | import userReducer from "./features/users/reducers";
3 | import gifReducer from "./features/gif/reducers";
4 |
5 | const middleware = [...getDefaultMiddleware({ thunk: false })];
6 |
7 | const store = configureStore({
8 | reducer: {
9 | users: userReducer,
10 | gif: gifReducer
11 | },
12 | middleware
13 | });
14 |
15 | export default store;
16 |
--------------------------------------------------------------------------------
/src/stores/toolkitWithRTKQ/features/gif/reducers.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const gifInitialState = {
4 | data: ""
5 | };
6 |
7 | const gifSlice = createSlice({
8 | name: "gif",
9 | initialState: gifInitialState,
10 | reducers: {
11 | setGif: (state, { payload }) => {
12 | state.data = payload;
13 | },
14 | clear: (state) => {
15 | state.data = "";
16 | }
17 | }
18 | });
19 |
20 | export default gifSlice.reducer;
21 | export const { setGif, clear } = gifSlice.actions;
22 | export const selectGif = (state: { gif: { data: any } }) => state.gif.data;
23 |
--------------------------------------------------------------------------------
/src/stores/toolkitWithRTKQ/features/users/reducers.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | const usersInitialState = {
4 | data: []
5 | };
6 |
7 | const usersSlice = createSlice({
8 | name: "users",
9 | initialState: usersInitialState,
10 | reducers: {
11 | addUsers: (state, { payload }) => {
12 | state.data = payload;
13 | }
14 | }
15 | });
16 |
17 | export default usersSlice.reducer;
18 | export const { addUsers } = usersSlice.actions;
19 | export const selectUsers = (state: { users: { data: any } }) =>
20 | state.users.data;
21 |
--------------------------------------------------------------------------------
/src/stores/toolkitWithRTKQ/index.ts:
--------------------------------------------------------------------------------
1 | import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
2 | import userReducer from "./features/users/reducers";
3 | import gifReducer from "./features/gif/reducers";
4 | import { gifApi } from "./services/gif";
5 | import { usersApi } from "./services/users";
6 |
7 | const middleware = [
8 | ...getDefaultMiddleware(),
9 | gifApi.middleware,
10 | usersApi.middleware,
11 | ];
12 |
13 | const store = configureStore({
14 | reducer: {
15 | users: userReducer,
16 | gif: gifReducer,
17 | // Add the generated reducer as a specific top-level slice
18 | [gifApi.reducerPath]: gifApi.reducer,
19 | [usersApi.reducerPath]: usersApi.reducer,
20 | },
21 | middleware,
22 | });
23 |
24 | export default store;
25 |
--------------------------------------------------------------------------------
/src/stores/toolkitWithRTKQ/services/gif.ts:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from '@rtk-incubator/rtk-query';
2 |
3 | // Define a service using a base URL and expected endpoints
4 | export const gifApi = createApi({
5 | reducerPath: 'gifApi',
6 | baseQuery: fetchBaseQuery({ baseUrl: `${process.env.REACT_APP_GIF_API_URL}` }),
7 | endpoints: (builder) => ({
8 | getNextGif: builder.query({
9 | query: () => '',
10 | }),
11 | }),
12 | });
13 |
14 | // Export hooks for usage in functional components, which are
15 | // auto-generated based on the defined endpoints
16 | export const { useGetNextGifQuery } = gifApi;
--------------------------------------------------------------------------------
/src/stores/toolkitWithRTKQ/services/users.ts:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from "@rtk-incubator/rtk-query";
2 |
3 | // Define a service using a base URL and expected endpoints
4 | export const usersApi = createApi({
5 | reducerPath: "usersApi",
6 | baseQuery: fetchBaseQuery({
7 | baseUrl: `${process.env.REACT_APP_USERS_API_URL}`,
8 | }),
9 | endpoints: (builder) => ({
10 | getUsers: builder.query({
11 | query: () => "",
12 | }),
13 | }),
14 | });
15 |
16 | // Export hooks for usage in functional components, which are
17 | // auto-generated based on the defined endpoints
18 | export const { useGetUsersQuery } = usersApi;
19 |
--------------------------------------------------------------------------------
/src/stores/types.ts:
--------------------------------------------------------------------------------
1 | export interface Todo {
2 | id: string;
3 | desc: string;
4 | isComplete: boolean;
5 | }
6 |
7 | export interface State {
8 | todos: Todo[];
9 | selectedTodo: string | null;
10 | counter: number;
11 | }
12 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | .App {
2 | font-family: sans-serif;
3 | text-align: center;
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | , "mocks" ]
26 | }
27 |
--------------------------------------------------------------------------------