Lifting State Up

Thường thì một số component cần phải phản ánh cùng một dữ liệu đang thay đổi. Chúng tôi khuyên bạn nên truyền lên state đã chia sẻ cho component cha chung gần nhất. Hãy xem cách thức hoạt động.

Trong phần này, chúng ta sẽ tạo một máy tính nhiệt mà tính toán xem liệu nước có sôi trong một nhiệt độ nhất định không.

Chúng ta sẽ bắt đầu với component được gọi là BoilingVerdict. Component này chấp nhận nhiệt độ celsius như một prop, và in ra liệu có đủ nhiệt độ để đun sôi nước:

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

Tiếp theo, chúng ta sẽ tạo một component được gọi là Calculator. Component hiện thị một thẻ <input> cho bạn có thể nhập nhiệt độ, và lưu giá trị vào this.state.temperature.

Ngoài ra, nó hiển thị thẻ BoilingVerdict cho giá trị nhập hiện tại.

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

Thêm đầu vào thứ hai

Yêu cầu mới của chúng ta đó là trong phần nhập của giá trị celsius chúng ta cũng cung cấp đầu vào fahrenheit, và chúng đồng bộ với nhau.

Chúng ta có thể bắt đầu bằng cách lấy component TemperatureInput từ Calculator.Chúng ta sẽ thêm biến mới scale có thể là cả "c" hoặc "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>
    );
  }
}

Bây giờ chúng ta có thể thay đổi Calculator để hiển thị hai phần nhập nhiệt độ riêng biệt:

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

Hiện tại có hai phần, nhưng khi bạn nhập nhiệt độ vào một trong hai component, phần còn lại không cập nhật. Điều này mâu thuẫn với yêu cầu của chúng ta: giữ cho các thành phần luôn đồng bộ.

Chúng ta cũng không thể hiển thị BoilingVerdict từ Calculator. Component Calculator không biết nhiệt độ hiện tại bởi chúng ẩn bên trong TemperatureInput.

Viết hàm chuyển đổi
Đầu tiên, chúng ra sẽ viết hai function để chuyển từ celsius sang fahrenheit và ngược lại:

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

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

Hai funtion này sẽ chuyển đổi các giá trị số. Chúng ta sẽ viết một function khác sẽ lấy chuỗi temperature và function chuyển đổi ở trên như đối số và trả về một chuỗi. Chúng ta sẽ sử dụng function này để tính toán giá trị của một đầu vào dựa trên đầu vào còn lại.

Nó trả về một chuỗi rỗng với một temperature không hợp lệ, và nó giữ đầu ra gắn với làm tròn đến giá trị thập phân thứ ba:

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

Ví dụ, tryConvert('abc', toCelsius) trả về một chuỗi rỗng, và tryConvert('10.22', toCelsius) trả về ‘50.396’.

Truyền state
Hiện tại, cả hai component TemperatureInput giữ giá trị trong local state một cách độc lập:

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;
    // ... 

Tuy nhiên, chúng ta muốn cả hai đầu vào này được đồng bộ với nhau. Khi chúng ta cập nhật đầu vào celsius, đầu vào fahrenheit cũng nên thể hiện nhiệt độ đã thay đổi, và ngược lại.

Trong React, chia sẻ state là hoàn thành bằng cách chuyển nó lên cho component cha gần nhất của component cần state này. Đây được gọi là “lifting state up”. Chúng ta sẽ loại bỏ local state từ TemperatureInput và thay vào đó là chuyển nó vào Calculator.

Nếu Calculator sở hữu state được chia sẻ, nó sẽ trở thành “source of truth” cho nhiệt độ hiện tại với hai đầu vào. Nó có thể chỉ có hai đầu vào có được giá trị khớp với nhau. Vì prop của hai component TemperatureInput cùng đến từ component cha Calculator, hai đầu vào sẽ luôn đồng bộ.

Hãy cùng xem cách hoạt động theo từng bước.

Đầu tiên, chúng ta sẽ thay thế this.state.temperature với this.props.temperature trong component TemperatureInput. Với hiện tại, hãy coi như this.props.temperature đã tồn tại, mặc dù chúng ta sẽ cần truyền nó từ Calculator trong tương lai:

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

Chúng ta biết rằng props là read-only. Khi temperature đã ở trong local state, TemperatureInput chỉ cần gọi this.setState() để thay đổi state. Tuy nhiên, hiện tại temperature đến từ component cha như một prop, TemperatureInput không có quyền điều khiển nữa.

Trong React, điều này thường được giải quyết bằng cách tạo một component “controlled”. Cũng giống DOM <input> nhận cả value và prop onChange, vậy nên TemperatureInput nhận cả temperature và prop onTemperatureChange từ component cha Calculator.

Bây giờ, khi TemperatureInput muốn cập nhật nhiệt độ, sẽ gọi this.props.onTempertatureChange:

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

Note:
Trong component tùy chỉnh không có ý nghĩa đặc biệt cho temperature hoặc tên prop của onTemperatureChange. Chúng ta có thể gọi chúng bằng tên khác, như là đặt tên theo convention thông thường như valueonChange.

Component cha Calculator sẽ cung cấp cả hai prop temperature onTemperatureChange .Component cha sẽ xử lý các thay đổi bằng cách sửa local state của nó, rồi sau đó hiển thị lại cả hai đầu vào với giá trị mới. Sau này chúng ta sẽ xem cách triển khai Calculator mới.

