by Matthew Choi

Building Tesla’s Battery Range Calculator with React (Part 2: Redux version)

WbgsWs24LX2LeimcxUV1mX975GX20AoDTA0R

This tutorial is the second part of building Tesla’s battery range calculator with React.

In part 1, after constructing the project through create-react-app, we implemented each component by subdividing the UI. We managed the state and event using local state and props, and completed the entire app.

If you haven’t read it yet, be sure to check out Part 1, which focuses on React, here:

Building Tesla’s Battery Range Calculator with React (Part 1)
In this series of articles, I will walk you through the process of building Tesla’s battery range calculator with React…medium.freecodecamp.com

In this installment, we’ll introduce Redux, a state management solution, to see how we can transform our application into an application that manages the state of the app with Redux.

This is the final image of our application in part 2:

Li0MK48ShwxYzOPdWJ0059b54M1Y5mesLhoj

? Check out the part 2 live demo.

Before we taking a look into what Redux is, let’s see why we need to use Redux to solve problems.

1. What problem do we solve?

Redux is getting becoming the de facto way to build React apps. But should Redux be used in all React apps? At the very least, not all apps will need an ambitious state management solution from the beginning.

Today’s front-end development trends are component-based. Components can have data state and UI state, and the state that they need to manage becomes more and more complicated as your app grows.

State management solutions have emerged to solve the following problems, and Redux is becoming popular as a standard among other solutions.

  • components share state
  • state should be accessible from anywhere
  • components need to mutate the state
  • components need to mutate the state of other components

Redux is a state management library, which is a tool that allows you to store the state of our app somewhere, mutate the state, and receive the updated state.

In other words, with Redux, we have one place where we can refer the state, change the state, and get the updated state.

Redux was written with React in mind, but it is also framework-agnostic and even can be used with Angular or jQuery applications.

I recommend you read Dan Abramov’s You Might Not Need Redux before choosing Redux.

2. Data flow in Redux

As you saw in the part 1, in React, the data is passed through the component using props. This is called unidirectional data flow that flows from parent to child.

Due to these characteristics, communication between components other than parent-child relationship is not clear.

D6WWNQW1cIy98DIqBqpMbaz5cG5DGxAIAPTM

React does not recommend direct component-to-component communication as shown above. There is a suggested way for this in React, but you have to implement it yourself.

According to React docs:

For communication between two components that don’t have a parent-child relationship, you can set up your own global event system. … Flux pattern is one of the possible ways to arrange this.
mzhkN3GbyDepg6QmY2oNVRt9-pQUVfTNPtw8

This is where Redux comes in handy.

Redux provides a solution for managing all application state in a single place called a store.

The component then dispatches the state change to the store instead of passing it directly to the other components

The components that need to be aware of state changes can subscribe to the store.

Redux is, in a word, a state container that represents and manages the state of an app as a single object from a JavaScript-based application.

3. Redux Core Concept

Redux itself is very simple. The state of the app we created in the last article can be represented as a generic object like this:

r-QHkrWij0nOfDNEcqcMlGeeT0uHFoy7KlZa

This object is the same as the model without setters.

To change this state in Redux, you must dispatch an action.

Actions are plain objects describing what happened in the app, and serve as the sole way to describe an intention to mutate the data. It’s one of the fundamental design choices of Redux.

Here are some examples to be implemented in our app soon.

i6SGgqJFAiUVMfH0MnnXKevPxQgFc3gYF3Ox

Forcing all of these state changes into action will give us a clear understanding of what’s going on in your app. When something happens, we can see why it happened.

Now we need a function called reducer to bind these states and actions together. Reducer is nothing more than a function that takes a state and an action as arguments and returns a new state.

In a word:

(state, action) => state

Actions only describe that something happened and don’t specify how the application’s state changes in response. This is the job of reducers.

Here is one example of a reducer to implement in our app:

l9hxbT8w2vX33pXWjY9MHJhSJYsiKEwIxaPy

4. Redux Three Principles

I’ve mentioned Flux a few times. Flux is a pattern of state management, not a downloadable tool like Redux. Redux, on the other hand, is a practical implementation of the Flux pattern and has three main principles.

4.1 Single source of truth

The state of the entire application is stored in an object tree within a single store.
msDr2OXjaSjecjp1J0LExZoeR07djQGP0LHO

Since all states exist in one place, this is called a single source of truth.

This one-store approach of Redux is one of the primary differences between it and Flux’s multiple store approach.

What are the advantages of a single state tree? This makes it easier to debug applications or perform internal inspections and to easily implement some features that were previously difficult to implement (for example, undo / redo).

4.2 State is read-only

The only way to change the state is to emit an action that describes what happened.

In other words, the application does not directly change the state, but instead expresses the intention to transform the state by passing the action.

In fact, if you look at the Redux API, you can see that it consists of just four methods:

store.dispatch(action)store.subscribe(listener)store.getState()replaceReducer(nextReducer)

As you can see, there is no setState() method. Therefore, passing an action is the only channel that can mutate the state of the application.

4.3 Changes are made with pure functions

You write reducers as pure functions to specify the concrete way the state tree is transformed by action.

Reducers are pure functions that take a previous state and action and return a new state. Keep in mind that you must return a new state object instead of changing the old state.

“Given the same arguments, it should calculate the next state and return it. No surprises. No side effects. No API calls. No mutations. Just a calculation.” — Redux Docs

The pure function has the following characteristics:

  • It does not make outside network or database calls.
  • Its return value depends solely on the values of its parameters.
  • Its arguments should be considered “immutable”, meaning they should not be changed.
  • Calling a pure function with the same set of arguments will always return the same value.

5. Divide The App Into Containers and Components

Now, let’s re-build our Tesla calculator app that we made in Part 1 as a Redux version.

First, let’s look at the overall component UI layout of the app that will be implemented in this article.

O2G59T0ExYs5ADbHl7lgwfc9vJgM0n12huLE
overall component UI layout

Placing React and Redux logic inside a single component can be messy, so it is recommended that you create a Presentational component for presentation purposes only, and a Container component, an upper wrapper component that handles Redux and dispatches actions.

The role of the parent Container component is to provide state values to presentational components, to manage events, and to communicate with Redux on behalf of presentational components.

6. List State and Actions For Each Component

Refer to the entire component layout to create a list of states and actions for each component:

TeslaCar Container :	state : wheels	action : N/A
TeslaStats Container :	state : carstats(array)	action : N/A	TeslaSpeedCounter Container : 	state : config.speed	action : SPEED_UP, SPEED_DOWN
TeslaTempCounter Container : 	state : config.temperature	action : TEMPERATURE_UP, TEMPERATURE_DOWN	TeslaClimate Container : 	state : config.climate	action : CHANGE_CLIMATE
TeslaWheel Container : 	state : config.wheel	action : CHANGE_WHEEL

7. Set up part 1 project code base

If you want to go directly to part 2 without looking at part 1, you need to build the codebase first by cloning the part 1 code.

After the npm start, let’s make sure the application works.

nZhI6ZwDG3l0rwSv6ZdW6cN4qCKxVlghhoAq
  • npm install
DZNzI-OVy32CzU4Ww0w1JDCie3PB2wCGnsrY
  • npm start
xtCHz-FGbWLurL5bPkPPhh23E3taWyYPLhRw

8. Create Action Creators For Each Action

Now that you’ve created an action list, it’s time to create action creators.

An action creator is a function that literally creates an action object. In Redux, action creators simply return an action object and pass the argument value if necessary.

changeWheel action creator sample :

const changeWheel = (value) => {  return {    type: 'CHANGE_WHEEL',    value  }}

These action creators are passed to the dispatch function as the result value, and the dispatch is executed.

dispatch(changeWheel(size))

The dispatch function can be accessed directly from the store as store.dispatch(), but in most cases it will be accessed using a helper like react-redux’s connect(). We’ll look at connect later.

8.1 Create Action.js

Create an index file in the src/actions directory and define action creators as follows:

src/actions/index.js

import { counterDefaultVal } from '../constants/counterDefaultVal';
export const speedUp = (value) => {  return {    type: 'SPEED_UP',    value,    step: counterDefaultVal.speed.step,    maxValue: counterDefaultVal.speed.max  }}
export const speedDown = (value) => {  return {    type: 'SPEED_DOWN',    value,    step: counterDefaultVal.speed.step,    minValue: counterDefaultVal.speed.min  }}
export const temperatureUp = (value) => {  return {    type: 'TEMPERATURE_UP',    value,    step: counterDefaultVal.temperature.step,    maxValue: counterDefaultVal.temperature.max  }}
export const temperatureDown = (value) => {  return {    type: 'TEMPERATURE_DOWN',    value,    step: counterDefaultVal.temperature.step,    minValue: counterDefaultVal.temperature.min  }}
export const changeClimate = () => {  return {    type: 'CHANGE_CLIMATE'  }}
export const changeWheel = (value) => {  return {    type: 'CHANGE_WHEEL',    value  }}
export const updateStats = () => {  return {    type: 'UPDATE_STATS'  }}

