10. React - sdílené stavy (Lifting State Up)

17.02.2019 Programování #react #learning

Často je třeba, aby některé komponenty předávali stejné měnící se údaje. Doporučujeme zvednout sdílený stav až na nejbližšího společného předka. Podívejme se, jak to funguje v akci.


V této části vytvoříme kalkulačku teploty, která vypočítá, zda by se voda měla při určité teplotě vařit.

Začneme s komponentou nazvanou BoilingVerdict. Přijme teplotu v celsius jako podklad pro to test, zda se bude vařit voda:

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}

Dále vytvoříme komponentu nazvanou Calculator. Ukazuje element, < input > který umožňuje zadat teplotu a udržuje její hodnotu this.state.temperature.

Navíc vykresluje BoilingVerdict aktuální vstupní hodnotu.

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

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

  render() {
    const temperature = this.state.temperature;
    return (
      <fieldset>
        <legend>Enter temperature in Celsius:</legend>
        <input
          value={temperature}
          onChange={this.handleChange} />

        <BoilingVerdict
          celsius={parseFloat(temperature)} />

      </fieldset>
    );
  }
}

GitHub

Přidání druhého vstupu

Naším novým požadavkem je, že vedle pro zadání Celsia poskytujeme pole pro zadání Fahrenheita a jsou udržovány v synchronizaci.

Můžeme začít extrahováním TemperatureInput komponenty z Calculator. Přidáme novou scale rekvizitu k tomu, že může být buď "c" anebo "f":

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

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {temperature: ''};
  }

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

  render() {
    const temperature = this.state.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

Nyní můžeme změnit, Calculator abychom měli dva oddělené teplotní vstupy:

class Calculator extends React.Component {
  render() {
    return (
      <div>
        <TemperatureInput scale="c" />
        <TemperatureInput scale="f" />
      </div>
    );
  }
}

Nyní máme dva vstupy, ale když zadáte teplotu v jednom z nich, druhý se neaktualizuje. To je v rozporu s naším požadavkem: chceme je udržovat v synchronizaci.

Nemůžeme také zobrazit BoilingVerdict CalculatorCalculator nezná aktuální teplotu, protože se skrývá uvnitř TemperatureInput.

Napsání konverzních funkcí

Nejprve napíšeme dvě funkce, které převádí z Celsia na Fahrenheita a zpět:

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

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

Tyto dvě funkce převádějí čísla. Budeme psát další funkci, která vezme řetězec temperature a konvertuje  ji jako argument a vrátí řetězec. Použijeme jej k výpočtu hodnoty jednoho vstupu na základě druhého vstupu.

Vrátí prázdný řetězec na neplatné temperature a udržuje výstup zaokrouhlený na třetí desetinné místo:

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

Například tryConvert('abc', toCelsius) vrátí prázdný řetězec a tryConvert('10.22', toFahrenheit) vrátí se '50.396'.

Sdílení stavu

V současné době obě složky TemperatureInput nezávisle udržují své hodnoty v lokálním stavu:

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {temperature: ''};
  }

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

  render() {
    const temperature = this.state.temperature;
    // ...  

Nicméně chceme, aby byly tyto dva vstupy vzájemně synchronizovány. Když aktualizujeme vstup Celsia, vstup Fahrenheita by měl odrážet převedenou teplotu a naopak.

V Reactu je sdílení stavu dosaženo přesunutím až k nejbližšímu společnému předku komponent, které jej potřebují. Toto se nazývá "sdílení stavu". Odstraníme z něj lokální stav TemperatureInput Calculator místo toho jej přesměrujeme .

Pokud Calculator vlastní sdílený stav, stává se "zdrojem pravdy" pro současnou teplotu v obou vstupech. Může jim nařídit, aby měli hodnoty, které jsou vzájemně konzistentní. Vzhledem k tomu, že props obou TemperatureInput komponent pocházejí ze stejné rodičovské Calculator komponenty, budou dva vstupy vždy synchronizovány.

Uvidíme, jak to funguje krok za krokem.

Nejprve this.state.temperature se this.props.temperature TemperatureInput komponentě nahradíme. Prozatím budeme předstírat, že this.props.temperature již existuje, ačkoli to budeme muset předat v budoucnu Calculator:

 render() {
    // Before: const temperature = this.state.temperature;
    const temperature = this.props.temperature;
    // ...

Víme, že props jsou jen pro čtení. Když temperature byl v lokálním stavu, TemperatureInput mohl jen volat this.setState() o změnu. Nicméně, nyní, když temperature pochází od rodiče jako podpora, TemperatureInput nemá nad ním kontrolu.

V Reactu se to obvykle řeší tím, že se součást "řídí". Stejně jako DOM  přijímá oba value onChange prop, tak může uživatel TemperatureInput přiznat oba temperature onTemperatureChange props od svého rodiče Calculator.

Nyní, když TemperatureInput chce aktualizovat teplotu, volá this.props.onTemperatureChange:

handleChange(e) {
    // Before: this.setState({temperature: e.target.value});
    this.props.onTemperatureChange(e.target.value);
    // ...

onTemperatureChange props bude dodána s temperature stojkou mateřskou Calculator komponentou. To zvládne změnu tím, že upraví svůj vlastní lokální stav, čímž znovu provede oba vstupy s novými hodnotami. V Calculator se podíváme na novou implementaci.

Předtím, než se ponoříme do změn Calculator, zopakujme naše změny komponenty TemperatureInput. Odstranili jsme z něj místní stav a namísto čtení this.state.temperature jsme četli this.props.temperature. Namísto volání, this.setState() když chceme provést změnu, nyní voláme this.props.onTemperatureChange(), kterou poskytnou Calculator:

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.props.onTemperatureChange(e.target.value);
  }

  render() {
    const temperature = this.props.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

Teď se podíváme na komponentu Calculator.

Uložíme aktuální vstupy temperature scale jejich lokální stav. To je stav, který jsme zvedli ze vstupů, a bude sloužit jako "zdroj pravdy" pro oba. Je to minimální reprezentace všech dat, které potřebujeme znát, abychom získali oba vstupy.

Například pokud zadáme hodnotu 37 do stupně Celsia, stav Calculator komponenty bude:

{
  temperature: '37',
  scale: 'c'
}

Pokud budeme později upravovat pole Fahrenheita na 212, stav Calculator bude:

{
  temperature: '212',
  scale: 'f'
}

Mohli jsme uložit hodnotu obou vstupů, ale bylo to zbytečné. Stačí uložit hodnotu naposledy změněného vstupu a veličiny, kterou reprezentuje. Pak můžeme odvodit hodnotu druhého vstupu založeného na aktuálním temperature scale samotném.

Vstupy zůstávají synchronizovány, protože jejich hodnoty jsou vypočítávány ze stejného stavu:

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
    this.state = {temperature: '', scale: 'c'};
  }

  handleCelsiusChange(temperature) {
    this.setState({scale: 'c', temperature});
  }

  handleFahrenheitChange(temperature) {
    this.setState({scale: 'f', temperature});
  }

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

    return (
      <div>
        <TemperatureInput
          scale="c"
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange} />

        <TemperatureInput
          scale="f"
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange} />

        <BoilingVerdict
          celsius={parseFloat(celsius)} />

      </div>
    );
  }
}