Trước khi tìm hiểu thay đổi của Calculator, hãy xem lại những điều chúng ta thay đổi trên component TemperatureInput. Chúng ta phải loại bỏ local state thay vào đó là truyền vào this.state.temperature, giờ chúng ta có thể đọc this.props.temperature. Thay vì gọi this.setState() khi chúng ta muốn tạo thay đổi, giờ chúng ta gọi this.props.onTemperatureChange(), sẽ được Calculator cung cấp:

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

Bây giờ hãy tìm hiểu component Calculator.

Chúng ta sẽ lưu đầu vào hiện tại của temperaturescale trong local state của nó. Đây chính là state chúng ta “lifted up” từ các đầu vào, vào sẽ được sử dụng như “source of truth” cho cả hai. Để hiển thị cả hai đầu vào thì đây là đại diện tối thiếu cho tất cả dữ liệu chúng ta cần.

Ví dụ, nếu chúng ta nhập 37 vào đầu vào celsius, state của component Calculator sẽ là:

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

Nếu sau đó chúng ta thay đổi trường fahrenheit là 212, state của Calculator sẽ là:

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

Chúng ta có thể phải lưu giá trị của cả hai đầu vào nhưng hóa ra là không cần thiết. Đã là đủ khi lưu giá trị của đầu vào đã thay đổi gần nhất, và phạm vi mà nó đại diện. Chúng ta có thể suy ra giá trị đầu vào còn lại chỉ dựa trên temperaturescale hiện tại.

Các đầu vào sẽ đồng bộ bởi vì giá trị của chúng được tính từ cùng một state:

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

Bây giờ, bạn thay đổi đầu vào là gì không quan trọng, this.state.temperaturethis.state.scale trong Calculator đã được cập nhật. Một trong những đầu vào nhận giá trị như vậy, đầu vào của người dùng được lưu trữ, và giá trị đầu vào còn lại luôn được tính lại dựa trên nó.

Hãy xem lại những gì đã xảy ra khi bạn thay đổi một đầu vào:

  • React gọi hàm được chỉ định như onChange trên DOM <input>.Trong trường hợp của chúng ta, dây là phương thức handleChange trong component TemperatureInput.
  • Phương thức handleChange trong component TemperatureInput gọi this.props.onTemperatureChange() với giá trị mong muốn mới. Component cha Calculator cung cấp props, có cả onTemperatureChange.
  • Trước khi được hiển thị, Calculator được chỉ định là onTemperatureChange của celsius TemperatureInput là phương thức handleCelsiusChange của Calculator, và onTemperatureChange của fahrenheit TemperatureInput là phương thức handleFahrenheitChange của Calculator. Vậy nên cả hai phương thức của Calculator được gọi dựa trên đầu vào nào mà ta đã thay đổi.
  • Bên trong các phương thức, component Calculator yêu cầu React hiển thị lại chính nó bằng cách gọi this.setState() với giá trị đầu vào mới và phạm vi hiện tại của đầu vào mà chúng ta vừa thay đổi.
  • React gọi phương thức render của component Calculator để biết giao diện nên hiển thị gì. Giá trị của cả hai đầu vào được tính lại dựa trên nhiệt độ hiện tại và phạm vi đang được sử dụng. Việc chuyển đổi nhiệt độ được thực hiện ở đây.
  • React gọi các phương thức render của các component TemperatureInput riêng với props mới của chúng được chỉ định bởi Calculator. Chúng học giao diện nên hiển thị cái gì.
  • React gọi phương thức render của component BoilingVerdict, truyền nhiệt độ celsius như là các props.
  • React DOM cập nhật DOM với nhiệt độ sôi và để khớp với giá trị đầu vào mong muốn. Đầu vào chúng ta vừa chỉnh sửa nhận giá trị hiện tại, và đầu vào còn lại được cập nhật nhiệt độ sau khi chuyển đổi.

Mỗi cập nhật đều trải qua các bước giống nhau nên các đầu vào luôn đồng bộ.

Bài học

Trong ứng dụng React chỉ nên có một “source of truth” cho mọi dữ liệu thay đổi.Thường thì, state được thêm vào đầu tiên cho component cần nó để hiển thị. Tiếp theo, nếu các component khác cũng cần nó, bạn có thể chuyển nó lên component cha chung gần nhất. Thay vì cố gắng đồng bộ state giữa các component, bạn nên
dùng “top-down data flow”.

Chuyển state yêu cầu viết code “boilerplate” nhiều hơn phương pháp binding hai chiều, nhưng vì tiện lợi, nó tốn ít sức hơn trong việc tìm và tách các bugs. Vì bất kì state “sống” trong vài component và tự các component có thể thay đổi nó, nơi ở cho bugs giảm một cách rõ rệt. Ngoài ra, bạn có thể thực hiện vài thay đổi logic để từ chối hoặc thay đổi đầu vào của người dùng.

Nếu thứ gì đó có thể có nguồn gốc từ props hay state, rõ ràng chúng không nên được ở trong state. Ví dụ, thay vì lưu cả hai celsiusValuefahrenheitValue, chúng ta chỉ lưu giá trị temperature chỉnh sửa cuối cùng và scale của nó. Giá trị của đầu vào input còn lại có thể luôn được tính toán từ chúng trong phương thức render(). Điều này làm chúng ta xóa hay áp dụng làm tròn cho trường khác mà không làm mất đi độ chính xác trong đầu vào của người dùng.

Khi bạn thấy điều gì đó sai trong giao diện, bạn có thể sử dụng React Developer Tools để quan sát props và duyệt cây dữ liệu cho đến khi bạn tìm được component chịu trách nhiệm về cập nhật dữ liệu. Điều này giúp bạn truy tìm bugs đến nguồn của chúng:
image