Because we need default values based on the action creator, we define this constant value in constants/counterDefaultVal under src directory and import it.

src/constants/counterDefaultVal.js

export const counterDefaultVal = {  speed: {    title: "Speed",    unit: "mph",    step: 5,    min: 45,    max: 70  },  temperature: {    title: "Outside Temperature",    unit: "°",    step: 10,    min: -10,    max: 40  }}

9. Create Reducers For Each Action

Reducers are functions that receive state and action objects from a Redux store and return a new state to be stored back into Redux.

It’s important not to directly modify the given state here. Reducers must be pure functions and must return a new state.

  • Reducer functions are called from the Container that will be created when a user action occurs.
  • When the Reducer returns a state, Redux passes the new state to each component, and React renders each component again.

9.1 Immutable Data Structures

  • JavaScript primitive data type(number, string, boolean, undefined, and null) => immutable
  • Object, array and function => mutable

Changes to the data structure are known to be buggy. Since our store consists of state objects and arrays, we need to implement a strategy to keep the state immutable.

There are three ways to change the state here:

ES5

// Example Onestate.foo = '123';
// Example TwoObject.assign(state, { foo: 123 });
// Example Threevar newState = Object.assign({}, state, { foo: 123 });

In the example above, the first and second mutate the state object. The second example mutates because Object.assign() merges all its arguments into the first argument.

The third example doesn’t mutate the state. It merges the contents of state and { foo: 123 } into a new empty object which is the first argument.

The spread operator introduced in ES6 provides a simpler way to keep the state immutable.

ES6 (ES2015)

const newState = { ...state, foo: 123 };
For more information about the spread operator, see here.

9.2 Create a Reducer for ChangeClimate

First, we will create ChangeClimate through test-driven development method.

In Part1, our app was generated through create-react-app, so we basically use jest as test runner.

The jest looks for a test file using one of the following naming conventions:

Files with .js suffix in __tests__ foldersFiles with .test.js suffixFiles with .spec.js suffix

Create teslaRangeApp.spec.js in src/reducers and create a test case.

describe('test reducer', () => {  it('should handle initial stat', () => {    expect(      appReducer(undefined, {})    ).toEqual(initialState)  })})

After create the test, run the npm test command. You should be able to see the following test failure message. This is because we have not written the appReducer yet.

qfssmqzhC-uiewXk7Tdd9p9oSQ0RuETDUEdb
npm test console

To make the first test successful, we need to create teslaRangeApp.js in the same reducers directory and write initial state and reducer functions.

src/reducers/teslaRangeApp.js

const initialState = {  carstats:[    {miles:246, model:"60"},    {miles:250, model:"60D"},    {miles:297, model:"75"},    {miles:306, model:"75D"},    {miles:336, model:"90D"},    {miles:376, model:"P100D"}  ],  config: {    speed: 55,    temperature: 20,    climate: true,    wheels: 19  }}
function appReducer(state = initialState, action) {  switch (action.type) {        default:      return state   }}
export default appReducer;

Next, import teslaRangeApp.js from teslaRangeApp.spec.js and set initialState.

src/reducers/teslaRangeApp.spec.js

import appReducer from './teslaRangeApp';
const initialState =  {  carstats:[    {miles:246, model:"60"},    {miles:250, model:"60D"},    {miles:297, model:"75"},    {miles:306, model:"75D"},    {miles:336, model:"90D"},    {miles:376, model:"P100D"}  ],  config: {    speed: 55,    temperature: 20,    climate: true,    wheels: 19  }}
describe('test reducer', () => {  it('should handle initial stat', () => {    expect(      appReducer(undefined, {})    ).toEqual(initialState)  })})

Run npm test again and the test will succeed.

In the current test case, the action type is {}, so the initialState is returned.

SY4N0ajH7uQMJ3X0FiQsWaIrGbrlzWxErc8Z

Now let’s test the CHANGE_CLIMATE action.

Add the following climateChangeState and CHANGE_CLIMATE test cases to teslaRangeApp.spec.js.

const climateChangeState = {  carstats:[    {miles:267, model:"60"},    {miles:273, model:"60D"},    {miles:323, model:"75"},    {miles:334, model:"75D"},    {miles:366, model:"90D"},    {miles:409, model:"P100D"}  ],  config: {    speed: 55,    temperature: 20,    climate: false,    wheels: 19  }}
it('should handle CHANGE_CLIMATE', () => {    expect(      appReducer(initialState,{        type: 'CHANGE_CLIMATE'      })    ).toEqual(climateChangeState)  })

Then add the CHANGE_CLIMATE case, updateStats, and calculateStats functions to teslaRangeApp.js. Then import the BatteryService.js that was used in part 1.

import { getModelData } from '../services/BatteryService';
function updateStats(state, newState) {  return {    ...state,    config:newState.config,    carstats:calculateStats(newState)  }  }
function calculateStats(state) {  const models = ['60', '60D', '75', '75D', '90D', 'P100D'];  const dataModels = getModelData();  return models.map(model => {    const { speed, temperature, climate, wheels } = state.config;    const miles = dataModels[model][wheels][climate ? 'on' : 'off'].speed[speed][temperature];    return {      model,      miles    };  });}
function appReducer(state = initialState, action) {  switch (action.type) {    case 'CHANGE_CLIMATE': {      const newState = {        ...state,        config: {          climate: !state.config.climate,          speed: state.config.speed,          temperature: state.config.temperature,          wheels: state.config.wheels        }      };      return updateStats(state, newState);    }    default:      return state  }}

If you check the test results, you can see that the two test cases are successful.

lI0QvpPXhWKbgre-ziHBMqQwvFvsDnyjscmz

What we have implemented so far is that the changes in the state that occur when the user turns the air conditioner on and off in the application through the test runner only from the viewpoint of Action and Reducer without Redux Store or View.

kGJucfa76sLOk319vhcy6FzaAfZJPri80yvx
EFjQMINJpaW8G6YpBG4m10FfY-PzeDXj0sW3

9.3 Create Reducer for others

If you create the rest of the test cases by referring to the above method, you finally define the teslaRangeApp.js file in which the reducers of all the apps are defined and the teslaRangeApp.spec.js to test them.

The final code can be found at:

After completing the code and testing, a total of seven test cases must succeed.

VD8dC3EQTrnKQVANDVshPxo3pB8NE75iIu4h

10. The views: smart and dumb components

As mentioned in 5. Divide The App Into in Containers and Components, we will create Presentational components (dumb components) for presentation purposes and a Container components (smart components) which are wrapper component responsible for Actions while communicating with Redux.

Smart components are responsible for the actions. If a dumb component underneath them needs to trigger an action, the smart component will pass a function through props, and the dumb component can then treat that as a callback.

We already have dumb components for presentation purposes in part 1, and will reuse them.

Here we create container components as upper wrapper around each dumb components.

10.1 The view layer binding

Redux needs some help to connect the store to the view. It needs something to bind the two together. This is called the view layer binding. In an app that uses react, this is react-redux.

Technically, a container component is just a React component that uses store.subscribe() to read a part of the Redux state tree and supply props to a presentational component it renders.

Hence, we can manually create container components, but this is not recommended for Redux official docs. This is because react-redux performs many performance optimizations that are difficult to perform manually.

For this reason, instead of writing the container component by hand, we write it using the connect() function provided by react-redux.

Let’s install the necessary packages first.

  • npm install –save redux
  • npm install –save react-redux

10.2 TeslarCar Container

To use connect(), you need to define a special function called mapStateToProps. This function tells you how to convert the current Redux store state to props to be passed to the presentation component.

The TeslarCar container takes the wheelsize stored in the current store and passes it to props so that it can be rendered by the TeslarCar component. This props will be updated every time the state is updated.

EVp01AYHe6EToIWZTXAqOb9zaglovdBBsuKp

After defining the mapStateToProps function, we defined the connect() function as shown below.

const TeslaCarContainer = connect(mapStateToProps, null)(TeslaCar)

connect() accepts mapDispatchToProps as the second argument, which takes the dispatch method of the store as its first argument. In the TeslaCar component, we do not need an action, so we have to pass null.

