├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── _components │ ├── Alert.jsx │ ├── Nav.jsx │ └── index.js ├── _helpers │ ├── fake-backend.js │ ├── fetch-wrapper.js │ ├── index.js │ └── role.js ├── _services │ ├── alert.service.js │ ├── index.js │ └── user.service.js ├── app │ └── Index.jsx ├── home │ └── Index.jsx ├── index.html ├── index.jsx ├── styles.less └── users │ ├── AddEdit.jsx │ ├── Index.jsx │ └── List.jsx └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | "@babel/preset-env" 5 | ] 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | typings 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jason Watmore 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 | # react-hook-form-crud-example 2 | 3 | React - CRUD Example with React Hook Form 4 | 5 | For documentation and live demo go to https://jasonwatmore.com/post/2020/10/09/react-crud-example-with-react-hook-form -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hook-form-crud-example", 3 | "version": "1.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/cornflourblue/react-hook-form-crud-example.git" 7 | }, 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "webpack --mode production", 11 | "start": "webpack-dev-server --open" 12 | }, 13 | "dependencies": { 14 | "@hookform/resolvers": "^1.0.0", 15 | "prop-types": "^15.7.2", 16 | "react": "^16.13.1", 17 | "react-dom": "^16.13.1", 18 | "react-hook-form": "^6.9.2", 19 | "react-router-dom": "^5.1.2", 20 | "rxjs": "^6.5.5", 21 | "yup": "^0.29.3" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.4.3", 25 | "@babel/preset-env": "^7.4.3", 26 | "@babel/preset-react": "^7.0.0", 27 | "babel-loader": "^8.0.5", 28 | "css-loader": "^4.3.0", 29 | "html-webpack-plugin": "^4.5.0", 30 | "less": "^3.11.0", 31 | "less-loader": "^7.0.1", 32 | "path": "^0.12.7", 33 | "style-loader": "^1.1.3", 34 | "webpack": "^5.64.2", 35 | "webpack-cli": "^4.9.1", 36 | "webpack-dev-server": "^4.5.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/_components/Alert.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import { alertService, AlertType } from '../_services'; 6 | 7 | const propTypes = { 8 | id: PropTypes.string, 9 | fade: PropTypes.bool 10 | }; 11 | 12 | const defaultProps = { 13 | id: 'default-alert', 14 | fade: true 15 | }; 16 | 17 | function Alert({ id, fade }) { 18 | const history = useHistory(); 19 | const [alerts, setAlerts] = useState([]); 20 | 21 | useEffect(() => { 22 | // subscribe to new alert notifications 23 | const subscription = alertService.onAlert(id) 24 | .subscribe(alert => { 25 | // clear alerts when an empty alert is received 26 | if (!alert.message) { 27 | setAlerts(alerts => { 28 | // filter out alerts without 'keepAfterRouteChange' flag 29 | const filteredAlerts = alerts.filter(x => x.keepAfterRouteChange); 30 | 31 | // remove 'keepAfterRouteChange' flag on the rest 32 | filteredAlerts.forEach(x => delete x.keepAfterRouteChange); 33 | return filteredAlerts; 34 | }); 35 | } else { 36 | // add alert to array 37 | setAlerts(alerts => ([...alerts, alert])); 38 | 39 | // auto close alert if required 40 | if (alert.autoClose) { 41 | setTimeout(() => removeAlert(alert), 3000); 42 | } 43 | } 44 | }); 45 | 46 | // clear alerts on location change 47 | const historyUnlisten = history.listen(({ pathname }) => { 48 | // don't clear if pathname has trailing slash because this will be auto redirected again 49 | if (pathname.endsWith('/')) return; 50 | 51 | alertService.clear(id); 52 | }); 53 | 54 | // clean up function that runs when the component unmounts 55 | return () => { 56 | // unsubscribe & unlisten to avoid memory leaks 57 | subscription.unsubscribe(); 58 | historyUnlisten(); 59 | }; 60 | }, []); 61 | 62 | function removeAlert(alert) { 63 | if (fade) { 64 | // fade out alert 65 | const alertWithFade = { ...alert, fade: true }; 66 | setAlerts(alerts => alerts.map(x => x === alert ? alertWithFade : x)); 67 | 68 | // remove alert after faded out 69 | setTimeout(() => { 70 | setAlerts(alerts => alerts.filter(x => x !== alertWithFade)); 71 | }, 250); 72 | } else { 73 | // remove alert 74 | setAlerts(alerts => alerts.filter(x => x !== alert)); 75 | } 76 | } 77 | 78 | function cssClasses(alert) { 79 | if (!alert) return; 80 | 81 | const classes = ['alert', 'alert-dismissable']; 82 | 83 | const alertTypeClass = { 84 | [AlertType.Success]: 'alert alert-success', 85 | [AlertType.Error]: 'alert alert-danger', 86 | [AlertType.Info]: 'alert alert-info', 87 | [AlertType.Warning]: 'alert alert-warning' 88 | } 89 | 90 | classes.push(alertTypeClass[alert.type]); 91 | 92 | if (alert.fade) { 93 | classes.push('fade'); 94 | } 95 | 96 | return classes.join(' '); 97 | } 98 | 99 | if (!alerts.length) return null; 100 | 101 | return ( 102 |
103 |
104 | {alerts.map((alert, index) => 105 |
106 | removeAlert(alert)}>× 107 | 108 |
109 | )} 110 |
111 |
112 | ); 113 | } 114 | 115 | Alert.propTypes = propTypes; 116 | Alert.defaultProps = defaultProps; 117 | export { Alert }; -------------------------------------------------------------------------------- /src/_components/Nav.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | function Nav() { 5 | return ( 6 | 12 | ); 13 | } 14 | 15 | export { Nav }; -------------------------------------------------------------------------------- /src/_components/index.js: -------------------------------------------------------------------------------- 1 | export * from './Alert'; 2 | export * from './Nav'; 3 | -------------------------------------------------------------------------------- /src/_helpers/fake-backend.js: -------------------------------------------------------------------------------- 1 | import { Role } from './' 2 | 3 | export function configureFakeBackend() { 4 | // array in local storage for user records 5 | let users = JSON.parse(localStorage.getItem('users')) || [{ 6 | id: 1, 7 | title: 'Mr', 8 | firstName: 'Joe', 9 | lastName: 'Bloggs', 10 | email: 'joe@bloggs.com', 11 | role: Role.User, 12 | password: 'joe123' 13 | }]; 14 | 15 | // monkey patch fetch to setup fake backend 16 | let realFetch = window.fetch; 17 | window.fetch = function (url, opts) { 18 | return new Promise((resolve, reject) => { 19 | // wrap in timeout to simulate server api call 20 | setTimeout(handleRoute, 500); 21 | 22 | function handleRoute() { 23 | const { method } = opts; 24 | switch (true) { 25 | case url.endsWith('/users') && method === 'GET': 26 | return getUsers(); 27 | case url.match(/\/users\/\d+$/) && method === 'GET': 28 | return getUserById(); 29 | case url.endsWith('/users') && method === 'POST': 30 | return createUser(); 31 | case url.match(/\/users\/\d+$/) && method === 'PUT': 32 | return updateUser(); 33 | case url.match(/\/users\/\d+$/) && method === 'DELETE': 34 | return deleteUser(); 35 | default: 36 | // pass through any requests not handled above 37 | return realFetch(url, opts) 38 | .then(response => resolve(response)) 39 | .catch(error => reject(error)); 40 | } 41 | } 42 | 43 | // route functions 44 | 45 | function getUsers() { 46 | return ok(users); 47 | } 48 | 49 | function getUserById() { 50 | let user = users.find(x => x.id === idFromUrl()); 51 | return ok(user); 52 | } 53 | 54 | function createUser() { 55 | const user = body(); 56 | 57 | if (users.find(x => x.email === user.email)) { 58 | return error(`User with the email ${user.email} already exists`); 59 | } 60 | 61 | // assign user id and a few other properties then save 62 | user.id = newUserId(); 63 | user.dateCreated = new Date().toISOString(); 64 | delete user.confirmPassword; 65 | users.push(user); 66 | localStorage.setItem('users', JSON.stringify(users)); 67 | 68 | return ok(); 69 | } 70 | 71 | function updateUser() { 72 | let params = body(); 73 | let user = users.find(x => x.id === idFromUrl()); 74 | 75 | // only update password if included 76 | if (!params.password) { 77 | delete params.password; 78 | } 79 | // don't save confirm password 80 | delete params.confirmPassword; 81 | 82 | // update and save user 83 | Object.assign(user, params); 84 | localStorage.setItem('users', JSON.stringify(users)); 85 | 86 | return ok(); 87 | } 88 | 89 | function deleteUser() { 90 | users = users.filter(x => x.id !== idFromUrl()); 91 | localStorage.setItem('users', JSON.stringify(users)); 92 | 93 | return ok(); 94 | } 95 | 96 | // helper functions 97 | 98 | function ok(body) { 99 | resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(body)) }); 100 | } 101 | 102 | function error(message) { 103 | resolve({ status: 400, text: () => Promise.resolve(JSON.stringify({ message })) }); 104 | } 105 | 106 | function idFromUrl() { 107 | const urlParts = url.split('/'); 108 | return parseInt(urlParts[urlParts.length - 1]); 109 | } 110 | 111 | function body() { 112 | return opts.body && JSON.parse(opts.body); 113 | } 114 | 115 | function newUserId() { 116 | return users.length ? Math.max(...users.map(x => x.id)) + 1 : 1; 117 | } 118 | }); 119 | } 120 | }; -------------------------------------------------------------------------------- /src/_helpers/fetch-wrapper.js: -------------------------------------------------------------------------------- 1 | export const fetchWrapper = { 2 | get, 3 | post, 4 | put, 5 | delete: _delete 6 | }; 7 | 8 | function get(url) { 9 | const requestOptions = { 10 | method: 'GET' 11 | }; 12 | return fetch(url, requestOptions).then(handleResponse); 13 | } 14 | 15 | function post(url, body) { 16 | const requestOptions = { 17 | method: 'POST', 18 | headers: { 'Content-Type': 'application/json' }, 19 | body: JSON.stringify(body) 20 | }; 21 | return fetch(url, requestOptions).then(handleResponse); 22 | } 23 | 24 | function put(url, body) { 25 | const requestOptions = { 26 | method: 'PUT', 27 | headers: { 'Content-Type': 'application/json' }, 28 | body: JSON.stringify(body) 29 | }; 30 | return fetch(url, requestOptions).then(handleResponse); 31 | } 32 | 33 | // prefixed with underscored because delete is a reserved word in javascript 34 | function _delete(url) { 35 | const requestOptions = { 36 | method: 'DELETE' 37 | }; 38 | return fetch(url, requestOptions).then(handleResponse); 39 | } 40 | 41 | // helper functions 42 | 43 | function handleResponse(response) { 44 | return response.text().then(text => { 45 | const data = text && JSON.parse(text); 46 | 47 | if (!response.ok) { 48 | const error = (data && data.message) || response.statusText; 49 | return Promise.reject(error); 50 | } 51 | 52 | return data; 53 | }); 54 | } -------------------------------------------------------------------------------- /src/_helpers/index.js: -------------------------------------------------------------------------------- 1 | export * from './fake-backend'; 2 | export * from './fetch-wrapper'; 3 | export * from './role'; 4 | -------------------------------------------------------------------------------- /src/_helpers/role.js: -------------------------------------------------------------------------------- 1 | export const Role = { 2 | Admin: 'Admin', 3 | User: 'User' 4 | }; -------------------------------------------------------------------------------- /src/_services/alert.service.js: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs'; 2 | import { filter } from 'rxjs/operators'; 3 | 4 | const alertSubject = new Subject(); 5 | const defaultId = 'default-alert'; 6 | 7 | export const alertService = { 8 | onAlert, 9 | success, 10 | error, 11 | info, 12 | warn, 13 | alert, 14 | clear 15 | }; 16 | 17 | export const AlertType = { 18 | Success: 'Success', 19 | Error: 'Error', 20 | Info: 'Info', 21 | Warning: 'Warning' 22 | }; 23 | 24 | // enable subscribing to alerts observable 25 | function onAlert(id = defaultId) { 26 | return alertSubject.asObservable().pipe(filter(x => x && x.id === id)); 27 | } 28 | 29 | // convenience methods 30 | function success(message, options) { 31 | alert({ ...options, type: AlertType.Success, message }); 32 | } 33 | 34 | function error(message, options) { 35 | alert({ ...options, type: AlertType.Error, message }); 36 | } 37 | 38 | function info(message, options) { 39 | alert({ ...options, type: AlertType.Info, message }); 40 | } 41 | 42 | function warn(message, options) { 43 | alert({ ...options, type: AlertType.Warning, message }); 44 | } 45 | 46 | // core alert method 47 | function alert(alert) { 48 | alert.id = alert.id || defaultId; 49 | alert.autoClose = (alert.autoClose === undefined ? true : alert.autoClose); 50 | alertSubject.next(alert); 51 | } 52 | 53 | // clear alerts 54 | function clear(id = defaultId) { 55 | alertSubject.next({ id }); 56 | } -------------------------------------------------------------------------------- /src/_services/index.js: -------------------------------------------------------------------------------- 1 | export * from './alert.service'; 2 | export * from './user.service'; 3 | -------------------------------------------------------------------------------- /src/_services/user.service.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import { fetchWrapper } from '@/_helpers'; 3 | 4 | const baseUrl = `${config.apiUrl}/users`; 5 | 6 | export const userService = { 7 | getAll, 8 | getById, 9 | create, 10 | update, 11 | delete: _delete 12 | }; 13 | 14 | function getAll() { 15 | return fetchWrapper.get(baseUrl); 16 | } 17 | 18 | function getById(id) { 19 | return fetchWrapper.get(`${baseUrl}/${id}`); 20 | } 21 | 22 | function create(params) { 23 | return fetchWrapper.post(baseUrl, params); 24 | } 25 | 26 | function update(id, params) { 27 | return fetchWrapper.put(`${baseUrl}/${id}`, params); 28 | } 29 | 30 | // prefixed with underscored because delete is a reserved word in javascript 31 | function _delete(id) { 32 | return fetchWrapper.delete(`${baseUrl}/${id}`); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/Index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch, Redirect, useLocation } from 'react-router-dom'; 3 | 4 | import { Nav, Alert } from '@/_components'; 5 | import { Home } from '@/home'; 6 | import { Users } from '@/users'; 7 | 8 | function App() { 9 | const { pathname } = useLocation(); 10 | 11 | return ( 12 |
13 |
24 | ); 25 | } 26 | 27 | export { App }; -------------------------------------------------------------------------------- /src/home/Index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | function Home() { 5 | return ( 6 |
7 |

React - CRUD Example with React Hook Form

8 |

An example app showing how to list, add, edit and delete user records with React and the React Hook Form library.

9 |

>> Manage Users

10 |
11 | ); 12 | } 13 | 14 | export { Home }; -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React - CRUD Example with React Hook Form 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 |
16 |

