├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── src
├── context
│ ├── theme
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── context.js
│ └── users
│ │ ├── index.js
│ │ ├── reducer.js
│ │ ├── actions.js
│ │ └── context.js
├── index.js
├── App.js
├── components
│ ├── UserList.js
│ └── User.js
├── _layout.js
└── pages
│ └── Users.js
├── README.md
├── .gitignore
├── package.json
└── .eslintcache
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanderdebr/react-usereducer-context-tutorial/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanderdebr/react-usereducer-context-tutorial/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanderdebr/react-usereducer-context-tutorial/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/context/theme/index.js:
--------------------------------------------------------------------------------
1 | import { ThemeProvider, useThemeState } from "./context";
2 |
3 | export { useThemeState, ThemeProvider };
4 |
--------------------------------------------------------------------------------
/src/context/users/index.js:
--------------------------------------------------------------------------------
1 | import { UsersProvider, useUsersState } from "./context";
2 |
3 | export { useUsersState, UsersProvider };
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Simple user app for my React useReducer with Context API tutorial
2 |
3 | [Live version](https://pensive-benz-7656bc.netlify.app/)
4 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import App from "./App";
2 | import React from "react";
3 | import ReactDOM from "react-dom";
4 |
5 | ReactDOM.render(
6 |
7 |
8 | ,
9 | document.getElementById("root")
10 | );
11 |
--------------------------------------------------------------------------------
/src/context/theme/reducer.js:
--------------------------------------------------------------------------------
1 | export const themeReducer = (state, { type }) => {
2 | switch (type) {
3 | case "TOGGLE_THEME":
4 | return {
5 | ...state,
6 | switched: state.switched + 1,
7 | theme: state.theme === "light" ? "dark" : "light",
8 | };
9 | default:
10 | throw new Error(`Unhandled action type: ${type}`);
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { Route, BrowserRouter as Router, Switch } from "react-router-dom";
2 |
3 | import Layout from "./_layout";
4 | import { ThemeProvider } from "./context/theme";
5 | import Users from "./pages/Users";
6 | import { UsersProvider } from "./context/users";
7 |
8 | function App() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {/* Add more routes here */}
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | export default App;
28 |
--------------------------------------------------------------------------------
/src/components/UserList.js:
--------------------------------------------------------------------------------
1 | import { Grid } from "@material-ui/core";
2 | import React from "react";
3 | import User from "./User";
4 | import { useUsersState } from "../context/users";
5 |
6 | export default function UserList() {
7 | const { users, loading, error } = useUsersState();
8 |
9 | if (loading) {
10 | return "Loading...";
11 | }
12 |
13 | if (error) {
14 | return "Error...";
15 | }
16 |
17 | return (
18 |
19 | {users.length
20 | ? users.map((user, i) => (
21 |
22 |
23 |
24 | ))
25 | : "Click load users to load some users"}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/context/users/reducer.js:
--------------------------------------------------------------------------------
1 | export const usersReducer = (state, { type, payload, error }) => {
2 | switch (type) {
3 | case "REQUEST_USERS":
4 | return {
5 | ...state,
6 | loading: true,
7 | };
8 | case "USERS_SUCCESS":
9 | return {
10 | ...state,
11 | loading: false,
12 | users: payload,
13 | };
14 | case "USERS_FAIL":
15 | return {
16 | ...state,
17 | loading: false,
18 | error,
19 | };
20 | case "DELETE_USER":
21 | return {
22 | ...state,
23 | users: state.users.filter((user) => user.id !== payload),
24 | };
25 | default:
26 | throw new Error(`Unhandled action type: ${type}`);
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/src/context/users/actions.js:
--------------------------------------------------------------------------------
1 | export const getUsers = async (dispatch) => {
2 | dispatch({ type: "REQUEST_USERS" });
3 | try {
4 | // Fetch server
5 | const response = await fetch(`https://jsonplaceholder.typicode.com/users`);
6 |
7 | if (!response.ok) {
8 | throw Error(response.statusText);
9 | }
10 |
11 | let data = await response.json();
12 |
13 | // Received users from server
14 | if (data.length) {
15 | dispatch({ type: "USERS_SUCCESS", payload: data });
16 | return data;
17 | }
18 |
19 | // No match found on server
20 | dispatch({
21 | type: "USERS_FAIL",
22 | error: { message: "Could not fetch users" },
23 | });
24 |
25 | return null;
26 | } catch (error) {
27 | dispatch({ type: "USERS_FAIL", error });
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/src/_layout.js:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Container,
4 | CssBaseline,
5 | ThemeProvider,
6 | createMuiTheme,
7 | } from "@material-ui/core";
8 |
9 | import React from "react";
10 | import { useThemeState } from "./context/theme";
11 |
12 | export const light = {
13 | palette: {
14 | type: "light",
15 | },
16 | };
17 |
18 | export const dark = {
19 | palette: {
20 | type: "dark",
21 | },
22 | };
23 |
24 | export default function Layout({ children }) {
25 | const { theme } = useThemeState();
26 |
27 | const lightTheme = createMuiTheme(light);
28 | const darkTheme = createMuiTheme(dark);
29 |
30 | return (
31 |
32 |
33 |
34 | {children}
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-usereducer-context-tutorial",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^4.11.2",
7 | "@material-ui/icons": "^4.11.2",
8 | "@testing-library/jest-dom": "^5.11.4",
9 | "@testing-library/react": "^11.1.0",
10 | "@testing-library/user-event": "^12.1.10",
11 | "react": "^17.0.1",
12 | "react-dom": "^17.0.1",
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 |
--------------------------------------------------------------------------------
/src/context/theme/context.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useReducer } from "react";
2 |
3 | import { themeReducer } from "./reducer";
4 |
5 | const { createContext } = require("react");
6 |
7 | const initialState = {
8 | switched: 0,
9 | theme: "light",
10 | };
11 |
12 | const initializer = localStorage.getItem("theme")
13 | ? JSON.parse(localStorage.getItem("theme"))
14 | : initialState;
15 |
16 | const ThemeStateContext = createContext();
17 | const ThemeDispatchContext = createContext();
18 |
19 | export const useThemeState = () => useContext(ThemeStateContext);
20 | export const useThemeDispatch = () => useContext(ThemeDispatchContext);
21 |
22 | export const ThemeProvider = ({ children }) => {
23 | const [theme, dispatch] = useReducer(themeReducer, initializer);
24 |
25 | // Persist state on each update
26 | useEffect(() => {
27 | localStorage.setItem("theme", JSON.stringify(theme));
28 | }, [theme]);
29 |
30 | return (
31 |
32 |
33 | {children}
34 |
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/context/users/context.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useReducer } from "react";
2 |
3 | import { usersReducer } from "./reducer";
4 |
5 | const { createContext } = require("react");
6 |
7 | const initialState = {
8 | loading: false,
9 | error: null,
10 | users: [],
11 | };
12 |
13 | const initializer = localStorage.getItem("users")
14 | ? JSON.parse(localStorage.getItem("users"))
15 | : initialState;
16 |
17 | const UsersStateContext = createContext();
18 | const UsersDispatchContext = createContext();
19 |
20 | export const useUsersState = () => useContext(UsersStateContext);
21 | export const useUsersDispatch = () => useContext(UsersDispatchContext);
22 |
23 | export const UsersProvider = ({ children }) => {
24 | const [state, dispatch] = useReducer(usersReducer, initializer);
25 |
26 | // Persist state on each update
27 | useEffect(() => {
28 | localStorage.setItem("users", JSON.stringify(state));
29 | }, [state]);
30 |
31 | return (
32 |
33 |
34 | {children}
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/User.js:
--------------------------------------------------------------------------------
1 | import Button from "@material-ui/core/Button";
2 | import Card from "@material-ui/core/Card";
3 | import CardActionArea from "@material-ui/core/CardActionArea";
4 | import CardActions from "@material-ui/core/CardActions";
5 | import CardContent from "@material-ui/core/CardContent";
6 | import CardMedia from "@material-ui/core/CardMedia";
7 | import React from "react";
8 | import Typography from "@material-ui/core/Typography";
9 | import { makeStyles } from "@material-ui/core/styles";
10 | import { useUsersDispatch } from "../context/users/context";
11 |
12 | const useStyles = makeStyles({
13 | media: {
14 | height: 140,
15 | },
16 | });
17 |
18 | export default function User({ user }) {
19 | const classes = useStyles();
20 | const dispatch = useUsersDispatch();
21 |
22 | return (
23 |
24 |
25 |
26 |
27 | {user.name}
28 |
29 |
30 | ID: {user.id}
31 |
32 |
33 | Username: {user.username}
34 |
35 |
36 | Email: {user.email}
37 |
38 |
39 |
40 |
41 |
49 |
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
22 |
31 | React App
32 |
33 |
34 |
35 |
36 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/pages/Users.js:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Divider,
4 | Grid,
5 | Paper,
6 | Typography,
7 | makeStyles,
8 | } from "@material-ui/core";
9 |
10 | import Brightness4Icon from "@material-ui/icons/Brightness4";
11 | import Brightness7Icon from "@material-ui/icons/Brightness7";
12 | import React from "react";
13 | import UserList from "../components/UserList";
14 | import { getUsers } from "../context/users/actions";
15 | import { useThemeDispatch } from "../context/theme/context";
16 | import { useThemeState } from "../context/theme";
17 | import { useUsersDispatch } from "../context/users/context";
18 |
19 | const useStyles = makeStyles((theme) => ({
20 | paper: {
21 | padding: theme.spacing(4),
22 | margin: "auto",
23 | },
24 | img: {
25 | width: "100%",
26 | },
27 | divider: {
28 | marginBottom: theme.spacing(2),
29 | },
30 | }));
31 |
32 | export default function Users() {
33 | const classes = useStyles();
34 |
35 | const { theme } = useThemeState();
36 |
37 | const dispatchTheme = useThemeDispatch();
38 | const dispatchUsers = useUsersDispatch();
39 |
40 | const _toggleTheme = () => dispatchTheme({ type: "TOGGLE_THEME" });
41 | const _getUsers = () => getUsers(dispatchUsers);
42 |
43 | return (
44 |
45 |
46 |
47 |
48 | Users
49 |
50 |
51 |
52 |
53 |
54 |
57 |
58 |
59 | {theme === "light" ? : }
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/.eslintcache:
--------------------------------------------------------------------------------
1 | [{"C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\App.js":"1","C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\index.js":"2","C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\_layout.js":"3","C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\context\\theme\\context.js":"4","C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\context\\theme\\index.js":"5","C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\components\\User.js":"6","C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\pages\\Users.js":"7","C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\context\\theme\\reducer.js":"8","C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\components\\UserList.js":"9","C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\context\\users\\context.js":"10","C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\context\\users\\reducer.js":"11","C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\context\\users\\actions.js":"12"},{"size":624,"mtime":1611317345096,"results":"13","hashOfConfig":"14"},{"size":197,"mtime":1611303125588,"results":"15","hashOfConfig":"14"},{"size":748,"mtime":1611313254289,"results":"16","hashOfConfig":"14"},{"size":1054,"mtime":1611316998865,"results":"17","hashOfConfig":"14"},{"size":103,"mtime":1611311754051,"results":"18","hashOfConfig":"14"},{"size":1832,"mtime":1611316524728,"results":"19","hashOfConfig":"14"},{"size":1892,"mtime":1611316635131,"results":"20","hashOfConfig":"14"},{"size":322,"mtime":1611313213316,"results":"21","hashOfConfig":"14"},{"size":638,"mtime":1611317526862,"results":"22","hashOfConfig":"14"},{"size":1068,"mtime":1611317452149,"results":"23","hashOfConfig":"14"},{"size":633,"mtime":1611316601805,"results":"24","hashOfConfig":"14"},{"size":696,"mtime":1611315510530,"results":"25","hashOfConfig":"14"},{"filePath":"26","messages":"27","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"otspbl",{"filePath":"28","messages":"29","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"30","messages":"31","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"32","messages":"33","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"34","messages":"35","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"36","messages":"37","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"38","messages":"39","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"40","messages":"41","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"42","messages":"43","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"44","messages":"45","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"46","messages":"47","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"48","messages":"49","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\App.js",[],"C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\index.js",[],"C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\_layout.js",[],"C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\context\\theme\\context.js",[],"C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\context\\theme\\index.js",[],"C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\components\\User.js",["50"],"C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\pages\\Users.js",[],"C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\context\\theme\\reducer.js",[],"C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\components\\UserList.js",[],"C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\context\\users\\context.js",[],"C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\context\\users\\reducer.js",[],"C:\\Users\\sande\\GitHub\\react-usereducer-context-tutorial\\src\\context\\users\\actions.js",[],{"ruleId":"51","severity":1,"message":"52","line":6,"column":8,"nodeType":"53","messageId":"54","endLine":6,"endColumn":17},"no-unused-vars","'CardMedia' is defined but never used.","Identifier","unusedVar"]
--------------------------------------------------------------------------------