Nyní, bez ohledu na to, který vstup jste upravili, this.state.temperature this.state.scale Calculator aktualizovat. Jeden ze vstupů dostane hodnotu tak, jak je, takže každý uživatelský vstup je zachován a druhá vstupní hodnota je vždy na základě toho přepočítána.

Zjistěte, co se stane, když upravíte vstup:

  • React volá funkci zadané jako onChange v DOM. V našem případě jde o handleChange metodu v TemperatureInput komponentě.
  • Metodu handleChange v komponentě TemperatureInput volá this.props.onTemperatureChange() s novou požadovanou hodnotu. Jeho props, včetně onTemperatureChange, byly poskytnuty jeho mateřská složka, tj. Calculator.
  • Když se již poskytnuté se Calculator určil, že onTemperatureChange v stupních Celsia TemperatureInput je Calculator to handleCelsiusChange způsob, a onTemperatureChange v Fahrenheit TemperatureInput je Calculator to handleFahrenheitChange metoda. Každá z těchto dvou Calculator metod se tedy volá podle toho, který vstup jsme upravili.
  • Uvnitř těchto metod Calculator požádá komponenta React, aby se znovu vykreslil voláním this.setState() s novou vstupní hodnotou a aktuálním měřítkem vstupu, který jsme právě upravili.
  • React volá metodu Calculator komponenty a render zjistí, jak by měl vypadat uživatelský rozhraní. Hodnoty obou vstupů se přepočítávají na základě aktuální teploty a aktivní stupnice. Konverze teploty se provádí zde.
  • React volá render metody jednotlivých TemperatureInput komponent s novými props, které jsou specifikovány příkazem Calculator. Učí se, jak by měl vypadat jejich uživatelský rozhraní.
  • React volá render metodu BoilingVerdict komponenty a prochází teplotu ve stupních Celsia.
  • React DOM aktualizuje DOM s větším verdiktem a odpovídá požadovaným vstupním hodnotám. Vstup, který jsme právě upravili, obdrží jeho aktuální hodnotu a druhý vstup je po konverzi aktualizován na teplotu.

Každá aktualizace prochází stejnými kroky, takže vstupy zůstávají v synchronizaci.

GitHub