[React MAIN CONCEPTS] 10. Lifting State Up
10. Lifting State Up
Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor. Let’s see how this works in action.
Currently, both TemperatureInput components independently keep their values in the local state:
1 | class TemperatureInput extends React.Component { |
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”. 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 replace this.state.temperature
with this.props.temperature
in the TemperatureInput
component. For now, let’s pretend this.props.temperature
already exists, although we will need to pass it from the Calculator
in the future:
1 | render() { |
We know that props are read-only. When the temperature was in the local state, the TemperatureInput
could just call this.setState()
to change it. However, now that the temperature is coming from the parent as a prop, the TemperatureInput has no control over it.
In React, this is usually solved by making a component “controlled”. Just like the DOM <input>
accepts both a value and an onChange
prop, so can the custom TemperatureInput
accept both temperature
and onTemperatureChange
props from its parent Calculator
.
Now, when the TemperatureInput wants to update its temperature, it calls this.props.onTemperatureChange
:
1 | handleChange(e) { |
Note:
There is no special meaning to either temperature
or onTemperatureChange
prop names in custom components. We could have called them anything else, like name them value and onChange
which is a common convention.
The onTemperatureChange
prop will be provided together with the temperature prop by the parent Calculator
component. It will handle the change by modifying its own local state, thus re-rendering both inputs with the new values. We will look at the new Calculator
implementation very soon.
Before diving into the changes in the Calculator, let’s recap our changes to the TemperatureInput
component. We have removed the local state from it, and instead of reading this.state.temperature, we now read this.props.temperature
. Instead of calling this.setState()
when we want to make a change, we now call this.props.onTemperatureChange()
, which will be provided by the Calculator
:
1 | class TemperatureInput extends React.Component { |
Now let’s turn to the Calculator
component.
We will store the current input’s temperature and scale in its local state. This is the state we “lifted up” from the inputs, and it will serve as the “source of truth” for both of them. It is the minimal representation of all the data we need to know in order to render both inputs.
For example, if we enter 37 into the Celsius input, the state of the Calculator component will be:
1 | { |
If we later edit the Fahrenheit field to be 212, the state of the Calculator will be:
1 | { |
We could have stored the value of both inputs but it turns out to be unnecessary. It is enough to store the value of the most recently changed input, and the scale that it represents. We can then infer the value of the other input based on the current temperature and scale alone.
The inputs stay in sync because their values are computed from the same state:
1 | class Calculator extends React.Component { |
Try it on CodePen - https://codepen.io/gaearon/pen/WZpxpz?editors=0010
Now, no matter which input you edit, this.state.temperature and this.state.scale in the Calculator get updated. One of the inputs gets the value as is, so any user input is preserved, and the other input value is always recalculated based on it.
Let’s recap what happens when you edit an input:
-
React calls the function specified as
onChange
on the DOM<input>
. In our case, this is thehandleChange
method in theTemperatureInput
component. -
The
handleChange
method in theTemperatureInput
component callsthis.props.onTemperatureChange()
with the new desired value. Its props, includingonTemperatureChange
, were provided by its parent component, theCalculator
. -
When it previously rendered, the
Calculator
had specified thatonTemperatureChange
of theCelsius TemperatureInput
is the Calculator’shandleCelsiusChange
method, andonTemperatureChange
of the FahrenheitTemperatureInput
is the Calculator’shandleFahrenheitChange
method. So either of these twoCalculator
methods gets called depending on which input we edited. -
Inside these methods, the
Calculator
component asks React to re-render itself by callingthis.setState()
with the new input value and the current scale of the input we just edited. -
React calls the
Calculator
component’s render method to learn what the UI should look like. The values of both inputs are recomputed based on the current temperature and the active scale. The temperature conversion is performed here. -
React calls the render methods of the individual
TemperatureInput
components with their new props specified by theCalculator
. It learns what their UI should look like. -
React calls the render method of the
BoilingVerdict
component, passing the temperature in Celsius as its props. -
React DOM updates the DOM with the boiling verdict and to match the desired input values. The input we just edited receives its current value, and the other input is updated to the temperature after conversion.
Every update goes through the same steps so the inputs stay in sync.
Lessons Learned
There should be a single “source of truth” for any data that changes in a React application. Usually, the state is first added to the component that needs it for rendering. Then, if other components also need it, you can lift it up to their closest common ancestor. Instead of trying to sync the state between different components, you should rely on the top-down data flow.
Lifting state involves writing more “boilerplate” code than two-way binding approaches, but as a benefit, it takes less work to find and isolate bugs. Since any state “lives” in some component and that component alone can change it, the surface area for bugs is greatly reduced. Additionally, you can implement any custom logic to reject or transform user input.
If something can be derived from either props or state, it probably shouldn’t be in the state. For example, instead of storing both celsiusValue and fahrenheitValue, we store just the last edited temperature and its scale. The value of the other input can always be calculated from them in the render() method. This lets us clear or apply rounding to the other field without losing any precision in the user input.\
References
[1] Lifting State Up – React - https://reactjs.org/docs/lifting-state-up.html
[2] React – A JavaScript library for building user interfaces - https://reactjs.org/