Chuyển state lên trên

Thông thường, khi một dữ liệu thay đổi nó sẽ ảnh hưởng tới nhiều component cùng lúc. State được khuyến khích chia sẻ ở component cha của chúng. Hãy cùng xem nó được ứng dụng trong thực tế như thế nào.

Chúng ta sẽ xây dựng một ứng dụng tính nhiệt độ. Nó sẽ cho người dùng biết nước có sôi ở nhiệt độ cho trước hay không.

Chúng ta sẽ bắt đầu với một component BoilingVerdict. Nhiệt độ celsius được truyền vào component này như là một prop, nó cũng sẽ hiển thị thông báo nếu như nhiệt độ đủ làm sôi nước hay không:

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 ra một component khác là Calculator. Nó sẽ có một <input> để người dùng nhập dữ liệu, và giữ giá trị đó trong this.state.temperature.

Thêm vào đó, nó sẽ tạo ra component BoilingVerdict với giá trị hiện tại của input.

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

Xem trên CodePen

Thêm Input thứ hai

Yêu cầu mới là bên cạnh input cho Celsius, chúng ta cần cung cấp thêm một input cho Fahrenheit, và chúng phải đồng bộ hoá.

Chúng ta có thể bắt đầu bằng việc tách một component TemperatureInput ra từ Calculator. Nó sẽ được truyền vào một prop mới tên là scale mang một trong hai giá trị là "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 có thể tạo ra hai input riêng biệt cho nhiệt độ:

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

Xem thêm trên CodePen

Bây giờ, chúng ta đã có hai input, nhưng khi bạn nhập giá trị nhiệt độ vào một trong hai input, input còn lại không được cập nhật. Điều này chưa thoả mãn yêu cầu là hai input đồng bộ hoá.

Chúng ta cũng chưa thể hiển thị BoilingVerdict từ Calculator. Calculator không biết giá trị nhiệt độ hiện thời bởi vì nó bị ẩn đi bên trong component TemperatureInput.

Viết các hàm để chuyển đổi

Đầu tiên chúng ta sẽ viết hai hàm để chuyển đổi 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;
}

Có hai hàm để chuyển đổi nhiệt độ. Chúng ta sẽ viết thêm một hàm khác nhận các tham số truyền vào là một chuỗi temperature và một hàm chuyển đổi, sau đó nó sẽ trả lại một chuỗi. Chúng ta sẽ sử dụng nó để tính toán giá của của một input dựa trên input còn lại.

Nó sẽ trả lại một chuỗi rỗng nếu như tham số temperature không hợp lệ, và nó sẽ làm tròn kết quả với ba chữ số thập phân.

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

Trong ví dụ, tryConvert('abc', toCelsius) trả về một chuỗi rỗng, và tryConvert('10.22', toFahrenheit) cho kết quả là '50.396'

Chuyển state lên trên

Hiện tại, hai component TemperatureInput lưu trữ giá trị của chúng một cách riêng rẽ trong state cục bộ:

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 hai input này được đồng bộ hoá. Khi chúng ta cập nhật nhiệt độ cho Celsius input, Fahrenheit input cũng phải được cập nhật nhiệt độ sau khi đã chuyển đổi và ngược lại.

Trong React, chia sẻ state được thực hiện bằng cách chuyển nó lên component cha gần nhất cần state này. Việc này được gọi là “chuyển state lên trên”. Chúng ta sẽ xoá state cục bộ từ TemperatureInput và chuyển nó tới Calculator.

Nếu Calculator nắm giữ state chia sẻ, nó sẽ trở thành “nguồn dữ liệu tin cậy” về nhiệt độ hiện tại cho cả hai input. Nó có thể cung cấp cho cả hai những giá trị phù hợp cho chúng. Vì các prop của cả hai component TemperatureInput đều đến từ cùng một component cha Calculator, nên chúng luôn luôn được đồng bộ hoá.

