├── .eslintcache ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── features └── users │ ├── AddUser.jsx │ ├── EditUser.jsx │ ├── UserList.jsx │ └── usersSlice.js ├── index.js └── store.js /.eslintcache: -------------------------------------------------------------------------------- 1 | [{"C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\App.js":"1","C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\index.js":"2","C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\store.js":"3","C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\features\\users\\usersSlice.js":"4","C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\features\\users\\UserList.jsx":"5","C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\features\\users\\AddUser.jsx":"6","C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\features\\users\\EditUser.jsx":"7"},{"size":630,"mtime":1611923406145,"results":"8","hashOfConfig":"9"},{"size":368,"mtime":1611927815334,"results":"10","hashOfConfig":"9"},{"size":195,"mtime":1611917361195,"results":"11","hashOfConfig":"9"},{"size":1525,"mtime":1611928190070,"results":"12","hashOfConfig":"9"},{"size":1994,"mtime":1611927867863,"results":"13","hashOfConfig":"9"},{"size":1889,"mtime":1611928310209,"results":"14","hashOfConfig":"9"},{"size":2012,"mtime":1611927721240,"results":"15","hashOfConfig":"9"},{"filePath":"16","messages":"17","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"ltq8zx",{"filePath":"18","messages":"19","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"20","messages":"21","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"22"},{"filePath":"23","messages":"24","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"25","messages":"26","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"27","messages":"28","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"29","messages":"30","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\App.js",[],"C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\index.js",[],"C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\store.js",[],["31","32"],"C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\features\\users\\usersSlice.js",[],"C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\features\\users\\UserList.jsx",[],"C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\features\\users\\AddUser.jsx",[],"C:\\Users\\sande\\GitHub\\redux-crud-tutorial\\src\\features\\users\\EditUser.jsx",[],{"ruleId":"33","replacedBy":"34"},{"ruleId":"35","replacedBy":"36"},"no-native-reassign",["37"],"no-negated-in-lhs",["38"],"no-global-assign","no-unsafe-negation"] -------------------------------------------------------------------------------- /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Simple React Redux CRUD app for my tutorial on Dev.to 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-crud-tutorial", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.5.0", 7 | "@testing-library/jest-dom": "^5.11.4", 8 | "@testing-library/react": "^11.1.0", 9 | "@testing-library/user-event": "^12.1.10", 10 | "react": "^17.0.1", 11 | "react-dom": "^17.0.1", 12 | "react-redux": "^7.2.2", 13 | "react-router-dom": "^5.2.0", 14 | "react-scripts": "4.0.1", 15 | "web-vitals": "^0.2.4" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanderdebr/redux-crud-tutorial/b29f13b70f2b6af56e1f596fe198d2c912b6dbd3/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 32 | 33 | 34 | 35 |
36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanderdebr/redux-crud-tutorial/b29f13b70f2b6af56e1f596fe198d2c912b6dbd3/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanderdebr/redux-crud-tutorial/b29f13b70f2b6af56e1f596fe198d2c912b6dbd3/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { Route, BrowserRouter as Router, Switch } from "react-router-dom"; 2 | 3 | import { AddUser } from "./features/users/AddUser"; 4 | import { EditUser } from "./features/users/EditUser"; 5 | import React from "react"; 6 | import { UserList } from "./features/users/UserList"; 7 | 8 | export default function App() { 9 | return ( 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/features/users/AddUser.jsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | 3 | import { useHistory } from "react-router-dom"; 4 | import { useState } from "react"; 5 | import { userAdded } from "./usersSlice"; 6 | 7 | export function AddUser() { 8 | const dispatch = useDispatch(); 9 | const history = useHistory(); 10 | 11 | const [name, setName] = useState(""); 12 | const [email, setEmail] = useState(""); 13 | const [error, setError] = useState(null); 14 | 15 | const handleName = (e) => setName(e.target.value); 16 | const handleEmail = (e) => setEmail(e.target.value); 17 | 18 | const usersAmount = useSelector((state) => state.users.entities.length); 19 | 20 | const handleClick = () => { 21 | if (name && email) { 22 | dispatch( 23 | userAdded({ 24 | id: usersAmount + 1, 25 | name, 26 | email, 27 | }) 28 | ); 29 | 30 | setError(null); 31 | history.push("/"); 32 | } else { 33 | setError("Fill in all fields"); 34 | } 35 | 36 | setName(""); 37 | setEmail(""); 38 | }; 39 | 40 | return ( 41 |
42 |
43 |

Add user

44 |
45 |
46 |
47 | 48 | 56 | 57 | 65 | {error && error} 66 | 69 |
70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/features/users/EditUser.jsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import { useHistory, useLocation } from "react-router-dom"; 3 | 4 | import { useState } from "react"; 5 | import { userUpdated } from "./usersSlice"; 6 | 7 | export function EditUser() { 8 | const { pathname } = useLocation(); 9 | const userId = parseInt(pathname.replace("/edit-user/", "")); 10 | 11 | const user = useSelector((state) => 12 | state.users.entities.find((user) => user.id === userId) 13 | ); 14 | 15 | const dispatch = useDispatch(); 16 | const history = useHistory(); 17 | 18 | const [name, setName] = useState(user.name); 19 | const [email, setEmail] = useState(user.email); 20 | const [error, setError] = useState(null); 21 | 22 | const handleName = (e) => setName(e.target.value); 23 | const handleEmail = (e) => setEmail(e.target.value); 24 | 25 | const handleClick = () => { 26 | if (name && email) { 27 | dispatch( 28 | userUpdated({ 29 | id: userId, 30 | name, 31 | email, 32 | }) 33 | ); 34 | 35 | setError(null); 36 | history.push("/"); 37 | } else { 38 | setError("Fill in all fields"); 39 | } 40 | }; 41 | 42 | return ( 43 |
44 |
45 |

Edit user

46 |
47 |
48 |
49 | 50 | 58 | 59 | 67 | {error && error} 68 | 71 |
72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/features/users/UserList.jsx: -------------------------------------------------------------------------------- 1 | import { fetchUsers, userDeleted } from "./usersSlice"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | 4 | import { Link } from "react-router-dom"; 5 | 6 | export function UserList() { 7 | const dispatch = useDispatch(); 8 | 9 | const { entities } = useSelector((state) => state.users); 10 | const loading = useSelector((state) => state.loading); 11 | 12 | const handleDelete = (id) => { 13 | dispatch(userDeleted({ id })); 14 | }; 15 | 16 | return ( 17 |
18 |
19 |

Redux CRUD User app

20 |
21 |
22 |
23 | 29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 |
37 | {loading ? ( 38 | "Loading..." 39 | ) : ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {entities.length && 51 | entities.map(({ id, name, email }, i) => ( 52 | 53 | 54 | 55 | 56 | 62 | 63 | ))} 64 | 65 |
IDNameEmailActions
{id}{name}{email} 57 | 58 | 59 | 60 | 61 |
66 | )} 67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/features/users/usersSlice.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; 2 | 3 | export const fetchUsers = createAsyncThunk("users/fetchUsers", async () => { 4 | const response = await fetch("https://jsonplaceholder.typicode.com/users"); 5 | const users = await response.json(); 6 | return users; 7 | }); 8 | 9 | const usersSlice = createSlice({ 10 | name: "users", 11 | initialState: { 12 | entities: [], 13 | loading: false, 14 | }, 15 | reducers: { 16 | userAdded(state, action) { 17 | state.entities.push(action.payload); 18 | }, 19 | userUpdated(state, action) { 20 | const { id, name, email } = action.payload; 21 | const existingUser = state.entities.find((user) => user.id === id); 22 | if (existingUser) { 23 | existingUser.name = name; 24 | existingUser.email = email; 25 | } 26 | }, 27 | userDeleted(state, action) { 28 | const { id } = action.payload; 29 | const existingUser = state.entities.find((user) => user.id === id); 30 | if (existingUser) { 31 | state.entities = state.entities.filter((user) => user.id !== id); 32 | } 33 | }, 34 | }, 35 | extraReducers: { 36 | [fetchUsers.pending]: (state, action) => { 37 | state.loading = true; 38 | }, 39 | [fetchUsers.fulfilled]: (state, action) => { 40 | state.loading = false; 41 | state.entities = [...state.entities, ...action.payload]; 42 | }, 43 | [fetchUsers.rejected]: (state, action) => { 44 | state.loading = false; 45 | }, 46 | }, 47 | }); 48 | 49 | export const { userAdded, userUpdated, userDeleted } = usersSlice.actions; 50 | 51 | export default usersSlice.reducer; 52 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import App from "./App"; 2 | import { Provider } from "react-redux"; 3 | import React from "react"; 4 | import ReactDOM from "react-dom"; 5 | import { fetchUsers } from "./features/users/usersSlice"; 6 | import store from "./store"; 7 | 8 | store.dispatch(fetchUsers()); 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById("root") 15 | ); 16 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import usersReducer from "./features/users/usersSlice"; 3 | 4 | export default configureStore({ 5 | reducer: { 6 | users: usersReducer, 7 | }, 8 | }); 9 | --------------------------------------------------------------------------------