基本方案
我有一个受React文本框控制的组件,该组件的onChange
事件最终触发了对服务器端API的AJAX调用。该调用的结果可能会更改文本框的值。因此,在AJAX调用的回调中有一个对setState
的调用。
基本问题
当在AJAX调用完成之前对输入进行更改时,我很难找到一种方法来平稳,一致地更新此值。到目前为止,我尝试了两种方法。主要区别在于如何最终会发生AJAX调用。
方法1
我的第一次尝试使用立即输入的数据调用setState
,最终触发了重新渲染和componentDidUpdate
。然后,后者在所讨论的状态数据不同的情况下进行AJAX调用。
handleChange(event) {
const inputTextValue = event.target.value;
setState({ inputText: inputTextValue }); // will trigger componentDidUpdate
}
componentDidUpdate(lastProps, lastState) {
const inputTextValue = this.state.inputText;
if (lastState.inputText !== inputTextValue) { // string comparison to prevent infinite loop
$.ajax({
url: serviceUrl,
data: JSON.stringify({ inputText: inputTextValue })
// set other AJAX options
}).done((response) => {
setState({ inputText: response.validatedInputTextValue }); // will also trigger componentDidUpdate
});
}
}
此方法的优点是快速更新状态以反映用户的立即输入。但是,如果快速进行两个输入,则会发生如下序列:
事件处理程序1使用值'1'
触发
setState
的{{1}} '1'
由重新渲染触发componentDidUpdate
与上一个值不同,所以'1'
进行的AJAX调用1 正在进行AJAX调用1时,事件2处理程序将以值'1'
触发
'12'
的{{1}} setState
由重新渲染触发'12'
与componentDidUpdate
不同,所以'12'
进行的AJAX调用2 正在进行AJAX调用2时,AJAX调用1返回值'1'
'12'
的{{1}} '1'
由重新渲染触发setState
与'1'
不同,所以componentDidUpdate
进行的AJAX调用3 正在进行AAJX调用3时,AJAX调用2返回值'1'
...
TL; DR尽管'12'
中有最后一个状态检查,但仍发生无限循环,因为两个重叠的AJAX调用将交替的值赋给'1'
。
方法2
为解决这个问题,我的第二种方法简化了系统,并直接从事件处理程序进行AJAX调用:
'12'
但是,如果执行此操作,则受控组件值的立即更新将停止,直到AJAX调用完成并调用componentDidUpdate
。它简单而稳定,只需设置状态并渲染一次;但是在等待AJAX调用时停止输入是不好的UX。第一种方法至少类似于(过度)立即更新。
方法3?
在等待答案的同时,我将实现以下方法3,该方法基本上是方法1的增强版本:
setState
问题
我对React还是比较陌生的。我想象其他人也遇到过这个用例,但是我很难找到解决方案。我想要一种设置状态并立即更新组件值的方法, a 方法1,并且仍然具有方法2的数据稳定性。方法3似乎很有前途,但有点太复杂了。是否有一种优雅的图案可以实现这一目标?
答案 0 :(得分:1)
建议的解决方案(#1)有一个很大的警告: 您无法保证第一个请求将在第二个请求之前返回。
为了避免这种情况,您可以采用以下方法之一:
您选择的组件:
const Select = props => {
const {disabled, options} = props;
return (<select disabled={disabled}>
{ options.map(item => <option value={item}> {item} </option> }
</select>)
}
您的逻辑组件:
class LogicalComponent extends React.Component {
constructor(props) {
this.state = {
selectDisabled: false;
options: ['item1', 'item2', 'item3'],
inputText: ''
}
}
handleChange(event) {
const inputTextValue = event.target.value;
setState({ inputText: inputTextValue }); // will trigger componentDidUpdate
}
componentDidUpdate(lastProps, lastState) {
const inputTextValue = this.state.inputText;
if (lastState.inputText !== inputTextValue) { // string comparison to prevent infinite loop
// disabling the select until the request finishes
this.setState({ selectDisabled: true });
$.ajax({
url: serviceUrl,
data: JSON.stringify({ inputText: inputTextValue })
// set other AJAX options
}).done((response) => {
//re-enabling it when done
setState({ inputText: response.validatedInputTextValue, selectDisabled: false }); // will also trigger componentDidUpdate
// don't forget to enable it when the request is failed
}).fail(res => this.setState({selectDisabled: false}));
}
}
render() {
const { selectDisabled, options, inputText } = this.state;
return <>
<Select disabled={selectDisabled} options={options} />
<input type="text" value={inputText}/>
<>
}
}
如果您正在处理AJAX请求,则可以取消它并触发新的请求。这样可以确保仅返回最近的请求。
class LogicalComponent extends React.Component {
constructor(props) {
this.requestInProgress = null;
this.state = {
options: ['item1', 'item2', 'item3'],
inputText: ''
}
}
handleChange(event) {
const inputTextValue = event.target.value;
setState({ inputText: inputTextValue }); // will trigger componentDidUpdate
}
componentDidUpdate(lastProps, lastState) {
const inputTextValue = this.state.inputText;
if (lastState.inputText !== inputTextValue) { // string comparison to prevent infinite loop
// checking to see if there's a request in progress
if(this.requestInProgress && this.requestInProgress.state() !== "rejected") {
// aborting the request in progress
this.requestInProgress.abort();
}
// setting the current requestInProgress
this.requestInProgress = $.ajax({
url: serviceUrl,
data: JSON.stringify({ inputText: inputTextValue })
// set other AJAX options
}).done((response) => {
setState({ inputText: response.validatedInputTextValue }); // will also trigger componentDidUpdate
// don't forget to enable it when the request is failed
})
}
}
render() {
const { selectDisabled, options, inputText } = this.state;
return <>
<Select disabled={selectDisabled} options={options} />
<input type="text" value={inputText}/>
<>
}
}
答案 1 :(得分:0)
我最终回到了方法1,但是对输入进行了反跳以消除重叠。
特别是,我使用了Lodash到debounce的一种方法,该方法是从componentDidUpdate
中的代码重构而来的,该方法实际上进行了AJAX调用:
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.validateInput = this.validateInput.bind(this);
this.validateInputDebounced = _.debounce(this.validateInput, 100);
}
handleChange(event) {
const inputTextValue = event.target.value;
setState({ inputText: inputTextValue }); // will trigger componentDidUpdate
}
componentDidUpdate(lastProps, lastState) {
const inputTextValue = this.state.inputText;
if (lastState.inputText !== inputTextValue) { // string comparison to prevent infinite loop
validateInputDebounced(inputTextValue);
}
}
validateInput(newInputTextValue) {
$.ajax({
url: serviceUrl,
data: JSON.stringify({ inputText: newInputTextValue })
// set other AJAX options
}).done((response) => {
setState({ inputText: response.validatedInputTextValue }); // will also trigger componentDidUpdate
});
}
这部分基于此处所做的工作:https://medium.com/@platonish/debouncing-functions-in-react-components-d42f5b00c9f5
在进一步检查中,该方法也不足。如果AJAX调用的时间长于防抖时间,则请求可能会再次乱序解决。我想我会保留去抖逻辑以节省网络流量。但是接受的解决方案取消了先前的正在进行的请求,就足以解决该问题。