Hãy xem nó hoạt động thế nào qua từng bước.

Đầu tiên, chúng ta sẽ thay thế this.state.temperature với this.props.temperature trong component TemperatureInput. Hiện thời, hãy giả định rằng this.props.temperature đã tồn tại, mặc dù sau này nó sẽ truyền xuống từ component Calculator.

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

Chúng ta biết rằng props không thể thay đổi. Lúc trước, khi temperature ở trong state cục bộ, component TemperatureInput chỉ cần gọi this.setState() để thay đổi nó. Tuy nhiên, khi temperature được truyền vào từ component cha như là một prop, thì TemperatureInput không có quyền kiểm soát nó nữa.

Trong React, điều này được giải quyết bằng cách tạo ra một component “kiểm soát”. Cũng tương tự như DOM <input> chấp nhận thuộc tính valueonChange, thì tuỳ chỉnh TemperatureInput có thể chấp nhận cả temperatureonTemperatureChange props từ component cha Calculator.

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

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

Chú ý:

Tên của temperature hoặc onTemperatureChange prop không mang một ý nghĩa đặc biệt nào trong những component tuỳ chỉnh này. Chúng ta có thể gọi chúng bằng những cái tên khác, theo một cách phổ biến hơn, như đặt tên chúng là valueonChange.

Prop onTemperatureChange sẽ được truyền vào cùng với prop temperature bởi component cha Calculator. Khi prop thay đổi, nó sẽ sửa lại chính state cục bộ của nó, vì thế sẽ tạo lại cả hai input với các giá trị mới. Chúng ta sẽ cùng xem component Calculator được triển khai lại sau đây.

Trước khi tìm hiểu những thay đổi trong Calculator, hãy cùng điểm lại những thay đổi trong component TemperatureInput. Chúng ta đã xoá đi state cục bộ, và sử dụng this.props.temperature thay vì this.state.temperature. Khi chúng ta muốn thay đổi, việc gọi hàm this.setState() được thay bằng hàm this.props.onTemperatureChange() từ component cha 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>
    );
  }
}

Bây giờ hãy cùng chuyển sang component Calculator.

Chúng ta sẽ lưu trữ giá trị hiện thời của temperaturescale từ input vào trong state cục bộ của nó. Đây là state mà chúng ta muốn chuyển lên từ những input, và nó sẽ được sử dụng như là “nguồn dữ liệu tin cậy” cho cả hai. Nó là đại diện tối thiểu cho tất cả những dữ liệu chúng ta cần biết để tạo ra cả hai input.

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

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

Nếu chúng ta nhập 212 cho Fahrenheit, state của component Calculator sẽ là:

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

Chúng ta có thể lưu trữ giá trị của cả hai input nhưng điều này là không cần thiết. Chúng ta chỉ cần lưu lại giá trị của input được thay đổi gần nhất, và đơn vị của nó. Chúng ta có thể tính ra giá trị của input còn lại dựa trên giá trị của temperaturescale hiện tại.

Các giá trị input sẽ được đồng bộ hoá bởi nó được tính toán 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>
    );
  }
}

làm thử trên CodePen

Bây giờ, bạn có thể thay đổi bất kì input nào, thì this.state.temperaturethis.state.scale trong component Calculator sẽ được cập nhật. Giá trị của một input sẽ được giữ nguyên, như giá trị người dùng đã nhập vào, và giá trị của input còn lại sẽ được tính toán dựa trên giá trị đó.

