├── 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"] --------------------------------------------------------------------------------