diff --git a/package.json b/package.json
index 4c08fa598..c897eda4b 100644
--- a/package.json
+++ b/package.json
@@ -33,14 +33,14 @@
"license": "MIT",
"dependencies": {
"@hot-loader/react-dom": "16.10.2",
+ "@reduxjs/toolkit": "1.0.4",
"connected-react-router": "6.5.2",
"object-assign": "4.1.1",
"react": "16.11.0",
"react-dom": "16.11.0",
"react-redux": "7.1.1",
"react-router-dom": "5.1.2",
- "redux": "4.0.4",
- "redux-thunk": "2.3.0"
+ "redux": "4.0.4"
},
"devDependencies": {
"@babel/cli": "7.6.4",
@@ -91,7 +91,6 @@
"raf": "3.4.1",
"react-hot-loader": "4.12.15",
"react-test-renderer": "16.11.0",
- "redux-immutable-state-invariant": "2.1.0",
"redux-mock-store": "1.5.3",
"replace": "1.1.1",
"rimraf": "3.0.0",
diff --git a/src/components/containers/FuelSavingsPage.js b/src/components/containers/FuelSavingsPage.js
index 61e88dabb..c75f712e2 100644
--- a/src/components/containers/FuelSavingsPage.js
+++ b/src/components/containers/FuelSavingsPage.js
@@ -1,17 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
-import {bindActionCreators} from 'redux';
-import * as actions from '../../actions/fuelSavingsActions';
+import {calculateFuelSavings, saveFuelSavings} from '../../actions/fuelSavingsActions';
import FuelSavingsForm from '../FuelSavingsForm';
export class FuelSavingsPage extends React.Component {
saveFuelSavings = () => {
- this.props.actions.saveFuelSavings(this.props.fuelSavings);
+ this.props.saveFuelSavings(this.props.fuelSavings);
}
calculateFuelSavings = e => {
- this.props.actions.calculateFuelSavings(this.props.fuelSavings, e.target.name, e.target.value);
+ this.props.calculateFuelSavings(this.props.fuelSavings, e.target.name, e.target.value);
}
render() {
@@ -26,7 +25,8 @@ export class FuelSavingsPage extends React.Component {
}
FuelSavingsPage.propTypes = {
- actions: PropTypes.object.isRequired,
+ saveFuelSavings: PropTypes.func.isRequired,
+ calculateFuelSavings: PropTypes.func.isRequired,
fuelSavings: PropTypes.object.isRequired
};
@@ -36,13 +36,12 @@ function mapStateToProps(state) {
};
}
-function mapDispatchToProps(dispatch) {
- return {
- actions: bindActionCreators(actions, dispatch)
- };
+const mapDispatchToProps = {
+ calculateFuelSavings,
+ saveFuelSavings,
}
export default connect(
mapStateToProps,
- mapDispatchToProps
+ mapDispatchToProps,
)(FuelSavingsPage);
diff --git a/src/components/containers/FuelSavingsPage.spec.js b/src/components/containers/FuelSavingsPage.spec.js
index f6f6954b8..8e8fead03 100644
--- a/src/components/containers/FuelSavingsPage.spec.js
+++ b/src/components/containers/FuelSavingsPage.spec.js
@@ -16,7 +16,7 @@ describe("", () => {
it("should contain ", () => {
const wrapper = shallow(
);
@@ -27,7 +27,7 @@ describe("", () => {
it("calls saveFuelSavings upon clicking save", () => {
const wrapper = mount(
);
@@ -43,7 +43,7 @@ describe("", () => {
it("calls calculateFuelSavings upon changing a field", () => {
const wrapper = mount(
);
diff --git a/src/features/fuelSavings/fuelSavingsSlice.js b/src/features/fuelSavings/fuelSavingsSlice.js
new file mode 100644
index 000000000..80dad700a
--- /dev/null
+++ b/src/features/fuelSavings/fuelSavingsSlice.js
@@ -0,0 +1,46 @@
+import {createSlice} from "@reduxjs/toolkit";
+import {necessaryDataIsProvidedToCalculateSavings, calculateSavings} from '../../utils/fuelSavings';
+
+const initialState = {
+ newMpg: '',
+ tradeMpg: '',
+ newPpg: '',
+ tradePpg: '',
+ milesDriven: '',
+ milesDrivenTimeframe: 'week',
+ displayResults: false,
+ dateModified: null,
+ necessaryDataIsProvidedToCalculateSavings: false,
+ savings: {
+ monthly: 0,
+ annual: 0,
+ threeYear: 0
+ }
+}
+
+const fuelSavingsSlice = createSlice({
+ name: "fuelSavings",
+ initialState,
+ reducers: {
+ saveFuelSavings(state, action) {
+ // For this example, just simulating a save by changing date modified.
+ // In a real app using Redux, you might use redux-thunk and handle the async call in fuelSavingsActions.js
+ state.dateModified = action.payload.dateModified;
+ },
+ calculateFuelSavings(state, action) {
+ const {fieldName, value, dateModified} = action.payload;
+
+ state[fieldName] = value;
+ state.necessaryDataIsProvidedToCalculateSavings = necessaryDataIsProvidedToCalculateSavings(state);
+ state.dateModified = dateModified;
+
+ if(state.necessaryDataIsProvidedToCalculateSavings) {
+ state.savings = calculateSavings(state);
+ }
+ }
+ }
+})
+
+export const {saveFuelSavings, calculateFuelSavings} = fuelSavingsSlice.actions;
+
+export default fuelSavingsSlice.reducer;
\ No newline at end of file
diff --git a/src/features/fuelSavings/fuelSavingsSlice.spec.js b/src/features/fuelSavings/fuelSavingsSlice.spec.js
new file mode 100644
index 000000000..4f1d66704
--- /dev/null
+++ b/src/features/fuelSavings/fuelSavingsSlice.spec.js
@@ -0,0 +1,67 @@
+import reducer, {saveFuelSavings, calculateFuelSavings} from './fuelSavingsSlice';
+import {getFormattedDateTime} from '../../utils/dates';
+
+describe('Reducers::FuelSavings', () => {
+ const getInitialState = () => {
+ return {
+ newMpg: '',
+ tradeMpg: '',
+ newPpg: '',
+ tradePpg: '',
+ milesDriven: '',
+ milesDrivenTimeframe: 'week',
+ displayResults: false,
+ dateModified: null,
+ necessaryDataIsProvidedToCalculateSavings: false,
+ savings: {
+ monthly: 0,
+ annual: 0,
+ threeYear: 0
+ }
+ };
+ };
+
+ const getAppState = () => {
+ return {
+ newMpg: 20,
+ tradeMpg: 10,
+ newPpg: 1.50,
+ tradePpg: 1.50,
+ milesDriven: 100,
+ milesDrivenTimeframe: 'week',
+ displayResults: false,
+ dateModified: null,
+ necessaryDataIsProvidedToCalculateSavings: false,
+ savings: {
+ monthly: 0,
+ annual: 0,
+ threeYear: 0
+ }
+ };
+ };
+ const dateModified = getFormattedDateTime();
+
+ it('should set initial state by default', () => {
+ const action = { type: 'unknown' };
+ const expected = getInitialState();
+
+ expect(reducer(undefined, action)).toEqual(expected);
+ });
+
+ it('should handle saveFuelSavings', () => {
+ const action = saveFuelSavings({dateModified, settings: getAppState() })
+ const expected = Object.assign(getAppState(), { dateModified });
+
+ expect(reducer(getAppState(), action)).toEqual(expected);
+ });
+
+ it('should handle calculateFuelSavings', () => {
+ const action = calculateFuelSavings({dateModified, settings: getAppState(), fieldName: 'newMpg', value: 30 });
+
+ const expectedMpg = 30;
+ const expectedSavings = { monthly: '$43.33', annual: '$519.96', threeYear: '$1,559.88' };
+
+ expect(reducer(getAppState(), action).newMpg).toEqual(expectedMpg);
+ expect(reducer(getAppState(), action).savings).toEqual(expectedSavings);
+ });
+})
\ No newline at end of file
diff --git a/src/reducers/index.js b/src/reducers/index.js
index 8f493ca6a..93de85714 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -2,9 +2,9 @@ import { combineReducers } from 'redux';
import fuelSavings from './fuelSavingsReducer';
import { connectRouter } from 'connected-react-router'
-const rootReducer = history => combineReducers({
+const createRootReducer = history => combineReducers({
router: connectRouter(history),
fuelSavings,
});
-export default rootReducer;
+export default createRootReducer;
diff --git a/src/store/configureStore.js b/src/store/configureStore.js
index fa9c864b9..50db67a9f 100644
--- a/src/store/configureStore.js
+++ b/src/store/configureStore.js
@@ -1,6 +1,4 @@
-import {createStore, compose, applyMiddleware} from 'redux';
-import reduxImmutableStateInvariant from 'redux-immutable-state-invariant';
-import thunk from 'redux-thunk';
+import {configureStore, getDefaultMiddleware} from "@reduxjs/toolkit"
import { createBrowserHistory } from "history";
// 'routerMiddleware': the new way of storing route changes with redux middleware since rrV4.
import { connectRouter, routerMiddleware } from 'connected-react-router';
@@ -9,46 +7,20 @@ import createRootReducer from '../reducers';
export const history = createBrowserHistory();
const connectRouterHistory = connectRouter(history);
-function configureStoreProd(initialState) {
+function configureAppStore(initialState) {
const reactRouterMiddleware = routerMiddleware(history);
- const middlewares = [
- // Add other middleware on this line...
- // thunk middleware can also accept an extra argument to be passed to each thunk action
- // https://github.com/reduxjs/redux-thunk#injecting-a-custom-argument
- thunk,
- reactRouterMiddleware,
- ];
+ const isProduction = process.env.NODE_ENV === 'production';
- return createStore(
- createRootReducer(history), // root reducer with router state
- initialState,
- compose(applyMiddleware(...middlewares))
- );
-}
-
-function configureStoreDev(initialState) {
- const reactRouterMiddleware = routerMiddleware(history);
- const middlewares = [
- // Add other middleware on this line...
-
- // Redux middleware that spits an error on you when you try to mutate your state either inside a dispatch or between dispatches.
- reduxImmutableStateInvariant(),
+ const store = configureStore({
+ reducer: createRootReducer(history), // root reducer with router state,
+ preloadedState: initialState,
+ // customize the default middleware via options if desired
+ middleware: [...getDefaultMiddleware(), reactRouterMiddleware],
+ devTools: !isProduction,
+ })
- // thunk middleware can also accept an extra argument to be passed to each thunk action
- // https://github.com/reduxjs/redux-thunk#injecting-a-custom-argument
- thunk,
- reactRouterMiddleware,
- ];
-
- const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // add support for Redux dev tools
- const store = createStore(
- createRootReducer(history), // root reducer with router state
- initialState,
- composeEnhancers(applyMiddleware(...middlewares))
- );
-
- if (module.hot) {
+ if (!isProduction && module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
const nextRootReducer = require('../reducers').default; // eslint-disable-line global-require
@@ -59,6 +31,4 @@ function configureStoreDev(initialState) {
return store;
}
-const configureStore = process.env.NODE_ENV === 'production' ? configureStoreProd : configureStoreDev;
-
-export default configureStore;
+export default configureAppStore;
diff --git a/yarn.lock b/yarn.lock
index 21edde94c..408fd2a27 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -955,6 +955,18 @@
"@types/istanbul-reports" "^1.1.1"
"@types/yargs" "^13.0.0"
+"@reduxjs/toolkit@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.0.4.tgz#cec88446a22a98b48808af7ab19a58aa93e6f5f9"
+ integrity sha512-nyCZ9/CpnMXFZ//0wm1mNPSEl0J0bCghY2qeHM8zuubaBBMBr6KsIaLLms1jThbOJ1O+Ej0Tl11z5naE9czfzA==
+ dependencies:
+ immer "^4.0.1"
+ redux "^4.0.0"
+ redux-devtools-extension "^2.13.8"
+ redux-immutable-state-invariant "^2.1.0"
+ redux-thunk "^2.3.0"
+ reselect "^4.0.0"
+
"@types/babel__core@^7.1.0":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30"
@@ -4639,6 +4651,11 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
+immer@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-4.0.2.tgz#9ff0fcdf88e06f92618a5978ceecb5884e633559"
+ integrity sha512-Q/tm+yKqnKy4RIBmmtISBlhXuSDrB69e9EKTYiIenIKQkXBQir43w+kN/eGiax3wt1J0O1b2fYcNqLSbEcXA7w==
+
immutable@^3, immutable@^3.8.1:
version "3.8.2"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
@@ -8069,7 +8086,12 @@ redent@^2.0.0:
indent-string "^3.0.0"
strip-indent "^2.0.0"
-redux-immutable-state-invariant@2.1.0:
+redux-devtools-extension@^2.13.8:
+ version "2.13.8"
+ resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1"
+ integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==
+
+redux-immutable-state-invariant@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.0.tgz#308fd3cc7415a0e7f11f51ec997b6379c7055ce1"
integrity sha512-3czbDKs35FwiBRsx/3KabUk5zSOoTXC+cgVofGkpBNv3jQcqIe5JrHcF5AmVt7B/4hyJ8MijBIpCJ8cife6yJg==
@@ -8084,12 +8106,12 @@ redux-mock-store@1.5.3:
dependencies:
lodash.isplainobject "^4.0.6"
-redux-thunk@2.3.0:
+redux-thunk@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
-redux@4.0.4:
+redux@4.0.4, redux@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796"
integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==
@@ -8272,6 +8294,11 @@ requires-port@1.x.x:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+reselect@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
+ integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
+
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"