我试图理解一些有些“神奇”行为的根本原因,我看到我无法完全解释,而且在阅读ReactJS源代码时并不明显。
在响应输入上的setState
事件时同步调用onChange
方法时,一切都按预期工作。输入的“新”值已经存在,因此DOM实际上没有更新。这是非常需要的,因为它意味着光标不会跳转到输入框的末尾。
但是,当运行具有完全相同结构但是异步调用setState
的组件时,输入的“新”值似乎不存在,从而导致ReactJS实际上触摸DOM,使光标跳转到输入的末尾。
显然,在异步情况下,正在进行干预以将输入“重置”回其先前的value
,这在同步情况下并非如此。这是什么机制?
同步示例
var synchronouslyUpdatingComponent =
React.createFactory(React.createClass({
getInitialState: function () {
return {value: "Hello"};
},
changeHandler: function (e) {
this.setState({value: e.target.value});
},
render: function () {
var valueToSet = this.state.value;
console.log("Rendering...");
console.log("Setting value:" + valueToSet);
if(this.isMounted()) {
console.log("Current value:" + this.getDOMNode().value);
}
return React.DOM.input({value: valueToSet,
onChange: this.changeHandler});
}
}));
请注意,代码将登录render
方法,打印出实际DOM节点的当前value
。
在两个Ls“Hello”之间键入“X”时,我们看到以下控制台输出,并且光标保持在预期的位置:
Rendering...
Setting value:HelXlo
Current value:HelXlo
异步示例
var asynchronouslyUpdatingComponent =
React.createFactory(React.createClass({
getInitialState: function () {
return {value: "Hello"};
},
changeHandler: function (e) {
var component = this;
var value = e.target.value;
window.setTimeout(function() {
component.setState({value: value});
});
},
render: function () {
var valueToSet = this.state.value;
console.log("Rendering...");
console.log("Setting value:" + valueToSet);
if(this.isMounted()) {
console.log("Current value:" + this.getDOMNode().value);
}
return React.DOM.input({value: valueToSet,
onChange: this.changeHandler});
}
}));
这与上述内容完全相同,只是对setState
的调用是在setTimeout
回调中。
在这种情况下,在两个Ls之间键入X会产生以下控制台输出,并且光标会跳转到输入的末尾:
Rendering...
Setting value:HelXlo
Current value:Hello
为什么会这样?
我理解React的Controlled Component概念,因此忽略用户对value
的更改是有道理的。但看起来value
实际上已更改,然后显式重置。
显然,同步调用setState
可确保在重置之前生效,而在重置之后在任何其他时间调用{em>} ,强制重新渲染。
这实际上是发生了什么吗?
JS Bin示例
答案 0 :(得分:68)
这里发生了什么。
setState({value: 'HelXlo'})
稍后......
setState({value: 'HelXlo'})
是的,这里有点神奇。 React调用在事件处理程序之后同步呈现。这是避免闪烁的必要条件。
答案 1 :(得分:5)
使用defaultValue而不是value解决了我的问题。我不确定这是否是最佳解决方案,例如:
自:
return React.DOM.input({value: valueToSet,
onChange: this.changeHandler});
要:
return React.DOM.input({defaultValue: valueToSet,
onChange: this.changeHandler});
JS Bin示例
答案 2 :(得分:4)
如上所述,这将是使用受控组件时的一个问题,因为React正在更新输入的值,而不是相反(React拦截更改请求并更新其状态以匹配)。
FakeRainBrigand的答案很棒,但我注意到,并不完全是一个同步或异步的更新导致输入以这种方式运行。如果您正在执行某些操作,例如应用掩码来修改返回的值,则还可能导致光标跳到行尾。不幸的是(?)这就是React在受控输入方面的工作原理。但它可以手动解决。
在反应github问题上有一个great explanation and discussion of this,其中包含JSBin solution到Sophie Alpert的链接[手动确保光标保留在应该的位置]
这是使用<Input>
组件实现的,如下所示:
var Input = React.createClass({
render: function() {
return <input ref="root" {...this.props} value={undefined} />;
},
componentDidUpdate: function(prevProps) {
var node = React.findDOMNode(this);
var oldLength = node.value.length;
var oldIdx = node.selectionStart;
node.value = this.props.value;
var newIdx = Math.max(0, node.value.length - oldLength + oldIdx);
node.selectionStart = node.selectionEnd = newIdx;
},
});
答案 3 :(得分:2)
这不是一个答案,而是一种可能的方法来缓解这个问题。它为React输入定义了一个包装器,它通过本地状态Shim同步管理值更新;并对传出值进行版本化,以便仅应用从异步处理返回的最新值。
这是基于Stephen Sugden(https://github.com/grncdr)的一些工作,我更新了现代的React,并通过版本化值来改进,从而消除了竞争条件。
它不漂亮:)。
http://jsfiddle.net/yrmmbjm1/1/
var AsyncInput = asyncInput('input');
以下是组件需要如何使用它:
var AI = asyncInput('input');
var Test = React.createClass({
// the controlling component must track
// the version
change: function(e, i) {
var v = e.target.value;
setTimeout(function() {
this.setState({v: v, i: i});
}.bind(this), Math.floor(Math.random() * 100 + 50));
},
getInitialState: function() { return {v: ''}; },
render: function() {
{/* and pass it down to the controlled input, yuck */}
return <AI value={this.state.v} i={this.state.i} onChange={this.change} />
}
});
React.render(<Test />, document.body);
另一个尝试使控制组件代码的影响不那么令人讨厌的版本在这里:
http://jsfiddle.net/yrmmbjm1/4/
最终看起来像:
var AI = asyncInput('input');
var Test = React.createClass({
// the controlling component must send versionedValues
// back down to the input
change: function(e) {
var v = e.target.value;
var f = e.valueFactory;
setTimeout(function() {
this.setState({v: f(v)});
}.bind(this), Math.floor(Math.random() * 100 + 50));
},
getInitialState: function() { return {v: ''}; },
render: function() {
{/* and pass it down to the controlled input, yuck */}
return <AI value={this.state.v} onChange={this.change} />
}
});
React.render(<Test />, document.body);
¯\ _(ツ)_ /¯
答案 4 :(得分:1)
使用Reflux时遇到了同样的问题。状态存储在React组件之外,导致与setState
内的setTimeout
包装类似的效果。
@dule建议,我们应该同时使状态更改同步和异步。所以我已经准备好了一个HOC来确保值的变化是同步的 - 所以包装遭受异步状态变化的输入很酷。
注意:此HOC仅适用于具有类似<input/>
API的组件,但我想如果有这样的需要,可以直接使其更通用。 < / p>
import React from 'react';
import debounce from 'debounce';
/**
* The HOC solves a problem with cursor being moved to the end of input while typing.
* This happens in case of controlled component, when setState part is executed asynchronously.
* @param {string|React.Component} Component
* @returns {SynchronousValueChanger}
*/
const synchronousValueChangerHOC = function(Component) {
class SynchronousValueChanger extends React.Component {
static propTypes = {
onChange: React.PropTypes.func,
value: React.PropTypes.string
};
constructor(props) {
super(props);
this.state = {
value: props.value
};
}
propagateOnChange = debounce(e => {
this.props.onChange(e);
}, onChangePropagationDelay);
onChange = (e) => {
this.setState({value: e.target.value});
e.persist();
this.propagateOnChange(e);
};
componentWillReceiveProps(nextProps) {
if (nextProps.value !== this.state.value) {
this.setState({value: nextProps.value});
}
}
render() {
return <Component {...this.props} value={this.state.value} onChange={this.onChange}/>;
}
}
return SynchronousValueChanger;
};
export default synchronousValueChangerHOC;
const onChangePropagationDelay = 250;
然后它可以这样使用:
const InputWithSynchronousValueChange = synchronousValueChangerHOC('input');
通过将其作为HOC,我们可以让它为输入,textarea和其他人工作。也许这个名字并不是最好的,所以如果你们中有人有建议如何改进,请告诉我们:)
有一个有去抖的黑客攻击,因为有时,当打字很快就完成了,错误再次出现。
答案 5 :(得分:-1)
我们有类似的问题,在我们的例子中,我们必须使用异步状态更新。
因此我们使用defaultValue,和将一个key
参数添加到与输入反映的模型关联的输入中。这确保了对于任何模型,输入将保持与模型同步,但如果实际模型更改将强制生成新输入。