Lifting State Up trong Reactjs
Bài đăng này đã không được cập nhật trong 5 năm
Thông thường, một số Component cần phản ứng với cùng một dữ liệu thay đổi. Trong trường hợp đó ta có thể nghĩ đến việc nâng trạng thái được chia sẻ lên đến Component cha chung gần nhất. Hãy cùng xem cách thức hoạt động của nó.
Sau đây chúng ta sẽ thử tạo ra một máy tính nhiệt độ để tính toán xem nước có sôi ở nhiệt độ nhất định hay không.
Chúng tôi sẽ bắt đầu với một Component được gọi là BoilingVerdict. Nó chấp nhận nhiệt độ celsius như một prop và in xem nó có đủ để đun sôi nước 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 tôi sẽ tạo ra một thành phần gọi là Calculator. Nó sẽ render input cho phép ta nhập nhiệt độ và giữ giá trị của nó trong this.state.temperature
Ngoài ra, nó hiển thị BoilingVerdict cho giá trị đầu vào 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>
);
}
}
Adding a Second Input
Yêu cầu mới của chúng ta là, ngoài đầu vào Celsius, chúng ta cũng cung cấp đầu vào Fahrenheit và giữ chúng đồng bộ với nhau.
Chúng ta có thể bắt đầu bằng cách tách một Component TemperatureInput từ Calculator. Chúng ta sẽ thêm một prop scale mới cho nó có thể 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 để render 2 input temperature inputs khác nhau.
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
Hiện tại chúng ta có hai input, nhưng khi bạn nhập nhiệt độ vào một trong số chúng, thì cái kia 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: chúng ta muốn giữ chúng đồng bộ.
Chúng ta cũng không thể hiển thị BoilingVerdict từ Calculator. Calculator không biết nhiệt độ hiện tại vì nó được ẩn bên trong TemperatureInput.
Writing Conversion Functions
Đầu tiên, chúng ta sẽ viết hai function để 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;
}
Hai hàm này để chuyển đổi số. Chúng ta sẽ viết một hàm khác lấy chuỗi temperature và hàm chuyển đổi làm đối số và trả về 1 chuỗi. Chúng ta sẽ sử dụng nó để tính giá trị của một input dựa trên input khác.
Nó trả về một chuỗi rỗng ở temperature không hợp lệ và nó giữ output được làm tròn đến vị 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 trống và tryConvert ('10 .22 ', toFahrenheit) trả về '50 .394'.
Lifting State Up
Hiện tại, cả hai Component của TemperatureInput đều độc lập giữ giá trị của chúng trong local state:
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 tôi muốn hai input này đồng bộ với nhau. Khi chúng ta cập nhật input Celsius, input Fahrenheit sẽ phản ánh nhiệt độ được chuyển đổi và ngược lại.
Trong React, việc chia sẻ state được thực hiện bằng cách di chuyển nó lên tổ tiên chung gần nhất của các Component cần nó. Điều này được gọi là "lifting state up". Chúng ta sẽ xóa local state khỏi TemperatureInput và thay vào đó chuyển nó sang Calculator .
Nếu Calculator sở hữu shared state, nó sẽ trở thành “source of truth” về nhiệt độ hiện tại ở cả hai input. Nó có thể hướng dẫn cả hai để có các giá trị đồng nhất với nhau. Vì props của cả hai components TemperatureInput đến từ cùng một component cha Calculator , hai input sẽ luôn đồng bộ.
Hãy để xem cách làm việc này từng bước một.
Đầu tiên, chúng ta sẽ thay thế this.state.temperature thành this.props.temperature trong component TemperatureInput .
Bây giờ, hãy để this.props.temperature tồn tại, mặc dù chúng ta sẽ cần pass 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 prop là read-only. Khi temperature ở trong local state , TemperatureInput chỉ có thể gọi this.setState () để thay đổi nó. Tuy nhiên, bây giờ khi temperature đến từ parent như một prop, thì temperature không kiểm soát được nó.
Trong React, điều này thường được giải quyết bằng cách tạo ra một component “controlled”. Giống như DOM <input> chấp nhận cả các prop value và onChange, do đó tùy chỉnh TemperatureInput có thể chấp nhận cả hai prop nhiệt độ và onTemperatureChange từ Calculator cha của nó.
Bây giờ, khi TemperatureInput muốn cập nhật nhiệt độ của nó, nó gọi this.props.onTemperatureChange:
handleChange(e) {
// Before: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value);
// ...
Prop onTemperatureChange sẽ được cung cấp cùng với prop temperature bởi component Calculator cha. Nó sẽ xử lý thay đổi bằng cách sửa đổi local state của chính nó, do đó re-rendering lại cả input với các giá trị mới. Chúng tôi sẽ xem xét cách thực hiện Calculator sớm.
Trước khi đi sâu vào các thay đổi trong Calculator, hãy recap lại các thay đổi của chúng ta với component TemperatureInput . Chúng tôi đã xóalocal state khỏi nó và thay vì đọc this.state.temperature, bây giờ chúng ta đọc this.props.temperature. Thay vì gọi this.setState () khi chúng tôi muốn thực hiện thay đổi, bây giờ chúng ta gọi this.props.onTemperatureChange(), mà được cung cấp bởi 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 chuyển sang component Calculator .
Chúng ta sẽ lưu trữ temperature và scale đầu vào hiện tại ở local state của chính nó. Đây là state mà chúng ta đã "lifted up" từ các input, nó sẽ đóng vai trò là “source of truth” cho cả hai. Đây là đại diện tối thiểu của tất cả các dữ liệu chúng ta cần biết để render cả hai input.
Ví dụ: nếu chúng ta nhập 37 vào input Celsius, state của component Celsius sẽ là:
{
temperature: '37',
scale: 'c'
}
Nếu sau dó chúng ta chỉnh sửa trường Fahrenheit thành 212, state của Calculator sẽ là:
{
temperature: '212',
scale: 'f'
}
Chúng ta có thể đã lưu trữ giá trị của cả input nhưng thực ra là không cần thiết. Chỉ cần lưu trữ giá trị của input thay đổi gần nhất và scale mà nó đại diện. Sau đó chúng ta có thể suy ra giá trị của input kia dựa trên temperature hiện tại và tỷ lệ hiện tại.
Các input sẽ đồng bộ vì các 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ất kể bạn chỉnh sửa input nào, this.state.temperature và this.state.scale trong Calculator sẽ được cập nhật. Một trong những input nhận được giá trị như vậy, vì vậy mọi input của người dùng đều được giữ nguyên và giá trị input khác luôn được tính toán lại dựa trên giá trị đó.
Hãy tóm tắt lại những gì xảy ra khi ta chỉnh sửa input:
-
React gọi hàm được chỉ định là onChange trên DOM <input>. Trong trường hợp này của chúng ta, đâ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. Các prop của nó, bao gồm onTemperatureChange, được cung cấp bởi component cha Calculator.
-
Trước khi nó được render, Calculator đã chỉ định rằng onTemperatureChange của Celsius TemperatureInput là method handleCelsiusChange của Calculator và onTemperatureChange của Fahrenheit là method handleFahrenheitChange của Calculator. Vì vậy, một trong hai method Calculator này được gọi tùy thuộc vào inputmà chúng ta đã chỉnh sửa.
-
Bên trong các phương thức này, component Calculator yêu cầu React tự re-render lại bằng cách gọi this.setState() với giá trị input mới và scale hiện tại của input mà chúng ta vừa chỉnh sửa.
-
React gọi phương thức render component Calculator để tìm hiểu UI sẽ như thế nào. Các giá trị của cả hai input được tính toán lại dựa trên temperature hiện tại và active scale. 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 rẽ với các prop mới được chỉ định bởi Calculator. Nó sẽ tìm hiểu UI sẽ trông như thế nào.
-
React gọi phương thức render của component BoilingVerdict, truyền nhiệt độ bằng Celsius làm prop của nó.
-
React DOM cập nhật DOM với boiling verdict và để khớp với các 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 của nó và đầu vào khác được cập nhật theo nhiệt độ sau khi chuyển đổi.
Mọi cập nhật đều trải qua các bước tương tự để các input luôn đồng bộ.
Lessons Learned
-
Cần có một “source of truth” cho bất kỳ dữ liệu nào thay đổi trong ứng dụng React. Thông thường, state đầu tiên được thêm vào component cần nó để render. Sau đó, nếu các component khác cũng cần nó, ta có thể nâng nó lên tổ tiên chung gần nhất của chúng. Thay vì cố gắng đồng bộ hóa trạng thái giữa các component khác nhau, ta nên dựa vào luồng dữ liệu từ trên xuống - top-down data flow.
-
Việc nâng trạng thái liên quan đến việc viết nhiều code hơn so với cách tiếp cận two-way binding, nhưng lợi ích là sẽ mất ít công sức hơn để find và isolate các bug. Vì bất kỳ state nào cũng sống trong một vài component và chỉ riêng compoent đó có thể thay đổi nó, diện tích bề mặt của các bug đã giảm đi rất nhiều. Ngoài ra, ta có thể thực hiện bất kỳ logic tùy chỉnh nào để từ chối hoặc chuyển đổi đầu vào của người dùng.
-
Nếu một cái gì đó có thể được bắt nguồn từ prop hoặc state, có lẽ nó không nên đặt ở state. Ví dụ, thay vì lưu trữ cả celsiusValue và fahrenheitValue, chúng ta chỉ lưu trữ temperature được chỉnh sửa cuối cùng và scale của nó. Giá trị của đầu vào khác luôn có thể được tính từ chúng trong phương thức render (). Điều này cho phép chúng ta xóa hoặc làm tròn cho trường khác mà không mất bất kỳ độ chính xác nào trong input của người dùng.
-
Khi ta thấy có gì đó không ổn trong UI, ta có thể sử dụng React Developer Tools để kiểm tra prop và di chuyển lên cây cho đến khi bạn tìm thấy component chịu trách nhiệm cập nhật state. Điều này cho phép bạn theo dõi các bug đến nguồn của chúng
All rights reserved