8

十、提升state

通常,如果有几个组件需要反映相同的变化数据。 我们建议将共享state提升到层级最近的,并且是共同的父组件上。 让我们看看这是如何工作的。

在本节中,我们将创建一个温度计算器来计算水是否在给定温度下沸腾。

我们将从一个名为BoilingVerdict的组件开始。 它接受celsius(摄氏温度)作为props,并打印是否足以煮沸水:

function BoilingVerdict(props) {
    if (props.celsius >= 100) {
        return <p>水沸腾了</p>
    }
    return <p>水没有沸腾</p>
}

接下来,我们创建一个名字叫Calculator的组件。它渲染一个<input>,让您输入温度,并将其值保存在this.state.value中。

此外,它还会根据当前输入值渲染BoilingVerdict

import React from 'react';
import ReactDOM from 'react-dom';

function BoilingVerdict(props) {
    if (props.celsius >= 100) {
        return <p>水沸腾了</p>
    }
    return <p>水没有沸腾</p>
}

class Calculator extends React.Component {
    constructor(props) {
        super(props);
        this.state = {value: ''};
        this.change = this.change.bind(this);
    }

    change(e) {
        this.setState({value: e.target.value});
    }

    render() {
        const value = this.state.value;
        return (
            <fieldset>
                <legend>请输入温度</legend>
                <input
                    value={value}
                    onChange={this.change}/>
                <BoilingVerdict celsius={parseFloat(value)}/>
            </fieldset>
        );
    }
}
ReactDOM.render(
    <Calculator />,
    document.getElementById('root')
);

添加第二个input

我们的新需求是,除了输入摄氏温度,我们提供一个输入华氏温度,并保持同步。

我们可以从Calculator提取一个TemperatureInput组件。 我们将添加一个新的scale prop,可以是“c”“f”

const scaleNames = {
    c: 'Celsius',
    f: 'Fahrenheit'
}
class TemperatureInput extends React.Component {
    constructor(props) {
        super(props);
        this.state = {value: ''};
        this.change = this.change.bind(this)
    }
    change(e) {
        this.setState({value: e.target.value});
    }
    render() {
        const value = this.state.value;
        const scale = this.props.scale;
        return (
            <fieldset>
                 <legend>输入{scaleName[scale]}温度</legend>
                 <input value={value} onChange={this.change} />
            </fieldset>
        );
    }
}

我们现在可以更改计算器来渲染两个单独的温度输入:

class Calculator extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            <div>
                <TemperatureInput scale='f' />
                <TemperatureInput scale='c' />
            </div>
        );
    }
}
ReactDOM.render(
    <Calculator />,
    document.getElementById('root')
)

我们现在有两个输入框,但是当您在其中一个输入温度时,另一个不更新。 这违反了我们的要求:我们希望保持它们同步。

我们也不能从Calculator显示BoilingVerdict。 计算器不知道当前温度,因为它隐藏在TemperatureInput中。

提升state

首先,我们将写两个函数来将摄氏度转换为华氏度,然后返回:

// 将华氏度转换为摄氏度
function toCelsius(f) {
    return (f - 32) * 5 / 9;
}
// 将摄氏度转换为华氏度
function toFahrenheit(c) {
    return (c * 9 / 5) + 32;
}

这两个函数转换数字。 我们将写另一个函数,它接受一个字符串值和一个转换函数作为参数,并返回一个字符串。 我们将使用它来计算一个输入基于另一个输入的值。

如果传入一个无效的value,那么会返回一个空字符串,并且保持输出四舍五入到小数点后第三位:

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

例如,tryConvert('abc', toCelsius)返回一个空字符串,tryConvert('10.22', toFahrenheit)返回50.396
接下来,我们会从TemperatureInput中移除state。同时从props接收一个value和一个onChange事件来代替。

class Temperature extends React.Component {
    constructor(props) {
        super(props);
        this.change = this.change.bind(this);
    }
    
    change(e) {
        this.props.onChange(e.target.value);
    }
    
    render() {
        const value = this.props.value;
        const scale = this.props.scale;
        return (
            <fieldset>
                <legend>请输入{scaleName[scale]}温度</legend>
                <input value={value} onChange={this.change} />
            </fieldset>
        );
    }
}

如果几个组件需要访问相同的state,这是一个state应该提升到层级最近的共同父级组件的标志。 在我们的例子中,这是那个Calculator组件。 我们将在其state中存储当前valuescale

我们可以存储两个输入框的值,但事实证明这是没有必要的。 它足以存储最近更改的输入的value及其表示的scale。 然后,我们可以基于当前valuescale单独推断其他输入的值。

输入保持同步,因为它们的值是从相同的state计算的:

