Building Tesla‘s Battery Range Calculator with React and Redux

In the previous article, we built a Tesla battery range calculator using React. We covered how to break down the application into modular components, manage state within those components, and handle user interactions. While this worked well for our relatively simple app, relying solely on React‘s component state can become cumbersome and hard to maintain as applications grow in size and complexity.

This is where Redux comes in. Redux is a predictable state container for JavaScript applications that helps manage state in a centralized store, making it easier to reason about and debug. In this article, we‘ll refactor our Tesla battery range calculator to use Redux for state management, exploring the core concepts, benefits, and patterns along the way.

What is Redux?

At its core, Redux is a state management library that follows three fundamental principles:

  1. Single source of truth: The entire state of your application is stored in a single JavaScript object called the "store". This makes it easy to understand what data is available and how it changes over time.

  2. State is read-only: The only way to change the state is by dispatching "actions", which are plain JavaScript objects describing what happened. This ensures that state changes are explicit and predictable.

  3. Changes are made with pure functions: To specify how the state tree is transformed by actions, you write pure "reducer" functions that take the previous state and an action, and return the next state.

By following these principles, Redux helps you write applications that are easy to test, debug, and reason about. It also enables powerful features like time-travel debugging and state persistence.

Redux Core Concepts

To understand how Redux works, let‘s dive into its core concepts:

Actions

Actions are plain JavaScript objects that represent an intention to change the state. They must have a `type` property indicating the type of action being performed. For example:

{ 
  type: ‘CHANGE_TEMPERATURE‘,
  payload: 30
}

Action Creators

Action creators are functions that create and return actions. They encapsulate the process of constructing an action object. For example:

function changeTemperature(temperature) {
  return {
    type: ‘CHANGE_TEMPERATURE‘,
    payload: temperature
  };
}

Reducers

Reducers are pure functions that specify how the state changes in response to actions. They take the current state and an action, and return the next state. For example:

function temperature(state = 20, action) {
  switch (action.type) {
    case ‘CHANGE_TEMPERATURE‘:
      return action.payload;
    default:
      return state;
  }
}

Store

The store is the object that holds the application state and provides methods to access and update it. It is created by passing the root reducer to the `createStore` function from Redux. For example:

import { createStore } from ‘redux‘;
import rootReducer from ‘./reducers‘;

const store = createStore(rootReducer);

Building the Redux Tesla Battery Range Calculator

Now that we understand the core concepts of Redux, let‘s refactor our Tesla battery range calculator to use it for state management.

Defining the State Shape

First, we need to define the shape of our application state. For the Tesla battery range calculator, our state will include:

  • The car model
  • The battery size
  • The speed
  • The outside temperature
  • The wheel size
  • Climate control setting

We can represent this in a JavaScript object like so:

{
  config: {
    model: ‘Model 3 Long Range‘,
    batterySize: 75,
    speed: 55,
    temperature: 20,
    wheelSize: 19,
    climateControl: true  
  },
  carstats: {
    // Derived car statistics  
  }
}

Defining Actions and Action Creators

Next, let‘s define the actions our app will use to modify the state. We‘ll need actions for changing the car model, battery size, speed, temperature, wheel size, and toggling climate control.

Here are the action types as constants:

export const CHANGE_MODEL = ‘CHANGE_MODEL‘;
export const CHANGE_BATTERY_SIZE = ‘CHANGE_BATTERY_SIZE‘; 
export const CHANGE_SPEED = ‘CHANGE_SPEED‘;
export const CHANGE_TEMPERATURE = ‘CHANGE_TEMPERATURE‘;
export const CHANGE_WHEEL_SIZE = ‘CHANGE_WHEEL_SIZE‘;
export const TOGGLE_CLIMATE_CONTROL = ‘TOGGLE_CLIMATE_CONTROL‘;

And here are the corresponding action creator functions:

export function changeModel(model) {
  return { type: CHANGE_MODEL, payload: model };
}

export function changeBatterySize(batterySize) {
  return { type: CHANGE_BATTERY_SIZE, payload: batterySize };  
}

export function changeSpeed(speed) {
  return { type: CHANGE_SPEED, payload: speed };
}

export function changeTemperature(temperature) {
  return { type: CHANGE_TEMPERATURE, payload: temperature };  
}

export function changeWheelSize(wheelSize) {
  return { type: CHANGE_WHEEL_SIZE, payload: wheelSize };
}

export function toggleClimateControl() {
  return { type: TOGGLE_CLIMATE_CONTROL };  
}

Writing Reducers

Now we need to write reducer functions to handle these actions and return the updated state.

Here‘s the reducer for the car configuration:

const initialState = {
  model: ‘Model 3 Long Range‘,
  batterySize: 75,
  speed: 55,
  temperature: 20,
  wheelSize: 19,
  climateControl: true
};

export default function config(state = initialState, action) {
  switch (action.type) {
    case CHANGE_MODEL:
      return { ...state, model: action.payload };
    case CHANGE_BATTERY_SIZE:
      return { ...state, batterySize: action.payload }; 
    case CHANGE_SPEED:
      return { ...state, speed: action.payload };
    case CHANGE_TEMPERATURE:
      return { ...state, temperature: action.payload };
    case CHANGE_WHEEL_SIZE:  
      return { ...state, wheelSize: action.payload };
    case TOGGLE_CLIMATE_CONTROL:
      return { ...state, climateControl: !state.climateControl };
    default:
      return state;
  }
}

Notice how we use the switch statement to handle different action types, and return a new state object each time using the spread operator (...) to copy the existing state and overwrite the changed properties. This approach keeps our reducer functions pure and avoids mutating the state directly.

We‘ll also need a reducer to calculate the derived car statistics based on the current configuration:

export default function carstats(state = {}, action) {
  switch (action.type) {
    case CHANGE_MODEL:
    case CHANGE_BATTERY_SIZE:
    case CHANGE_SPEED: 
    case CHANGE_TEMPERATURE:
    case CHANGE_WHEEL_SIZE:
    case TOGGLE_CLIMATE_CONTROL:
      return calculateCarStats(action.config);
    default:  
      return state;
  }
}

function calculateCarStats(config) {
  // Perform calculations based on config
  // Return updated carstats object
}

This reducer listens for any changes to the car configuration, and recalculates the car stats accordingly.

Configuring the Store

With our reducers in place, we can now create the Redux store. We‘ll use the `combineReducers` function to combine our `config` and `carstats` reducers into a single root reducer:

import { createStore, combineReducers } from ‘redux‘;
import config from ‘./reducers/config‘;
import carstats from ‘./reducers/carstats‘;

const rootReducer = combineReducers({
  config,
  carstats
});

const store = createStore(rootReducer);

export default store;

Integrating Redux with React Components

Finally, we need to connect our React components to the Redux store so they can dispatch actions and read state.

We‘ll use the react-redux library to access Redux functionality in our components. The connect function from react-redux allows us to map state and action creators to component props.

Here‘s an example of connecting the TeslaConfig component:

import React from ‘react‘;
import { connect } from ‘react-redux‘;
import { changeModel, changeBatterySize, changeSpeed, changeTemperature, changeWheelSize, toggleClimateControl } from ‘./actions‘;

function TeslaConfig(props) {
  // Render UI using props mapped from Redux  
}

function mapStateToProps(state) {
  return {
    model: state.config.model,
    batterySize: state.config.batterySize,
    speed: state.config.speed,
    temperature: state.config.temperature,
    wheelSize: state.config.wheelSize,
    climateControl: state.config.climateControl
  };
}

const mapDispatchToProps = {
  changeModel, 
  changeBatterySize,
  changeSpeed,
  changeTemperature,
  changeWheelSize,
  toggleClimateControl
};

export default connect(mapStateToProps, mapDispatchToProps)(TeslaConfig);

The mapStateToProps function maps the relevant parts of the Redux state to the component‘s props, while the mapDispatchToProps object provides the action creators as props that the component can call to dispatch actions.

We would follow a similar approach to connect the TeslaStats component to display the derived car statistics from the Redux state.

Handling Asynchronous Actions

In a real-world scenario, the Tesla battery range calculator would likely fetch vehicle data from an API. This introduces asynchronous behavior that plain Redux actions can‘t handle out of the box.

To manage asynchronous actions, we can use Redux middleware like redux-thunk. Thunks allow us to write action creators that return functions instead of objects. These functions can perform side effects like making API requests and dispatch regular actions when they complete.

Here‘s an example thunk action creator for fetching car data:

export function fetchCarData() {
  return (dispatch) => {
    dispatch({ type: ‘FETCH_CAR_DATA_REQUEST‘ });

    return fetch(‘https://api.example.com/cars‘)
      .then(response => response.json())
      .then(data => {
        dispatch({ 
          type: ‘FETCH_CAR_DATA_SUCCESS‘,
          payload: data  
        });
      })
      .catch(error => {
        dispatch({
          type: ‘FETCH_CAR_DATA_FAILURE‘, 
          error  
        });
      });
  };
}

To enable thunks, we apply the redux-thunk middleware when creating our store:

import { createStore, applyMiddleware } from ‘redux‘;
import thunk from ‘redux-thunk‘;
import rootReducer from ‘./reducers‘;

const store = createStore(rootReducer, applyMiddleware(thunk));

With thunks, we can handle asynchronous data fetching and update our application state when the requests complete, keeping our components in sync with the latest data.

Redux DevTools

One of the great benefits of using Redux is the ability to leverage powerful debugging tools. Redux DevTools is a browser extension that lets you inspect your application‘s state, travel back and forth between state changes, and even modify the state on the fly.

To use Redux DevTools, first install the browser extension. Then, update your store setup to include the DevTools enhancer:

const store = createStore(
  rootReducer, 
  applyMiddleware(thunk), 
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

With the DevTools enabled, you can open them in your browser to see a live view of your application state, a history of dispatched actions, and powerful time-travel debugging capabilities.

Conclusion

In this article, we learned how to refactor a Tesla battery range calculator built with React to use Redux for state management. We covered the core concepts of Redux, including actions, reducers, and the store, and saw how they help create a predictable state container for our application.

We also explored more advanced topics like handling asynchronous actions with Redux Thunk and leveraging Redux DevTools for powerful debugging and visualization.

By moving complex state management logic out of individual React components and into Redux, we can create more maintainable, scalable applications that are easier to reason about and debug. While Redux introduces some additional complexity and boilerplate, the benefits it provides in terms of state predictability, tooling, and developer experience are significant.

To dive deeper into Redux and level up your state management skills, check out the official Redux documentation, the "Building React Applications with Idiomatic Redux" course by Dan Abramov, and experiment with building your own Redux applications. Happy coding!

Similar Posts