Hooks by Example: Convert a Tesla Battery Range Calculator to Functional Components
In this article I change the TeslaBattery Class Component to a Functional Component with React hooks.
Join the DZone community and get the full member experience.
Join For FreeIn this tutorial, we started with an existing React-app with which we can calculate how much range the Tesla has under different circumstances. The range of a battery depends on the speed, the outside temperature, the climate and the wheel size.
In order to learn React hooks more thoroughly I converted this existing React-app from React Class Components to one with just React Functional Components. That is the goal of this tutorial!
As a starting point for the tutorial, clone this Github repository.
Then, move to the reactjs-app directory:
cd workshop-reactjs-vuejs/reactjs-app
Project Structure
The project we are working on has the same structure as the aforementioned image and shows the components that make up this application. The index.js is the entry point of the application. App.js is the entry component of the application. The outer edge of the figure above shows the App.js component.
Breaking Down the UI
Almost all React applications consist of a composition of components. This application consists of an entry App.js component with the TeslaBattery as a child component. And the TeslaBattery component contains the following child components:
TeslaCar: for rendering the TeslaCar image with wheel animation.
TeslaStats: for rendering the maximum battery range per Tesla model. This concerns the models: 60, 60D, 75, 75D, 90D, and P100D.
TeslaCounter: for manually controlling the speed and the outside temperature.
TeslaClimate: this changes the heating to air conditioning when the outside temperature is more than 20 degrees.
TeslaWheels: for manually adjusting the wheel size from 19 inches to 20 inches and vice versa.
These child components of the TeslaBattery are already React Functional Components.
But the TeslaBattery is still a React Class Component. In this tutorial, we are going to convert this component to a React Functional Component with hooks in 8 steps.
But What Is a Hook?
Hooks are functions that let you “hook into” React state and lifecycle features from functional components. Hooks don’t work inside classes. So our first step is to change the class to a function with hooks.
The components tree of the application is as follows:
Steps to Convert the TeslaBattery Component to a Functional Component
First, open the source file TeslaBattery.js in your IDE and follow these steps:
1. Change the Class to a Function
Change
xxxxxxxxxx
class TeslaBattery extends React.Component {
To
xxxxxxxxxx
function TeslaBattery (props) {
2. Remove the Render Method
Remove the render method, but keep everything after, including the return. Make this the last statement in your function.
From
xxxxxxxxxx
render() {
const { config, carstats } = this.state;
return (
<form className="tesla-battery">
<h1>Range Per Charge</h1>
<TeslaCar wheelsize={config.wheels} ></TeslaCar>
<TeslaStats carstats={carstats} ></TeslaStats>
<div className="tesla-controls cf">
<TeslaCounter
currentValue={this.state.config.speed}
initValues={this.props.counterDefaultVal.speed}
increment={this.increment}
decrement={this.decrement}
></TeslaCounter>
<div className="tesla-climate-container cf">
<TeslaCounter
currentValue={this.state.config.temperature}
initValues={this.props.counterDefaultVal.temperature}
increment={this.increment}
decrement={this.decrement}
></TeslaCounter>
<TeslaClimate
value={this.state.config.climate}
limit={this.state.config.temperature > 10}
handleChangeClimate={this.handleChangeClimate}
/>
</div>
<TeslaWheels
value={this.state.config.wheels}
handleChangeWheels={this.handleChangeWheels}
></TeslaWheels>
</div>
<TeslaNotice ></TeslaNotice>
</form>
)
}
To
xxxxxxxxxx
return (
<form className="tesla-battery">
<h1>Range Per Charge</h1>
<TeslaCar wheelsize={config.wheels} ></TeslaCar>
<TeslaStats carstats={carstats} ></TeslaStats>
<div className="tesla-controls cf">
<TeslaCounter
currentValue={config.speed}
initValues={props.counterDefaultVal.speed}
increment={increment}
decrement={decrement}
></TeslaCounter>
<div className="tesla-climate-container cf">
<TeslaCounter
currentValue={config.temperature}
initValues={props.counterDefaultVal.temperature}
increment={increment}
decrement={decrement}
></TeslaCounter>
<TeslaClimate
value={config.climate}
limit={config.temperature > 10}
handleChangeClimate={handleChangeClimate}
/>
</div>
<TeslaWheels
value={config.wheels}
handleChangeWheels={handleChangeWheels}
></TeslaWheels>
</div>
<TeslaNotice ></TeslaNotice>
</form>
You see that we also removed this.state in the first line. This is because we handle state in a different way (see step 5).
xxxxxxxxxx
// const { config, carstats } = this.state;
3. Convert All Methods to Functions
Class methods won't work inside a function, so let's convert them all to functions (closures).
From
xxxxxxxxxx
calculateStats = (models, value) => {
//..
}
To
xxxxxxxxxx
const calculateStats = (models, value) => {
//..
}
Note: Do this also for all other methods!
4. Remove this.state Throughout the Component
The this.state variable in your function isn't useful anymore. Remove the references to it throughout your render and functions. In step 6, we use the useState hook for this.
5. Remove References to this
The this variable in your function isn't useful any more. Remove the references to it throughout your render and functions.
6. Remove the Constructor
Simply removing the constructor is a little tricky, so I'l break it down further.
1. Remove Event Handler Bindings
We don't need to bind event handlers any more with functional components. So if you were doing this;
xxxxxxxxxx
constructor(props) {
super(props);
this.calculateStats = this.calculateStats.bind(this);
this.statsUpdate = this.statsUpdate.bind(this);
this.increment = this.increment.bind(this);
this.decrement = this.decrement.bind(this);
this.updateCounterState = this.updateCounterState.bind(this);
this.handleChangeClimate = this.handleChangeClimate.bind(this);
this.handleChangeWheels = this.handleChangeWheels.bind(this);
You can simply remove these lines (What a gross, overly verbose syntax anyway).
2. useState Hook
Instead of
xxxxxxxxxx
constructor(props) {
super(props);
this.state = {
carstats: [],
config: {
speed: 55,
temperature: 20,
climate: true,
wheels: 19
}
}
}
Use the useState hook
xxxxxxxxxx
function TeslaBattery (props) {
const [carstats,setCarstats] = useState([]);
const [config,setConfig] = useState({
speed: 55,
temperature: 20,
climate: true,
wheels: 19
});
What does calling useState do? It declares a “state variable”. Our variables are called carstats
and config
, but we could call it anything else, like 'banana'. This is a way to “preserve” some values between the function calls — useState is a new way to use the exact same capabilities that this.state provides in a class. Normally, variables “disappear” when the function exits but state variables are preserved by React.
What do we pass to useState as an argument? The only argument to the useState()
hook is the initial state. Unlike with classes, the state doesn’t have to be an object. We can keep a number or a string if that’s all we need. In our example, we pass an empty array as initial state for our variable carstats
. And we pass an object with default values as initial state for our variable config
.
What does useState return? It returns a pair of values: the current state and a function that updates it. This is why we write const [carstats, setCarstats] = useState([])
. This is similar to this.state.carstats
and this.setState()
in a class, except you get them in a pair.
So, do not use this.setState()
in Functional Component. this.setState()
works only in Class Components to update the state of variables. See step 7.
Finally import your hook:
xxxxxxxxxx
import React, { useState } from 'react';
7. Replace this.setState
this.setState
obviously doesn't exist any more in our function component. Instead, we need to replace each of our setState calls with the relevant state variable setter.
Replace first the this.setState
for carstats
:
xxxxxxxxxx
class TeslaBattery extends React.Component {
statsUpdate() {
const carModels = ['60', '60D', '75', '75D', '90D', 'P100D'];
// Fetch model info from BatteryService and calculate then update state
this.setState({
carstats: this.calculateStats(carModels, this.state.config)
})
}
With the setCarstats - setter
xxxxxxxxxx
function TeslaBattery (props) {
const [carstats,setCarstats] = useState([]);
const statsUpdate = () => {
const carModels = ['60', '60D', '75', '75D', '90D', 'P100D'];
// Fetch model info from BatteryService and calculate then update state
setCarstats(calculateStats(carModels, config));
}
Attention! Call React hooks always at the Top Level of your function.
AND
Replace the this.setState
for config
:
xxxxxxxxxx
class TeslaBattery extends React.Component {
updateCounterState(title, newValue) {
const config = { this.state.config };
// update config state with new value
title === 'Speed' ? config['speed'] = newValue : config['temperature'] = newValue;
// update our state
this.setState({ config }, () => {this.statsUpdate()});
}
Note: You see in this example how this.setState
accept a callback (() => {this.statsUpdate()
) that would run after the state-object (i.e. the config-object) is updated? Well our useState
updater function does no such thing. Instead, we have to use the useEffect
hook. This is explained in the next section.
Replace this.setState with this setConfig
- setter:
xxxxxxxxxx
function TeslaBattery (props) {
const [config,setConfig] = useState({
speed: 55,
temperature: 20,
climate: true,
wheels: 19
});
const updateCounterState = (title, newValue) => {
const config_ = { config };
// update config state with new value
title === 'Speed' ? config_['speed'] = newValue : config_['temperature'] = newValue;
// update our state
setConfig(config_);
}
Do this also for the following event handlers for setting the state of the config-object:
handleChangeClimate(size)
.handleChangeWheels(size)
.
8. Replace Lifecycle Methods With the useEffect Hooks
First import your useEffect hook
xxxxxxxxxx
import React, { useEffect, useState } from 'react';
Replace ComponentDidMount()
Instead of using the componentDidMount lifecycle method, use the useEffect
hook with the dependency array filled: [config] !
Replace
xxxxxxxxxx
componentDidMount() {
this.statsUpdate();
}
With
xxxxxxxxxx
useEffect(()=>{
statsUpdate();
// eslint-disable-next-line
},[config])
The Effect Hook lets you perform side effects in Functional Components. The side effect here is executing the statsUpdate()
function.
The dependency array [config] given in parameters lets you fire the side effect only when the config-object in the array is changed. This way, you can easily create lifecycle methods like componentDidMount
and componentDidUpdate
via the Effect hook.
What does useEffect
do? By using this Hook, you tell React that your component needs to do something after render. React will remember the function you passed (we’ll refer to it as our “effect”), and call it later after performing the DOM updates. In this effect, we call the statsUpdate()
function, but we could also perform data fetching or call some other imperative API.
Does useEffect
run after every render? Yes! By default, it runs both after the first render and after every update. Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.
Motivation of Using Hooks
With Hooks, you can extract stateful logic from a component so it can be tested independently and reused. Hooks allow you to reuse stateful logic without changing your component hierarchy. This makes it easy to share Hooks among many components or with the community.
And Hooks let you split one component into smaller functions based on what pieces are related (such as setting up a subscription or fetching data), rather than forcing a split based on lifecycle methods.
How to reuse your hooks between Components?
1. Create a separate file with a proper name for example: useStatesHooks.js. This file contain the useState hooks which you want to share between components.
xxxxxxxxxx
const useStatesHooks = () => {
const [carstats,setCarstats] = useState([]);
const [config,setConfig] = useState({
speed: 55,
temperature: 20,
climate: true,
wheels: 19
});
return { carstats, config };
}
export default useStatesHooks;
2. import this useStatesHooks into your TeslaBattery.js functional component
xxxxxxxxxx
import useStatesHooks from './useStatesHooks';
function TeslaBattery (props) {
const { carstats, config } = useStatesHooks();
.
Conclusion
In order to learn this more thoroughly I converted an existing React-app from class components to one with just functional components using just the hooks in this article. The result was dramatically cleaner and easier to read code.
I can’t see any reason why I would not use hooks exclusively in the future. I look forward to creating custom hooks and using some of the more advanced features that hooks have to offer.
Opinions expressed by DZone contributors are their own.
Comments