17 | React - CRUD Example with React Hook Form 18 |

19 |

20 | JasonWatmore.com 21 |

22 |
23 | 24 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import { render } from 'react-dom'; 4 | 5 | import { App } from './app'; 6 | 7 | import './styles.less'; 8 | 9 | // setup fake backend 10 | import { configureFakeBackend } from './_helpers'; 11 | configureFakeBackend(); 12 | 13 | render( 14 | 15 | 16 | , 17 | document.getElementById('app') 18 | ); -------------------------------------------------------------------------------- /src/styles.less: -------------------------------------------------------------------------------- 1 | // global styles 2 | a { cursor: pointer; } 3 | 4 | .app-container { 5 | min-height: 350px; 6 | } 7 | 8 | .btn-delete-user { 9 | width: 40px; 10 | text-align: center; 11 | box-sizing: content-box; 12 | } -------------------------------------------------------------------------------- /src/users/AddEdit.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { useForm } from "react-hook-form"; 4 | import { yupResolver } from '@hookform/resolvers/yup'; 5 | import * as Yup from 'yup'; 6 | 7 | import { userService, alertService } from '@/_services'; 8 | 9 | function AddEdit({ history, match }) { 10 | const { id } = match.params; 11 | const isAddMode = !id; 12 | 13 | // form validation rules 14 | const validationSchema = Yup.object().shape({ 15 | title: Yup.string() 16 | .required('Title is required'), 17 | firstName: Yup.string() 18 | .required('First Name is required'), 19 | lastName: Yup.string() 20 | .required('Last Name is required'), 21 | email: Yup.string() 22 | .email('Email is invalid') 23 | .required('Email is required'), 24 | role: Yup.string() 25 | .required('Role is required'), 26 | password: Yup.string() 27 | .transform(x => x === '' ? undefined : x) 28 | .concat(isAddMode ? Yup.string().required('Password is required') : null) 29 | .min(6, 'Password must be at least 6 characters'), 30 | confirmPassword: Yup.string() 31 | .transform(x => x === '' ? undefined : x) 32 | .when('password', (password, schema) => { 33 | if (password || isAddMode) return schema.required('Confirm Password is required'); 34 | }) 35 | .oneOf([Yup.ref('password')], 'Passwords must match') 36 | }); 37 | 38 | // functions to build form returned by useForm() hook 39 | const { register, handleSubmit, reset, setValue, errors, formState } = useForm({ 40 | resolver: yupResolver(validationSchema) 41 | }); 42 | 43 | function onSubmit(data) { 44 | return isAddMode 45 | ? createUser(data) 46 | : updateUser(id, data); 47 | } 48 | 49 | function createUser(data) { 50 | return userService.create(data) 51 | .then(() => { 52 | alertService.success('User added', { keepAfterRouteChange: true }); 53 | history.push('.'); 54 | }) 55 | .catch(alertService.error); 56 | } 57 | 58 | function updateUser(id, data) { 59 | return userService.update(id, data) 60 | .then(() => { 61 | alertService.success('User updated', { keepAfterRouteChange: true }); 62 | history.push('..'); 63 | }) 64 | .catch(alertService.error); 65 | } 66 | 67 | useEffect(() => { 68 | if (!isAddMode) { 69 | // get user and set form fields 70 | userService.getById(id).then(user => { 71 | const fields = ['title', 'firstName', 'lastName', 'email', 'role']; 72 | fields.forEach(field => setValue(field, user[field])); 73 | }); 74 | } 75 | }, []); 76 | 77 | return ( 78 |
79 |

