Edit on GitHub

Subindo o estado

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>
    );
  }
}

Teste este código no CodePen.

Adicionando uma segunda entrada #

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>
    );
  }
}

Teste este código no CodePen.

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.

Subindo o estado #

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>
    );
  }
}

Teste este código no CodePen.

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.

Lições aprendidas #

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:

Monitorando estado com React DevTools