Frequentemente, vários componentes precisam refletir a mesma mudança de dados. Nós recomendamos subir o estado compartilhado para seu ancestrai comun mais próximo. Vamos ver este trabalho em ação.
Nesta seção, vamos criar uma calculadora de temperatura que calcula se água ferve a uma dada temperatura.
Vamos começar com um componente chamado BoilingVerdict
. Ele aceita a temperatura em celsius
como uma propriedade, e irá imprimir se é suficiente para ferver a água:
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}
A seguir, criamos um componente chamado Calculator
. Ele renderiza um <input>
que permite digitar a temperatura, e mantém seu valor em this.state.value
.
Adicionalmente, renderizamos o BoilingVerdict
para a entrada atual.
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {value: ''};
}
handleChange(e) {
this.setState({value: e.target.value});
}
render() {
const value = this.state.value;
return (
<fieldset>
<legend>Enter temperature in Celsius:</legend>
<input
value={value}
onChange={this.handleChange} />
<BoilingVerdict
celsius={parseFloat(value)} />
</fieldset>
);
}
}
Nosso novo requisito é, a partir de um input em Celsius, prover um input em Fahrenheit, e manter as inputs sincronizados.
Podemos comerçar extraindo um componente TemperatureInput
de Calculator
. Iremos adicionar uma nova propriedade scale
que pode ser "c"
ou "f"
:
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {value: ''};
}
handleChange(e) {
this.setState({value: e.target.value});
}
render() {
const value = this.state.value;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={value}
onChange={this.handleChange} />
</fieldset>
);
}
}
Agora podemos alterar o Calculator
para renderizar dois inputs de temperatura:
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
Agora temos dois inputs, mas quando você digita a temperatura em um delas, o outro não atualiza. Isto vai de encontro ao nosso requisito: nós queremos manter as inputs sincronizados.
Também não é possível exibir o BoilingVerdict
de Calculator
. O Calculator
não sabe a temperatura atual porque está oculta dentro de TemperatureInput
.
Primeiro, escreveremos duas funções para converter de Celsius para Fahrenheit e de Fahrenheit para Celsius:
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
Estas duas funções convertem os números. Vamos escrever outra função que pega uma string value
e uma função de conversão como argumento e retorna uma string. Vamos usar isso para calcular uma temperatura em uma escala baseado no valor do outro input.
O código retorna uma string vazia se um value
inválido for informado, e mantém a saída arredondada até o terceiro decimal:
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 rounded.toString();
}
Por exemplo, tryConvert('abc', toCelsius)
retorna uma string vazia, e tryConvert('10.22', toFahrenheit)
retorna '50.396'
.
A seguir, vamos remover o estado de TemperatureInput
.
Ao invés disso, vamos recever ambos os value
e o manipulador onChange
como propriedade:
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onChange(e.target.value);
}
render() {
const value = this.props.value;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={value}
onChange={this.handleChange} />
</fieldset>
);
}
}
Se vários componentes precisam acessar o mesmo estado, é um sinal de que o estado deve ser subido para o ancestral comum mais próximo. No nosso caso, é o Calculator
. Vamos guardar o atual value
e scale
no seu estado.
Podemos ter armazenado o valor de ambos os inputs, mas isto se torna desnecessário. É suficiente armazenar o valor do input que mudou mais recentemente, e a escala que o representa. Nós podemos então inferir o valor do outro input baseado no estado atual de value
e scale
.
Os inputs se mantém sincronizados porque seus valores são calculados a partir do mesmo estado:
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {value: '', scale: 'c'};
}
handleCelsiusChange(value) {
this.setState({scale: 'c', value});
}
handleFahrenheitChange(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.handleCelsiusChange} />
<TemperatureInput
scale="f"
value={fahrenheit}
onChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
Agora, não importa qual input você edita, this.state.value
e this.state.scale
no Calculator
é atualizado. Um dos valores recebe o valor como ele é, então a entrada do usuário é preservada, e o valor do outro input é sempre recalculado baseado nele.
Deve haver uma única "fonte de verdade" para quaisuquer dados que mudam em uma aplicação React. Geralmente, o estado é primeiro adicionado ao componente que precisa dele para renderização. Então, se outros componentes precisam dele, você pode subir até o ancestral comum mais próximo. Ao invés disso você pode sincronizar o estado entre diferentes componentes, 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, você pode contar com o flux de dados de cima para baixo.
Subir o estado envolve mais escrever código "boilerrplate" que a abordagem two-way binding, mas como um benefício, isto traz menos trabalho para localizar e isolar bugs. Como qualquer estado "vive" em algum componente este componente sozinho pode alterá-lo por si, a área para bugs é muito reduzida. Adicionalmente, você pode implementar qualquer lógica customizada para rejeitar ou transformar entradas do usuário.
Se algo pode ser entregue ou por propriedades(props) ou por estado(state), provavelmente não deve estar no estado. Por exemplo, ao invés de armazenar ambos celsiusValue
e fahrenheitValue
, armazenamos apenas o último value
alterado e sua scale
. O valor do outro input pode sempre ser calculado do outro no método render()
. Isto possibilita limpar ou aplicar arredondamento ao outro campo sem perder qualquer precisão na entrada do usuário.
Quando você ver algo errado na interface do usuário, você pode usar as ferramentas de desenvolvedor do React para inspecionar as propriedades e mover pela árvore até você encontrar o componente responsável por atualizar o estado. Isto permite rastrear os bugs até sua fonte: