Skip to content

Commit a190cfc

Browse files
valsciongaearon
authored andcommitted
Update Lifting State Up not to mix up DOM value with component state (facebook#9032)
* Update Lifting State Up not to mix up DOM value with component state A few weeks ago when teaching my friend, she got stuck on `this.state.value` vs. `event.target.value`. As the documentation talked a lot about "values", and the term value could mean three different things (values in general, the "value" prop / DOM value of the <input> component and the value in state/props), it was not weird that she got a bit confused. * Rename Lifting State Up onChange props to onTemperatureChange This is in-line with how the temperature is provided as a prop named `temperature` * Fix one value prop not being renamed to temperature * Update codepen examples in Lifting state up documentation * Update devtools state change to reflect docs change
1 parent c8f14c2 commit a190cfc

File tree

2 files changed

+54
-54
lines changed

2 files changed

+54
-54
lines changed

docs/docs/lifting-state-up.md

Lines changed: 54 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function BoilingVerdict(props) {
2424
}
2525
```
2626

27-
Next, we will create a component called `Calculator`. It renders an `<input>` that lets you enter the temperature, and keeps its value in `this.state.value`.
27+
Next, we will create a component called `Calculator`. It renders an `<input>` that lets you enter the temperature, and keeps its value in `this.state.temperature`.
2828

2929
Additionally, it renders the `BoilingVerdict` for the current input value.
3030

@@ -33,30 +33,30 @@ class Calculator extends React.Component {
3333
constructor(props) {
3434
super(props);
3535
this.handleChange = this.handleChange.bind(this);
36-
this.state = {value: ''};
36+
this.state = {temperature: ''};
3737
}
3838
3939
handleChange(e) {
40-
this.setState({value: e.target.value});
40+
this.setState({temperature: e.target.value});
4141
}
4242
4343
render() {
44-
const value = this.state.value;
44+
const temperature = this.state.temperature;
4545
return (
4646
<fieldset>
4747
<legend>Enter temperature in Celsius:</legend>
4848
<input
49-
value={value}
49+
value={temperature}
5050
onChange={this.handleChange} />
5151
<BoilingVerdict
52-
celsius={parseFloat(value)} />
52+
celsius={parseFloat(temperature)} />
5353
</fieldset>
5454
);
5555
}
5656
}
5757
```
5858

59-
[Try it on CodePen.](http://codepen.io/gaearon/pen/Gjxgrj?editors=0010)
59+
[Try it on CodePen.](http://codepen.io/valscion/pen/VpZJRZ?editors=0010)
6060

6161
## Adding a Second Input
6262

@@ -74,20 +74,20 @@ class TemperatureInput extends React.Component {
7474
constructor(props) {
7575
super(props);
7676
this.handleChange = this.handleChange.bind(this);
77-
this.state = {value: ''};
77+
this.state = {temperature: ''};
7878
}
7979
8080
handleChange(e) {
81-
this.setState({value: e.target.value});
81+
this.setState({temperature: e.target.value});
8282
}
8383
8484
render() {
85-
const value = this.state.value;
85+
const temperature = this.state.temperature;
8686
const scale = this.props.scale;
8787
return (
8888
<fieldset>
8989
<legend>Enter temperature in {scaleNames[scale]}:</legend>
90-
<input value={value}
90+
<input value={temperature}
9191
onChange={this.handleChange} />
9292
</fieldset>
9393
);
@@ -110,7 +110,7 @@ class Calculator extends React.Component {
110110
}
111111
```
112112

113-
[Try it on CodePen.](http://codepen.io/gaearon/pen/NRrzOL?editors=0010)
113+
[Try it on CodePen.](http://codepen.io/valscion/pen/GWKbao?editors=0010)
114114

115115
We have two inputs now, but when you enter the temperature in one of them, the other doesn't update. This contradicts our requirement: we want to keep them in sync.
116116

@@ -130,13 +130,13 @@ function toFahrenheit(celsius) {
130130
}
131131
```
132132

133-
These two functions convert numbers. We will write another function that takes a string `value` 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.
133+
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.
134134

135-
It returns an empty string on an invalid `value`, and it keeps the output rounded to the third decimal place:
135+
It returns an empty string on an invalid `temperature`, and it keeps the output rounded to the third decimal place:
136136

137137
```js
138-
function tryConvert(value, convert) {
139-
const input = parseFloat(value);
138+
function tryConvert(temperature, convert) {
139+
const input = parseFloat(temperature);
140140
if (Number.isNaN(input)) {
141141
return '';
142142
}
@@ -157,15 +157,15 @@ class TemperatureInput extends React.Component {
157157
constructor(props) {
158158
super(props);
159159
this.handleChange = this.handleChange.bind(this);
160-
this.state = {value: ''};
160+
this.state = {temperature: ''};
161161
}
162162
163163
handleChange(e) {
164-
this.setState({value: e.target.value});
164+
this.setState({temperature: e.target.value});
165165
}
166166
167167
render() {
168-
const value = this.state.value;
168+
const temperature = this.state.temperature;
169169
```
170170

171171
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.
@@ -176,31 +176,31 @@ If the `Calculator` owns the shared state, it becomes the "source of truth" for
176176

177177
Let's see how this works step by step.
178178

179-
First, we will replace `this.state.value` with `this.props.value` in the `TemperatureInput` component. For now, let's pretend `this.props.value` already exists, although we will need to pass it from the `Calculator` in the future:
179+
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:
180180

181181
```js{3}
182182
render() {
183-
// Before: const value = this.state.value;
184-
const value = this.props.value;
183+
// Before: const temperature = this.state.temperature;
184+
const temperature = this.props.temperature;
185185
```
186186

187-
We know that [props are read-only](/react/docs/components-and-props.html#props-are-read-only). When the `value` was in the local state, the `TemperatureInput` could just call `this.setState()` to change it. However, now that the `value` is coming from the parent as a prop, the `TemperatureInput` has no control over it.
187+
We know that [props are read-only](/react/docs/components-and-props.html#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.
188188

189-
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 `value` and `onChange` props from its parent `Calculator`.
189+
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`.
190190

191-
Now, when the `TemperatureInput` wants to update its temperature, it calls `this.props.onChange`:
191+
Now, when the `TemperatureInput` wants to update its temperature, it calls `this.props.onTemperatureChange`:
192192

193193
```js{3}
194194
handleChange(e) {
195-
// Before: this.setState({value: e.target.value});
196-
this.props.onChange(e.target.value);
195+
// Before: this.setState({temperature: e.target.value});
196+
this.props.onTemperatureChange(e.target.value);
197197
```
198198

199-
Note that there is no special meaning to either `value` or `onChange` prop names in custom components. We could have called them anything else, although this is a common convention.
199+
Note that 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.
200200

201-
The `onChange` prop will be provided together with the `value` 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.
201+
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.
202202

203-
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.value`, we now read `this.props.value`. Instead of calling `this.setState()` when we want to make a change, we now call `this.props.onChange()`, which will be provided by the `Calculator`:
203+
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`:
204204

205205
```js{8,12}
206206
class TemperatureInput extends React.Component {
@@ -210,16 +210,16 @@ class TemperatureInput extends React.Component {
210210
}
211211
212212
handleChange(e) {
213-
this.props.onChange(e.target.value);
213+
this.props.onTemperatureChange(e.target.value);
214214
}
215215
216216
render() {
217-
const value = this.props.value;
217+
const temperature = this.props.temperature;
218218
const scale = this.props.scale;
219219
return (
220220
<fieldset>
221221
<legend>Enter temperature in {scaleNames[scale]}:</legend>
222-
<input value={value}
222+
<input value={temperature}
223223
onChange={this.handleChange} />
224224
</fieldset>
225225
);
@@ -229,13 +229,13 @@ class TemperatureInput extends React.Component {
229229

230230
Now let's turn to the `Calculator` component.
231231

232-
We will store the current input's `value` 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.
232+
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.
233233

234234
For example, if we enter 37 into the Celsius input, the state of the `Calculator` component will be:
235235

236236
```js
237237
{
238-
value: '37',
238+
temperature: '37',
239239
scale: 'c'
240240
}
241241
```
@@ -244,12 +244,12 @@ If we later edit the Fahrenheit field to be 212, the state of the `Calculator` w
244244

245245
```js
246246
{
247-
value: '212',
247+
temperature: '212',
248248
scale: 'f'
249249
}
250250
```
251251

252-
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 `value` and `scale` alone.
252+
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.
253253

254254
The inputs stay in sync because their values are computed from the same state:
255255

@@ -259,33 +259,33 @@ class Calculator extends React.Component {
259259
super(props);
260260
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
261261
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
262-
this.state = {value: '', scale: 'c'};
262+
this.state = {temperature: '', scale: 'c'};
263263
}
264264
265-
handleCelsiusChange(value) {
266-
this.setState({scale: 'c', value});
265+
handleCelsiusChange(temperature) {
266+
this.setState({scale: 'c', temperature});
267267
}
268268
269-
handleFahrenheitChange(value) {
270-
this.setState({scale: 'f', value});
269+
handleFahrenheitChange(temperature) {
270+
this.setState({scale: 'f', temperature});
271271
}
272272
273273
render() {
274274
const scale = this.state.scale;
275-
const value = this.state.value;
276-
const celsius = scale === 'f' ? tryConvert(value, toCelsius) : value;
277-
const fahrenheit = scale === 'c' ? tryConvert(value, toFahrenheit) : value;
275+
const temperature = this.state.temperature;
276+
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
277+
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
278278
279279
return (
280280
<div>
281281
<TemperatureInput
282282
scale="c"
283-
value={celsius}
284-
onChange={this.handleCelsiusChange} />
283+
temperature={celsius}
284+
onTemperatureChange={this.handleCelsiusChange} />
285285
<TemperatureInput
286286
scale="f"
287-
value={fahrenheit}
288-
onChange={this.handleFahrenheitChange} />
287+
temperature={fahrenheit}
288+
onTemperatureChange={this.handleFahrenheitChange} />
289289
<BoilingVerdict
290290
celsius={parseFloat(celsius)} />
291291
</div>
@@ -294,15 +294,15 @@ class Calculator extends React.Component {
294294
}
295295
```
296296

297-
[Try it on CodePen.](http://codepen.io/gaearon/pen/ozdyNg?editors=0010)
297+
[Try it on CodePen.](http://codepen.io/valscion/pen/jBNjja?editors=0010)
298298

299-
Now, no matter which input you edit, `this.state.value` 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.
299+
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.
300300

301301
Let's recap what happens when you edit an input:
302302

303303
* React calls the function specified as `onChange` on the DOM `<input>`. In our case, this is the `handleChange` method in `TemperatureInput` component.
304-
* The `handleChange` method in the `TemperatureInput` component calls `this.props.onChange()` with the new desired value. Its props, including `onChange`, were provided by its parent component, the `Calculator`.
305-
* When it previously rendered, the `Calculator` has specified that `onChange` of the Celsius `TemperatureInput` is the `Calculator`'s `handleCelsiusChange` method, and `onChange` of the Fahrenheit `TemperatureInput` is the `Calculator`'s `handleFahrehnheitChange` method. So either of these two `Calculator` methods gets called depending on which input we edited.
304+
* The `handleChange` method in the `TemperatureInput` component calls `this.props.onTemperatureChange()` with the new desired value. Its props, including `onTemperatureChange`, were provided by its parent component, the `Calculator`.
305+
* When it previously rendered, the `Calculator` has specified that `onTemperatureChange` of the Celsius `TemperatureInput` is the `Calculator`'s `handleCelsiusChange` method, and `onTemperatureChange` of the Fahrenheit `TemperatureInput` is the `Calculator`'s `handleFahrehnheitChange` method. So either of these two `Calculator` methods gets called depending on which input we edited.
306306
* Inside these methods, the `Calculator` component asks React to re-render itself by calling `this.setState()` with the new input value and the current scale of the input we just edited.
307307
* 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.
308308
* React calls the `render` methods of the individual `TemperatureInput` components with their new props specified by the `Calculator`. It learns what their UI should look like.
@@ -316,7 +316,7 @@ There should be a single "source of truth" for any data that changes in a React
316316

317317
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.
318318

319-
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 `value` 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.
319+
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.
320320

321321
When you see something wrong in the UI, you can use [React Developer Tools](https://github.com/facebook/react-devtools) to inspect the props and move up the tree until you find the component responsible for updating the state. This lets you trace the bugs to their source:
322322

-121 KB
Loading

0 commit comments

Comments
 (0)