class Calculator extends React.Component {
    constructor(props) {
        super(props);
        this.state = {value: '', scale: 'c'};
        this.CelsiusChange = this.CelsiusChange.bind(this);
        this.FahrenheitChange = this.FahrenheitChange.bind(this);
    }

    CelsiusChange() {
        this.setState({scale: 'c', value});
    }

    FahrenheitChange() {
        this.setState({scale: 'f', value});
    }

    render() {
        const scale = this.state.scale;
        const value = this.state.value;
        const celsius = scale === 'f' ? tryConvert(value, toCelsius) : value;
        const fahrenheit = scale === 'c' ? tryConvert(value, toFahrenheit) : value;

        return (
            <div>
                <Temperature scale="c" value={celsius} onChange={this.CelsiusChange} />
                <Temperature scale="f" value={fahrenheit} onChange={this.FahrenheitChange} />
                <BoilingVerdict celsius={parseFloat(celsius)} />
            </div>
        );
    }
}

最终代码:

import React from 'react';
import ReactDOM from 'react-dom';

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

function BoilingVerdict(props) {
    if (props.celsius >= 100) {
        return <p>水沸腾了</p>
    }
    return <p>水没有沸腾</p>
}

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

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

function tryConvert(value, convert) {
    const input = parseFloat(value);
    if (Number.isNaN(input)) {
        return '';
    }
    const output = convert(input);
    const rounded = Math.round(output * 1000) / 1000;
    return String(rounded);
}
class TemperatureInput extends React.Component {
    constructor(props) {
        super(props);
        this.change = this.change.bind(this);
    }

    change(e) {
        this.props.onChange(e.target.value);
    }

    render() {
        const value = this.props.value;
        const scale = this.props.scale;
        return (
            <fieldset>
                <legend>请输入{scaleNames[scale]}温度</legend>
                <input value={value} onChange={this.change}/>
            </fieldset>
        );
    }
}

class Calculator extends React.Component {
    constructor(props) {
        super(props);
        this.state = {value: '', scale: 'c'};
        this.CelsiusChange = this.CelsiusChange.bind(this);
        this.FahrenheitChange = this.FahrenheitChange.bind(this);
    }

    CelsiusChange(value) {
        this.setState({scale: 'c', value});
    }

    FahrenheitChange(value) {
        this.setState({scale: 'f', value});
    }

    render() {
        const scale = this.state.scale;
        const value = this.state.value;
        const celsius = scale === 'f' ? tryConvert(value, toCelsius) : value;
        const fahrenheit = scale === 'c' ? tryConvert(value, toFahrenheit) : value;

        return (
            <div>
                <TemperatureInput scale="c" value={celsius} onChange={this.CelsiusChange}/>
                <TemperatureInput scale="f" value={fahrenheit} onChange={this.FahrenheitChange}/>
                <BoilingVerdict celsius={parseFloat(celsius)}/>
            </div>
        );
    }
}
ReactDOM.render(
    <Calculator />,
    document.getElementById('root')
);

现在,无论您编辑哪个输入框,Calculator中的this.state.valuethis.state.scale都会更新。 其中一个输入框获取value为原样,所以任何用户输入都被保留,另一个输入值总是基于它重新计算。

经验教训

对于在React应用程序中更改的任何数据,都应该有一个唯一的“数据来源”,也就是state。通常,首先将state添加到需要渲染的组件。如果其他组件也需要它,你可以将其提升到它们层级最近的共同父级组件中。而不是尝试在不同组件之间去同步状态,总归就一句话:你应该依赖于自上而下的数据流

提升state会涉及编写比双向绑定方法更多的“样板”代码。但这样做有一个好处,就是开发者可以很快就找到错误。由于所有的state都“保存”在这些组件中并且只有该组件可以改变它,所以大大减少了错误的出现概率。此外,你可以实现任何自定义逻辑来拒绝或转换用户输入。

如果某些东西可以从prps或state派生,它都不应该再继续呆在state里。
例如,不是同时存储celsiusValuefahrenheitValue,而是只存储最后一次编辑的valuescale。另一个输入的值总是可以从render()方法中计算出来。这允许我们清除或应用四舍五入到其他字段,而不会丢失用户输入。

当您在UI中看到错误时,可以使用React Developer Tools检查props,并向上逐个排查DOM树,直到找到负责更新state的组件。这使你可以轻松地跟踪错误来源。


张亚涛
5.3k 声望2.8k 粉丝

人首先应该接受现实,承认问题的存在,并且反思它。而不是首先就跳出来回避这个问题,找比我们更差的。质疑甚至抨击提出问题的人,并且一杆子打死。