├── .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 |
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 |
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 | ID |
44 | Name |
45 | Email |
46 | Actions |
47 |
48 |
49 |
50 | {entities.length &&
51 | entities.map(({ id, name, email }, i) => (
52 |
53 | {id} |
54 | {name} |
55 | {email} |
56 |
57 |
58 |
59 |
60 |
61 | |
62 |
63 | ))}
64 |
65 |
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 |
--------------------------------------------------------------------------------