Another parenthesis in connect()() may look weird. This form actually means two function calls, the first connect() returns another function, and the second function needs you to pass a React component. In this case it’s our TeslaCar component. This pattern is called currying or partial application and is a form of functional programming.

Create src/containers/TeslaCarContainer.js and write the code.

Check out TeslaCarContainer.js

10.3 TeslaStats Container

As with the TeslaCar container, define only the mapStatToProps function and pass it to connect() in TeslaStats container.

zM276HEMnRULWWykYN35qPjWAbB43dlonzD5

Create src/containers/TeslaStatsContainer.js and write the code.

Check out TeslaStatsContainer.js

10.4 TeslaSpeedCounter Container

The TeslaSpeedCounter container defines an additional mapDispatchToProps function to handle the user actions that occur in the TeslaSpeedCounter component.

ODhyjWA2nlopJscvPzZHwTemDJWMM4eJ24Lu

Create src/containers/TeslaSpeedCounterContainer.js and write the code.

Check out TeslaSpeedCounterContainer.js

10.5 TeslaTempCounter Container

The TeslaTempCounter container is almost identical to the TeslaSpeedCounter except for the state and action creators being passed.

FmQpTPZJMxHyyNLYRyXXqWv9CAU0v5kWxlaT

Create src/containers/TeslaTempCounterContainer.js and write the code.

Check out TeslaTempCounterContainer.js

10.6 TeslaClimateContainer

5t-ueAA8-AS6Q1F9r2qQiMjrlJ7WwVB8mLRD

Create src/containers/TeslaClimateContainer.js and write the code.

Check out TeslaClimateContainer

10.7 TeslaWheelsContainer

T-n-gUS0W7XxZ0ge0DT29ZD8wiES6rGWRHnE

Create src/containers/TeslaWheelsContainer.js and write the code.

Check out TeslaWheelsContainer.js

We have created the container components corresponding to the presentation components generated in part 1 through connect() of react-redux.

11. Provider

Let’s put together all the things we’ve done so far and make our apps work. So far we have defined action objects and created action creators that create action objects. And when an action occurs, we have created reducers that actually treat and return a new state. We then created a container component that connects each of the presentation components to the Redux store.

Now every container component needs a way to access the store, which is what the Provider does. The Provider component wraps the entire application and allows subcomponents to communicate with the store via connect().

The top-level component of our app, App.js, looks like this:

Check out App.js
ca1XBqUditLLYVAU33ud9lXLpBcK4std6WNz
shield of Provider

12. How they all work together

Finally, all the puzzle pieces were completed. Now let’s look at the following animation as an example of when all the puzzle pieces are tied together and the user triggers the speed up event.

hgaIFhmOJXpOGY2bIpvh2fhpJ-V7RKLol2Qg

Now run npm start and it will be compiled normally and the application will be started.

But there are still a few things to do.

  • First, copy all the contents of /containers/TeslaBattery.css that you created in part 1 and add them to App.css.
Check out App.css
  • Next, open /components/TeslaCounter/TeslaCounter.js and modify the onClick event handler as follows: This is because part 2 no longer handles event handling in TeslaBattery.js.
onClick={(e) => props.increment(e, props.initValues.title)}-->onClick={(e) => {  e.preventDefault();  props.increment(props.currentValue)}}
onClick={(e) => props.decrement(e, props.initValues.title)}-->onClick={(e) => {  e.preventDefault();  props.decrement(props.currentValue)}}
  • Next, let’s not use props repeatedly by using ES6 Object destructuring.
const TeslaCounter = (props) => (  <p className="tesla-counter__title">{props.initValues.title}&lt;/p>  ...
-->const TeslaCounter = ({ initValues, currentValue, increment, decrement }) => (  <p className="tesla-counter__title">{initValues.title}</p>  ...
Check out TeslaCounter.js

Finally, our Redux version of Tesla Battery Range Calculator app is complete!

13. One more thing: Redux Dev Tools

The Redux Dev Tool makes it much easier to view Redux state tracking and take advantage of cool features like time travel debugging.

We’ll install it on Chrome here.

  • Chrome extension install
  • Add for Redux store

Open the App.js file and modify the createStore part as follows:

const store = createStore(appReducer);-->const store = createStore(appReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
  • Check on browser
rLCepIN4D6N0hkL1RprcWPGOjWbUfyTcUjk5

Before you go on to the next part:

Additional resources: