Course: Section

Get Started with Modern React: Step by Step

Episode: Title

S02・V20: Lifting State Up (Part 2)

Date Created: July 17th, 2019
Last Updated: 27 days ago

Objective
  • We will lift state up in order to keep the temperature values in our two inputs in sync with each other.
Watch Video
Duration: 8m 44s

Temperature Conversion Functions

We will write two functions to convert from Celsius to Fahrenheit and back. First, the conversion function from Fahrenheit to Celsius.

function toCelsius(fahrenheit) {
  return ((fahrenheit - 32) * 5) / 9;
}

Second, the conversion function from Celsius to Fahrenheit.

function toFahrenheit(celsius) {
  return (celsius * 9) / 5 + 32;
}

These two functions convert numbers.

We will write another function that takes a string temperature and a converter function as arguments and returns a string. We will use it to calculate the value of one input based on the other input. Inside the function, we will try to parse the temperature into a floating point number. If the result is invalid, that is NaN, we will return an empty string. Otherwise, we will run our converter function on the parsed temperature, and assign the result to a variable called output. Then, we will round the output to the third decimal place. Finally, we will return the string value of the rounded output.

function tryConvert(temperature, convert) {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return "";
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

Currently, both rendered TemperatureInput components independently keep their temperature values in local state. However, we want these two inputs to be in sync with each other. When we update the Celsius input, the Fahrenheit input should reflect the converted temperature, and vice versa.

In React, sharing state is accomplished by moving it up to the closest common ancestor of the components that need it. This is called “lifting state up”.

Our Calculator app currently looks like this:

import React, { useState } from "react";
import ReactDOM from "react-dom";

function toCelsius(fahrenheit) {  return ((fahrenheit - 32) * 5) / 9;}
function toFahrenheit(celsius) {  return (celsius * 9) / 5 + 32;}
function tryConvert(temperature, convert) {  const input = parseFloat(temperature);  if (Number.isNaN(input)) {    return "";  }  const output = convert(input);  const rounded = Math.round(output * 1000) / 1000;  return rounded.toString();}
const BoilingVerdict = props => {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
};

const scaleNames = {
  c: "Celsius",
  f: "Fahrenheit"
};

const TemperatureInput = props => {
  const [temperature, setTemperature] = useState("");

  return (
    <fieldset>
      <legend>Enter temperature in {scaleNames[props.scale]}:</legend>
      <input
        value={temperature}
        onChange={event => setTemperature(event.target.value)}
      />
      <BoilingVerdict celsius={parseFloat(temperature)} />
    </fieldset>
  );
};

const Calculator = props => {
  return (
    <div>
      <TemperatureInput scale="c" />
      <TemperatureInput scale="f" />
    </div>
  );
};

ReactDOM.render(
  <Calculator />,
  document.getElementById("root")
);

We will remove the local state from the TemperatureInput and move it into the Calculator instead. If the Calculator owns the shared state, it becomes the “source of truth” for the current temperature in both inputs. It can instruct them both to have values that are consistent with each other. Since the props of both TemperatureInput components are coming from the same parent Calculator component, the two inputs will always be in sync.

Let’s see how this works step by step. First, we will move the state up from TemperatureInput to Calculator.

const TemperatureInput = props => {
  return (
    <fieldset>
      <legend>Enter temperature in {scaleNames[props.scale]}:</legend>
      <input
        value={temperature}
        onChange={event => setTemperature(event.target.value)}
      />
      <BoilingVerdict celsius={parseFloat(temperature)} />
    </fieldset>
  );
};

const Calculator = props => {
  const [temperature, setTemperature] = useState("");
  return (
    <div>
      <TemperatureInput scale="c" />
      <TemperatureInput scale="f" />
    </div>
  );
};

Now the temperature has been lifted into the Calculator, it can pass it into both rendered TemperatureInput elements via props. We will pass values to the temperature prop later. First, we will return to the TemperatureInput component, and ensure that the reference to temperature in the input is now via props.

const TemperatureInput = props => {
  return (
    <fieldset>
      <legend>Enter temperature in {scaleNames[props.scale]}:</legend>
      <input
        value={props.temperature}        onChange={event => setTemperature(event.target.value)}
      />
      <BoilingVerdict celsius={parseFloat(temperature)} />
    </fieldset>
  );
};

Now, we will move the BoilingVerdict up into the Calculator component. Updates to the temperature will also be handled by the Calculator. And, the handler will be passed down to the TemperatureInput component, via a prop that we will call onTemperatureChange. We will pass handler functions to the onTemperatureChange prop later. First, we should now use the onTemperatureChange prop within the TemperatureInput component.

const TemperatureInput = props => {
  return (
    <fieldset>
      <legend>Enter temperature in {scaleNames[props.scale]}:</legend>
      <input
        value={props.temperature}
        onChange={event => props.onTemperatureChange(event.target.value)}      />
    </fieldset>
  );
};

const Calculator = props => {
  const [temperature, setTemperature] = useState("");

  return (
    <div>
      <TemperatureInput
        temperature={}        scale="c" 
        onTemperatureChange={}      />
      <TemperatureInput
        temperature={}        scale="f" 
        onTemperatureChange={}      />
      <BoilingVerdict celsius={parseFloat(temperature)} />
    </div>
  );
};

Let’s return to the Calculator component. In addition to keeping a record of the temperature in state, the Calculator should also keep a record of which scale the temperature is referring to: Celsius or Fahrenheit.

We will add another state variable for the scale. We will set the initial scale value to Celsius.

const Calculator = props => {
  const [temperature, setTemperature] = useState("");
  const [scale, setScale] = useState("c");
  return (
    <div>
      <TemperatureInput
        temperature={}
        scale="c" 
        onTemperatureChange={}
      />
      <TemperatureInput
        temperature={}
        scale="f" 
        onTemperatureChange={}
      />
      <BoilingVerdict celsius={parseFloat(temperature)} />
    </div>
  );
};

We will now calculate the temperature values in both Celsius and Fahrenheit for both inputs, depending on the temperate scale and temperature value.

First, to calculate the Celsius temperature, if the scale is Fahrenheit, then we will try to convert the input value into Celsius, otherwise, just use the value which was input.

In order to calculate the Fahrenheit temperature, if the scale is Celsius, then we will try to convert the input value into Fahrenheit, otherwise, just use the value which was input.

We can now pass these calcuated values into the rendered TemperatureInput elements.

const Calculator = props => {
  const [temperature, setTemperature] = useState("");
  const [scale, setScale] = useState("c");

  const celsius =    scale === "f" ? tryConvert(temperature, toCelsius) : temperature;  const fahrenheit =    scale === "c" ? tryConvert(temperature, toFahrenheit) : temperature;
  return (
    <div>
      <TemperatureInput
        temperature={celsius}        scale="c" 
        onTemperatureChange={}
      />
      <TemperatureInput
        temperature={fahrenheit}        scale="f" 
        onTemperatureChange={}
      />
      <BoilingVerdict celsius={parseFloat(temperature)} />
    </div>
  );
};

As a final step, we will need to define the handler functions which will be called when the input values change.

First, we will handle changes in the Celsius temperature input. We will call the handler handleCelsiusChange, which will take the temperature value as its input. We will set the scale to Celsius. And, we will set the temperature value.

Next, we will handle changes in the Fahrenheit temperature input. We will call the handler handleFahrenheitChange, which will take the temperature value as its input. We will set the scale to Fahrenheit. And, we will set the temperature value.

Now, we can pass these handlers into the rendered TemperatureInput elements.

const Calculator = props => {
  const [temperature, setTemperature] = useState("");
  const [scale, setScale] = useState("c");

  const celsius =
    scale === "f" ? tryConvert(temperature, toCelsius) : temperature;
  const fahrenheit =
    scale === "c" ? tryConvert(temperature, toFahrenheit) : temperature;

  const handleCelsiusChange = temperature => {    setScale("c");    setTemperature(temperature);  };
  const handleFahrenheitChange = temperature => {    setScale("f");    setTemperature(temperature);  };
  return (
    <div>
      <TemperatureInput
        temperature={celsius}
        scale="c"
        onTemperatureChange={handleCelsiusChange}      />
      <TemperatureInput
        temperature={fahrenheit}
        scale="f"
        onTemperatureChange={handleFahrenheitChange}      />
      <BoilingVerdict celsius={parseFloat(temperature)} />
    </div>
  );
};

The entire app now looks like this:

import React, { useState } from "react";
import ReactDOM from "react-dom";

function toCelsius(fahrenheit) {
  return ((fahrenheit - 32) * 5) / 9;
}

function toFahrenheit(celsius) {
  return (celsius * 9) / 5 + 32;
}

function tryConvert(temperature, convert) {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return "";
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

const BoilingVerdict = props => {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
};

const scaleNames = {
  c: "Celsius",
  f: "Fahrenheit"
};

const TemperatureInput = props => {
  return (
    <fieldset>
      <legend>Enter temperature in {scaleNames[props.scale]}:</legend>
      <input
        value={props.temperature}
        onChange={event => props.onTemperatureChange(event.target.value)}
      />
    </fieldset>
  );
};

const Calculator = props => {
  const [temperature, setTemperature] = useState("");
  const [scale, setScale] = useState("c");

  const celsius =
    scale === "f" ? tryConvert(temperature, toCelsius) : temperature;
  const fahrenheit =
    scale === "c" ? tryConvert(temperature, toFahrenheit) : temperature;

  const handleCelsiusChange = temperature => {
    setScale("c");
    setTemperature(temperature);
  };

  const handleFahrenheitChange = temperature => {
    setScale("f");
    setTemperature(temperature);
  };

  return (
    <div>
      <TemperatureInput
        temperature={celsius}
        scale="c"
        onTemperatureChange={handleCelsiusChange}
      />
      <TemperatureInput
        temperature={fahrenheit}
        scale="f"
        onTemperatureChange={handleFahrenheitChange}
      />
      <BoilingVerdict celsius={parseFloat(temperature)} />
    </div>
  );
};

ReactDOM.render(
  <Calculator />,
  document.getElementById("root")
);

Will now test the Calculator in the browser. Enter the value 100 into the Celcius input. Note that the value in the Fahrenheit input automatically shows as 212 degrees Fahrenheit, which is the boiling point of water.

Now, let’s enter the value 80 in the Fahrenheit input. Note that the value in the Celcius input automatically updates to be about 27 degrees Celcius.

Our inputs are now in sync.

Summary

We lifted state up in order to keep the temperature values in our two inputs in sync with each other.

Next Up…

In the next video, we will compare composition with inheritance.