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"