{isAddMode ? 'Add User' : 'Edit User'}

80 |
81 |
82 | 83 | 90 |
{errors.title?.message}
91 |
92 |
93 | 94 | 95 |
{errors.firstName?.message}
96 |
97 |
98 | 99 | 100 |
{errors.lastName?.message}
101 |
102 |
103 |
104 |
105 | 106 | 107 |
{errors.email?.message}
108 |
109 |
110 | 111 | 116 |
{errors.role?.message}
117 |
118 |
119 | {!isAddMode && 120 |
121 |

Change Password

122 |

Leave blank to keep the same password

123 |
124 | } 125 |
126 |
127 | 128 | 129 |
{errors.password?.message}
130 |
131 |
132 | 133 | 134 |
{errors.confirmPassword?.message}
135 |
136 |
137 |
138 | 142 | Cancel 143 |
144 |
145 | ); 146 | } 147 | 148 | export { AddEdit }; -------------------------------------------------------------------------------- /src/users/Index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | 4 | import { List } from './List'; 5 | import { AddEdit } from './AddEdit'; 6 | 7 | function Users({ match }) { 8 | const { path } = match; 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export { Users }; -------------------------------------------------------------------------------- /src/users/List.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { userService } from '@/_services'; 5 | 6 | function List({ match }) { 7 | const { path } = match; 8 | const [users, setUsers] = useState(null); 9 | 10 | useEffect(() => { 11 | userService.getAll().then(x => setUsers(x)); 12 | }, []); 13 | 14 | function deleteUser(id) { 15 | setUsers(users.map(x => { 16 | if (x.id === id) { x.isDeleting = true; } 17 | return x; 18 | })); 19 | userService.delete(id).then(() => { 20 | setUsers(users => users.filter(x => x.id !== id)); 21 | }); 22 | } 23 | 24 | return ( 25 |
26 |

Users

27 | Add User 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {users && users.map(user => 39 | 40 | 41 | 42 | 43 | 52 | 53 | )} 54 | {!users && 55 | 56 | 59 | 60 | } 61 | {users && !users.length && 62 | 63 | 66 | 67 | } 68 | 69 |
NameEmailRole
{user.title} {user.firstName} {user.lastName}{user.email}{user.role} 44 | Edit 45 | 51 |
57 |
58 |
64 |
No Users To Display
65 |
70 |
71 | ); 72 | } 73 | 74 | export { List }; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader' 11 | }, 12 | { 13 | test: /\.less$/, 14 | use: [ 15 | { loader: 'style-loader' }, 16 | { loader: 'css-loader' }, 17 | { loader: 'less-loader' } 18 | ] 19 | } 20 | ] 21 | }, 22 | resolve: { 23 | mainFiles: ['index', 'Index'], 24 | extensions: ['.js', '.jsx'], 25 | alias: { 26 | '@': path.resolve(__dirname, 'src/'), 27 | } 28 | }, 29 | plugins: [new HtmlWebpackPlugin({ 30 | template: './src/index.html' 31 | })], 32 | devServer: { 33 | historyApiFallback: true 34 | }, 35 | externals: { 36 | // global app config object 37 | config: JSON.stringify({ 38 | apiUrl: 'http://localhost:4000' 39 | }) 40 | } 41 | } --------------------------------------------------------------------------------