Hãy cùng điểm lại điều gì sẽ xảy ra khi bạn thay đổi giá trị của một input:

  • React sẽ gọi hàm onChange tương ứng trên DOM <input>. trong trường hợp này, đây là hàm handleChange trong component TemperatureInput.
  • Hàm handleChange trong component TemperatureInput được gọi this.props.onTemperatureChange() và truyền vào một giá trị mới. Các props của nó, bao gồm onTemperatureChange, sẽ được component cha Calculator cung cấp.
  • Trước đây, khi nó được tạo ra, component Calculator đã được lập trình rằng onTemperatureChange của Celsius TemperatureInput là hàm handleCelsiusChange của component Calculator, và onTemperatureChange của Fahrenheit TemperatureInput là hàm handleFahrenheitChange từ component Calculator. Vì thế nên một trong hai hàm của Calculator sẽ được gọi dựa trên input nào bị thay đổi.
  • Bên trong các hàm này, component Calculator sẽ yêu cầu React để tạo lại chính nó bằng cách gọi this.setState() với giá trị mới từ input và đơn vị hiện tại của input bị thay đổi.
  • React gọi hàm render từ component Calculator để xem giao diện người dùng trông như thế nào. Giá trị của cả hai input sẽ được tính toán lại dựa trên nhiệt độ hiện thời và đơn vị đo đang được sử dụng. Nhiệt độ được chuyển đổi tại đây.
  • React gọi hàm render của mỗi component TemperatureInput riêng với giá trị mới của props được truyền từ Calculator. Nó sẽ hiểu được giao diện người dùng như thế nào.
  • React gọi hàm render từ component BoilingVerdict, truyền nhiệt độ bằng Celsius như là một props của nó.
  • React DOM cập nhật DOM với nhiệt độ sôi và để phù hợp với những giá trị mong muốn của input. Input chúng ta vừa thay đổi sẽ nhận giá trị hiện thời, và những input khác được cập nhật với nhiệt độ sau khi chuyển đổi.

Tất cả những cập nhật đi qua cùng một lộ trình nên các input sẽ luôn được đồng bộ hoá.

Bài học rút ra

Cần có một nguồn “dữ liệu đáng tin cậy” cho bất kì một dữ liệu nào cần thay đổi trong ứng dụng React. Thường thì, state là cái đầu tiên mà component cần thêm vào để có thể tạo ra. Vì thế, nếu các component khác cũng cần nó, bạn có thể chuyển nó lên component cha gần nhất. Thay vì thử đồng bộ hoá state giữa những component khác nhau, bạn nên dựa trên luồng dữ liệu từ trên xuống dưới

Chuyển state liên quan tới thêm vào nhiều code “chuẩn” hơn là phương pháp ràng buộc 2 chiều, nhưng nó có một ích là việc tìm và cô lập các lỗi sẽ dễ dàng hơn. Bởi vì bất kì một state “tồn tại” trong một vài component và chỉ mình component đó có thể thay đổi nó, phạm vi tìm kiếm lỗi sẽ giảm đi một cách đáng kể. Thêm vào đó, bạn có thể thêm vào bất kì tuỳ chỉnh logic nhằm từ chối hoặc chuyển đổi giá trị người dùng nhập vào.

Nếu một vài thứ có thể bắt nguồn từ props hoặc state, nó có thể không nên là state. Ví dụ, thay vì lưu trữ cả celsiusValuefahrenheitValue, chúng ta sẽ lưu trữ giá trị được thay đổi gần nhất của temperaturescale của nó. Giá trị của input khác có thể được tính toán từ chúng trong hàm render(). Nó sẽ cho phép chúng ta dọn dẹp hoặc áp dụng để làm tròn giá trị của trường khác mà không làm mất đi tính chính xác của giá trị người dùng nhập vào.

<<<<<<< HEAD Khi bạn thấy giao diện người dùng không chính xác, bạn có thể dùng Công cụ phát triển React để kiểm tra props và di chuyển lên theo cây component cho tới khi bạn có thể tìm thấy component chịu trách nhiệm cho việc cập nhật state. Nó sẽ giúp bạn theo dõi nguồn gốc của các lỗi: ======= When you see something wrong in the UI, you can use React Developer Tools to inspect the props and move up the tree until you find the component responsible for updating the state. This lets you trace the bugs to their source:

81124465ac68335b2e3fdf21952a51265de6877f

Monitoring State in React DevTools