1.11 Reusable TeslaCounter Component
Tesla’s speed and external temperature controls should be reusable components, so I’ll make them a generic Counter component that allows for other metadata such as step, minimum, maximum, and title and units (mph / degrees).
Also, unlike the components we have created so far, we need an action to change the state value in response to user input (button click, checkbox selection, etc.). Let’s look at how to handle events that occur in a subcomponent.
Create the src/components/TeslaCounter
directory as before, create a TeslaCounter.js
file in it, and enter the following code:
import React from 'react';import PropTypes from 'react';import './TeslaCounter.css';const TeslaCounter = (props) => (<div className="tesla-counter"><p className="tesla-counter__title">{props.initValues.title}</p><div className="tesla-counter__container cf"><div className="tesla-counter__item"><p className="tesla-counter__number">{ props.currentValue }<span>{ props.initValues.unit }</span></p><div className="tesla-counter__controls"><buttononClick={(e) => props.increment(e, props.initValues.title)}disabled={props.currentValue >= props.initValues.max}></button><buttononClick={(e) => props.decrement(e, props.initValues.title)}disabled={props.currentValue <= props.initValues.min}></button></div></div></div></div>);TeslaCounter.propTypes = {currentValue: PropTypes.number,increment: PropTypes.func,decrement: PropTypes.func,initValues: PropTypes.object}export default TeslaCounter;
Let’s think about what we want here. Each time you click and change the speed and temperature, you must update the state so that the value is reflected between the maximum and minimum values.
Since the component only needs to update its own state, TeslaBattery
passes the callback (increment
, decrement
) to the TeslaCounter
each time it needs to update its state. You can use the onClick
event on a button to notify the event. The callback passed by TeslaBattery
calls setState()
and the app is updated.
We will implement a callback that will be passed by TeslaBattery
in a few moments.
1.11.1 TeslaCounter Component Style
Let’s implement the style first. Create a TeslaCounter.css
file in the src/components/TeslaCounter
directory and specify the following styles. Since the code is long and omitted here, let’s check the source code
.tesla-counter {float: left;width: 230px;}.tesla-counter__title {letter-spacing: 2px;font-size: 16px;}...
1.11.2 Import TeslaCounter Component in TeslaBattery Container
Now, we will implement callback
in TeslaBattery
and pass it to the TeslaCounter
component.
First, add import
to use the TeslaCounter
component in TeslaBattery.js
.
We also implement the callback functions increment()
and decrement()
, and the internal function updateCounterState()
and bind it in the constructor
. Then pass the callback
function to the TeslaCounter
component with props
.
...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.state = {carstats: [],config: {speed: 55,temperature: 20,climate: true,wheels: 19}}}...updateCounterState(title, newValue) {const config = { ...this.state.config };// update config state with new valuetitle === 'Speed' ? config['speed'] = newValue : config['temperature'] = newValue;// update our statethis.setState({ config });}increment(e, title) {e.preventDefault();let currentValue, maxValue, step;const { speed, temperature } = this.props.counterDefaultVal;if (title === 'Speed') {currentValue = this.state.config.speed;maxValue = speed.max;step = speed.step;} else {currentValue = this.state.config.temperature;maxValue = temperature.max;step = temperature.step;}if (currentValue < maxValue) {const newValue = currentValue + step;this.updateCounterState(title, newValue);}}decrement(e, title) {e.preventDefault();let currentValue, minValue, step;const { speed, temperature } = this.props.counterDefaultVal;if (title === 'Speed') {currentValue = this.state.config.speed;minValue = speed.min;step = speed.step;} else {currentValue = this.state.config.temperature;minValue = temperature.min;step = temperature.step;}if (currentValue > minValue) {const newValue = currentValue - step;this.updateCounterState(title, newValue);}}...render() {return (<form className="tesla-battery"><h1>Range Per Charge</h1><TeslaCar wheelsize={config.wheels} /><TeslaStats carstats={carstats} /><div className="tesla-controls cf"><TeslaCountercurrentValue={this.state.config.speed}initValues={this.props.counterDefaultVal.speed}increment={this.increment}decrement={this.decrement}/><div className="tesla-climate-container cf"><TeslaCountercurrentValue={this.state.config.temperature}initValues={this.props.counterDefaultVal.temperature}increment={this.increment}decrement={this.decrement}/></div></div><TeslaNotice /></form>)}
1.11.3 TeslaBattery Container Style
An additional style is required for TeslaBattery
as soon as the TeslaCounter component is added. Open the TeslaBattery.css
file and add the following:
.tesla-climate-container {float: left;width: 420px;padding: 0 40px;margin: 0 40px 0 0;border-left: 1px solid #ccc;border-right: 1px solid #ccc;}.tesla-controls {display: block;width: 100%;}
1.11.4 Default Value Props
Here, initValues
passed to TeslaCounter
is a constant value and is passed from App
which is a parent component of TeslaBattery
.
Open App.js
and pass the counterDefaultVal
object to the TeslaBattery
component as follows:
import React, { Component } from 'react';import './App.css';import Header from './components/Header/Header';import TeslaBattery from './containers/TeslaBattery';const counterDefaultVal = {speed: {title: "Speed",unit: "mph",step: 5,min: 45,max: 70},temperature: {title: "Outside Temperature",unit: "°",step: 10,min: -10,max: 40}};class App extends Component {render() {return (<div className="App"><Header /><TeslaBattery counterDefaultVal={counterDefaultVal}/></div>);}}export default App;
Now, when you click Speed and Temperature, you can see that the changed values are updated and re-rendered in the state object through the React Developer Tool
.
export function getModelData() { return { '60': { 19: { on: { speed: { 45: { '-10': 224, '0': 255, '10': 287, '20': 289, '30': 287, '40': 258 }, 50: { '-10': 211, '0': 238, '10': 264, '20': 267, '30': 267, '40': 244 }, 55: { '-10': 198, '0': 221, '10': 242, '20': 246, '30': 245, '40': 228 }, 60: { '-10': 184, '0': 204, '10': 222, '20': 225, '30': 226, '40': 212 }, 65: { '-10': 170, '0': 187, '10': 202, '20': 206, '30': 208, '40': 195 }, 70: { '-10': 156, '0': 172, '10': 185, '20': 189, '30': 190, '40': 179 }, } }, off: { speed: { 45: { '-10': 297, '0': 312, '10': 318, '20': 325, '30': 329, '40': 333 }, 50: { '-10': 269, '0': 283, '10': 288, '20': 294, '30': 298, '40': 304 }, 55: { '-10': 245, '0': 256, '10': 261, '20': 267, '30': 269, '40': 277 }, 60: { '-10': 221, '0': 231, '10': 236, '20': 242, '30': 243, '40': 252 }, 65: { '-10': 200, '0': 209, '10': 214, '20': 219, '30': 222, '40': 230 }, 70: { '-10': 181, '0': 189, '10': 194, '20': 199, '30': 202, '40': 209 }, } } }, 21: { on: { speed: { 45: { '-10': 220, '0': 251, '10': 283, '20': 285, '30': 282, '40': 284 }, 50: { '-10': 208, '0': 234, '10': 260, '20': 262, '30': 262, '40': 240 }, 55: { '-10': 194, '0': 216, '10': 238, '20': 241, '30': 240, '40': 224 }, 60: { '-10': 180, '0': 199, '10': 217, '20': 220, '30': 221, '40': 208 }, 65: { '-10': 166, '0': 183, '10': 198, '20': 202, '30': 203, '40': 191 }, 70: { '-10': 152, '0': 167, '10': 180, '20': 184, '30': 185, '40': 174 }, } }, off: { speed: { 45: { '-10': 292, '0': 307, '10': 314, '20': 320, '30': 324, '40': 328 }, 50: { '-10': 265, '0': 278, '10': 283, '20': 289, '30': 293, '40': 298 }, 55: { '-10': 240, '0': 251, '10': 256, '20': 261, '30': 264, '40': 272 }, 60: { '-10': 217, '0': 226, '10': 231, '20': 236, '30': 238, '40': 247 }, 65: { '-10': 196, '0': 204, '10': 209, '20': 214, '30': 217, '40': 225 }, 70: { '-10': 177, '0': 184, '10': 189, '20': 194, '30': 197, '40': 204 }, } } } }, '60D': { 19: { on: { speed: { 45: { '-10': 227, '0': 258, '10': 291, '20': 293, '30': 292, '40': 264 }, 50: { '-10': 215, '0': 242, '10': 269, '20': 271, '30': 272, '40': 250 }, 55: { '-10': 201, '0': 224, '10': 247, '20': 250, '30': 251, '40': 235 }, 60: { '-10': 187, '0': 207, '10': 226, '20': 229, '30': 232, '40': 217 }, 65: { '-10': 172, '0': 191, '10': 207, '20': 211, '30': 213, '40': 201 }, 70: { '-10': 158, '0': 175, '10': 189, '20': 193, '30': 195, '40': 185 }, } }, off: { speed: { 45: { '-10': 301, '0': 317, '10': 323, '20': 330, '30': 336, '40': 339 }, 50: { '-10': 275, '0': 288, '10': 294, '20': 301, '30': 306, '40': 311 }, 55: { '-10': 249, '0': 260, '10': 266, '20': 273, '30': 278, '40': 283 }, 60: { '-10': 225, '0': 236, '10': 241, '20': 248, '30': 251, '40': 258 }, 65: { '-10': 204, '0': 214, '10': 219, '20': 225, '30': 229, '40': 236 }, 70: { '-10': 184, '0': 193, '10': 198, '20': 205, '30': 207, '40': 215 }, } } }, 21: { on: { speed: { 45: { '-10': 223, '0': 255, '10': 287, '20': 289, '30': 288, '40': 260 }, 50: { '-10': 211, '0': 238, '10': 264, '20': 267, '30': 267, '40': 246 }, 55: { '-10': 197, '0': 220, '10': 242, '20': 246, '30': 246, '40': 230 }, 60: { '-10': 183, '0': 203, '10': 221, '20': 225, '30': 227, '40': 212 }, 65: { '-10': 168, '0': 186, '10': 202, '20': 206, '30': 208, '40': 196 }, 70: { '-10': 155, '0': 171, '10': 184, '20': 188, '30': 190, '40': 181 }, } }, off: { speed: { 45: { '-10': 297, '0': 312, '10': 319, '20': 326, '30': 331, '40': 335 }, 50: { '-10': 270, '0': 283, '10': 289, '20': 296, '30': 301, '40': 306 }, 55: { '-10': 244, '0': 256, '10': 261, '20': 268, '30': 272, '40': 278 }, 60: { '-10': 221, '0': 231, '10': 236, '20': 242, '30': 246, '40': 253 }, 65: { '-10': 199, '0': 209, '10': 214, '20': 220, '30': 223, '40': 231 }, 70: { '-10': 180, '0': 188, '10': 193, '20': 200, '30': 202, '40': 210 }, } } } }, '75': { 19: { on: { speed: { 45: { '-10': 271, '0': 309, '10': 347, '20': 350, '30': 347, '40': 312 }, 50: { '-10': 256, '0': 288, '10': 320, '20': 323, '30': 323, '40': 295 }, 55: { '-10': 240, '0': 267, '10': 293, '20': 297, '30': 297, '40': 276 }, 60: { '-10': 222, '0': 246, '10': 268, '20': 272, '30': 273, '40': 257 }, 65: { '-10': 205, '0': 227, '10': 245, '20': 250, '30': 252, '40': 236 }, 70: { '-10': 189, '0': 206, '10': 224, '20': 228, '30': 230, '40': 216 }, } }, off: { speed: { 45: { '-10': 359, '0': 377, '10': 385, '20': 393, '30': 398, '40': 403 }, 50: { '-10': 326, '0': 342, '10': 349, '20': 356, '30': 360, '40': 368 }, 55: { '-10': 296, '0': 309, '10': 316, '20': 323, '30': 326, '40': 335 }, 60: { '-10': 268, '0': 280, '10': 286, '20': 292, '30': 295, '40': 305 }, 65: { '-10': 242, '0': 253, '10': 259, '20': 265, '30': 268, '40': 278 }, 70: { '-10': 219, '0': 229, '10': 234, '20': 241, '30': 244, '40': 253 }, } } }, 21: { on: { speed: { 45: { '-10': 267, '0': 304, '10': 342, '20': 344, '30': 342, '40': 308 }, 50: { '-10': 251, '0': 283, '10': 314, '20': 317, '30': 317, '40': 290 }, 55: { '-10': 235, '0': 262, '10': 287, '20': 291, '30': 291, '40': 271 }, 60: { '-10': 218, '0': 241, '10': 262, '20': 266, '30': 267, '40': 251 }, 65: { '-10': 201, '0': 222, '10': 239, '20': 244, '30': 246, '40': 231 }, 70: { '-10': 184, '0': 203, '10': 218, '20': 223, '30': 224, '40': 211 }, } }, off: { speed: { 45: { '-10': 353, '0': 372, '10': 279, '20': 387, '30': 392, '40': 397 }, 50: { '-10': 320, '0': 336, '10': 343, '20': 350, '30': 354, '40': 361 }, 55: { '-10': 290, '0': 303, '10': 309, '20': 316, '30': 319, '40': 329 }, 60: { '-10': 262, '0': 274, '10': 279, '20': 286, '30': 288, '40': 299 }, 65: { '-10': 237, '0': 247, '10': 253, '20': 259, '30': 262, '40': 272 }, 70: { '-10': 214, '0': 223, '10': 229, '20': 235, '30': 238, '40': 247 }, } } } }, '75D': { 19: { on: { speed: { 45: { '-10': 227, '0': 316, '10': 356, '20': 358, '30': 357, '40': 323 }, 50: { '-10': 262, '0': 296, '10': 328, '20': 332, '30': 332, '40': 305 }, 55: { '-10': 246, '0': 274, '10': 302, '20': 306, '30': 307, '40': 287 }, 60: { '-10': 228, '0': 253, '10': 276, '20': 280, '30': 283, '40': 265 }, 65: { '-10': 211, '0': 233, '10': 253, '20': 257, '30': 260, '40': 246 }, 70: { '-10': 194, '0': 214, '10': 231, '20': 236, '30': 238, '40': 226 }, } }, off: { speed: { 45: { '-10': 368, '0': 387, '10': 395, '20': 404, '30': 410, '40': 415 }, 50: { '-10': 335, '0': 351, '10': 359, '20': 367, '30': 374, '40': 380 }, 55: { '-10': 304, '0': 318, '10': 325, '20': 334, '30': 339, '40': 346 }, 60: { '-10': 275, '0': 288, '10': 294, '20': 303, '30': 307, '40': 316 }, 65: { '-10': 249, '0': 261, '10': 267, '20': 275, '30': 279, '40': 289 }, 70: { '-10': 225, '0': 236, '10': 242, '20': 250, '30': 253, '40': 263 }, } } }, 21: { on: { speed: { 45: { '-10': 273, '0': 311, '10': 351, '20': 354, '30': 352, '40': 318 }, 50: { '-10': 258, '0': 291, '10': 323, '20': 326, '30': 327, '40': 300 }, 55: { '-10': 241, '0': 269, '10': 296, '20': 300, '30': 301, '40': 281 }, 60: { '-10': 223, '0': 248, '10': 270, '20': 275, '30': 277, '40': 259 }, 65: { '-10': 206, '0': 228, '10': 247, '20': 252, '30': 254, '40': 240 }, 70: { '-10': 189, '0': 209, '10': 225, '20': 230, '30': 232, '40': 221 }, } }, off: { speed: { 45: { '-10': 363, '0': 382, '10': 390, '20': 398, '30': 405, '40': 409 }, 50: { '-10': 330, '0': 346, '10': 353, '20': 361, '30': 368, '40': 373 }, 55: { '-10': 299, '0': 312, '10': 319, '20': 327, '30': 333, '40': 340 }, 60: { '-10': 270, '0': 282, '10': 288, '20': 296, '30': 301, '40': 309 }, 65: { '-10': 244, '0': 255, '10': 261, '20': 269, '30': 273, '40': 282 }, 70: { '-10': 219, '0': 230, '10': 236, '20': 244, '30': 247, '40': 257 }, } } } }, '90D': { 19: { on: { speed: { 45: { '-10': 308, '0': 349, '10': 392, '20': 394, '30': 392, '40': 357 }, 50: { '-10': 292, '0': 326, '10': 362, '20': 365, '30': 365, '40': 338 }, 55: { '-10': 273, '0': 303, '10': 332, '20': 336, '30': 337, '40': 317 }, 60: { '-10': 254, '0': 280, '10': 305, '20': 308, '30': 310, '40': 293 }, 65: { '-10': 235, '0': 258, '10': 279, '20': 283, '30': 285, '40': 273 }, 70: { '-10': 216, '0': 238, '10': 256, '20': 260, '30': 263, '40': 253 }, } }, off: { speed: { 45: { '-10': 406, '0': 426, '10': 434, '20': 443, '30': 451, '40': 455 }, 50: { '-10': 370, '0': 386, '10': 394, '20': 403, '30': 412, '40': 416 }, 55: { '-10': 336, '0': 350, '10': 358, '20': 366, '30': 274, '40': 380 }, 60: { '-10': 304, '0': 317, '10': 324, '20': 332, '30': 338, '40': 347 }, 65: { '-10': 276, '0': 288, '10': 295, '20': 302, '30': 308, '40': 317 }, 70: { '-10': 250, '0': 261, '10': 268, '20': 275, '30': 279, '40': 290 }, } } }, 21: { on: { speed: { 45: { '-10': 304, '0': 345, '10': 386, '20': 388, '30': 387, '40': 352 }, 50: { '-10': 287, '0': 321, '10': 356, '20': 359, '30': 360, '40': 332 }, 55: { '-10': 268, '0': 297, '10': 326, '20': 330, '30': 330, '40': 311 }, 60: { '-10': 249, '0': 274, '10': 299, '20': 302, '30': 303, '40': 287 }, 65: { '-10': 230, '0': 253, '10': 273, '20': 277, '30': 279, '40': 267 }, 70: { '-10': 211, '0': 232, '10': 250, '20': 254, '30': 257, '40': 247 }, } }, off: { speed: { 45: { '-10': 401, '0': 420, '10': 428, '20': 437, '30': 446, '40': 449 }, 50: { '-10': 364, '0': 380, '10': 388, '20': 397, '30': 405, '40': 410 }, 55: { '-10': 330, '0': 344, '10': 351, '20': 359, '30': 367, '40': 373 }, 60: { '-10': 298, '0': 311, '10': 318, '20': 325, '30': 331, '40': 340 }, 65: { '-10': 270, '0': 282, '10': 288, '20': 296, '30': 301, '40': 310 }, 70: { '-10': 244, '0': 255, '10': 262, '20': 269, '30': 273, '40': 284 }, } } } }, 'P100D': { 19: { on: { speed: { 45: { '-10': 341, '0': 390, '10': 439, '20': 442, '30': 440, '40': 401 }, 50: { '-10': 323, '0': 365, '10': 405, '20': 409, '30': 410, '40': 380 }, 55: { '-10': 303, '0': 339, '10': 372, '20': 376, '30': 379, '40': 353 }, 60: { '-10': 282, '0': 313, '10': 341, '20': 345, '30': 347, '40': 329 }, 65: { '-10': 261, '0': 289, '10': 312, '20': 317, '30': 318, '40': 306 }, 70: { '-10': 240, '0': 265, '10': 285, '20': 290, '30': 293, '40': 283 }, } }, off: { speed: { 45: { '-10': 447, '0': 474, '10': 485, '20': 496, '30': 505, '40': 509 }, 50: { '-10': 408, '0': 431, '10': 441, '20': 451, '30': 461, '40': 466 }, 55: { '-10': 372, '0': 391, '10': 400, '20': 409, '30': 419, '40': 425 }, 60: { '-10': 337, '0': 354, '10': 362, '20': 371, '30': 377, '40': 388 }, 65: { '-10': 306, '0': 321, '10': 329, '20': 337, '30': 341, '40': 354 }, 70: { '-10': 277, '0': 291, '10': 299, '20': 307, '30': 311, '40': 323 }, } } }, 21: { on: { speed: { 45: { '-10': 322, '0': 369, '10': 414, '20': 417, '30': 416, '40': 379 }, 50: { '-10': 306, '0': 347, '10': 384, '20': 388, '30': 389, '40': 360 }, 55: { '-10': 228, '0': 323, '10': 354, '20': 358, '30': 360, '40': 336 }, 60: { '-10': 269, '0': 299, '10': 325, '20': 329, '30': 331, '40': 313 }, 65: { '-10': 250, '0': 276, '10': 299, '20': 303, '30': 305, '40': 292 }, 70: { '-10': 230, '0': 254, '10': 273, '20': 278, '30': 281, '40': 271 }, } }, off: { speed: { 45: { '-10': 422, '0': 447, '10': 458, '20': 468, '30': 477, '40': 481 }, 50: { '-10': 387, '0': 409, '10': 418, '20': 428, '30': 437, '40': 442 }, 55: { '-10': 353, '0': 372, '10': 380, '20': 389, '30': 398, '40': 404 }, 60: { '-10': 322, '0': 338, '10': 345, '20': 354, '30': 359, '40': 370 }, 65: { '-10': 293, '0': 307, '10': 315, '20': 323, '30': 326, '40': 339 }, 70: { '-10': 265, '0': 279, '10': 286, '20': 294, '30': 298, '40': 310 }, } } } } } }
1.11.5 Virtual DOM
What a single-page application can give us is a seamless user experience and smooth interaction.
In our app, car model values are updated without having to reload the entire page every time the user changes speed or temperature. Even if you need to connect to the server to get the data. To provide this user experience, you need to know which part of the DOM
you need to update when changes or interactions occur.
Each JavaScript framework uses a different strategy: Ember
uses data-binding
, Angular1
uses dirty checking, and React
uses Virtual DOM.
In React, the first time the component’s rendering method is called, it prints a virtual DOM
model, rather than the actual DOM
element itself. The virtual DOM
is a JavaScript data structure that represents the appearance of DOM
. React then takes this model and creates the actual DOM
element.
Then, whenever the component’s state changes (eg, setState
is called), the rendering method of the component is called and a new virtual DOM
is created, and this new virtual DOM
is replaced with the previous virtual DOM
. The result of this comparison is to show the actual DOM
changes and the DOM
will be ‘patched’ with the changes and the screen will change.
The car model information does not change yet as the speed and temperature change. This will eventually be implemented later.