├── .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 |
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 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
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 |
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 | Name |
32 | Email |
33 | Role |
34 | |
35 |
36 |
37 |
38 | {users && users.map(user =>
39 |
40 | {user.title} {user.firstName} {user.lastName} |
41 | {user.email} |
42 | {user.role} |
43 |
44 | Edit
45 |
51 | |
52 |
53 | )}
54 | {!users &&
55 |
56 |
57 |
58 | |
59 |
60 | }
61 | {users && !users.length &&
62 |
63 |
64 | No Users To Display
65 | |
66 |
67 | }
68 |
69 |
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 | }
--------------------------------------------------------------------------------