javascript - script - En ReactJS, ¿por qué `setState` se comporta de manera diferente cuando se llama sincrónicamente?
use js in react (6)
Estoy tratando de entender la causa subyacente de un comportamiento algo "mágico" que estoy viendo que no puedo explicar completamente, y que no es evidente al leer el código fuente de ReactJS.
Al llamar al método setState
sincrónicamente en respuesta a un evento onChange
en una entrada, todo funciona como se espera. El "nuevo" valor de la entrada ya está presente, por lo que el DOM no se actualiza realmente. Esto es altamente deseable porque significa que el cursor no saltará al final del cuadro de entrada.
Sin embargo, al ejecutar un componente con exactamente la misma estructura pero que llama a setState
asincrónicamente , el "nuevo" valor de la entrada no parece estar presente, haciendo que ReactJS realmente toque el DOM, lo que hace que el cursor salte hasta el final de la entrada.
Aparentemente, algo está interviniendo para "restablecer" la entrada a su value
anterior en el caso asíncrono, lo que no ocurre en el caso síncrono. ¿Qué es esta mecánica?
Ejemplo sincrónico
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});
}
}));
Tenga en cuenta que el código iniciará sesión en el método de render
, imprimiendo el value
actual del nodo DOM real.
Al escribir una "X" entre las dos L de "Hola", vemos la siguiente salida de la consola, y el cursor permanece donde se esperaba:
Rendering...
Setting value:HelXlo
Current value:HelXlo
Ejemplo asíncrono
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});
}
}));
Esto es exactamente lo mismo que el anterior, excepto que la llamada a setState
está en una setTimeout
llamada setTimeout
.
En este caso, al escribir una X entre las dos L se obtiene la siguiente salida de la consola, y el cursor salta al final de la entrada:
Rendering...
Setting value:HelXlo
Current value:Hello
¿Por qué es esto?
Entiendo el concepto de React de un Componente Controlado , por lo que tiene sentido que los cambios del usuario al value
sean ignorados. Pero parece que el value
de hecho se cambia y luego se reinicia explícitamente.
Aparentemente, al llamar a setState
sincrónicamente se asegura de que surta efecto antes del reinicio, mientras que la llamada a setState
en cualquier otro momento ocurre después del restablecimiento, forzando una nueva representación.
De hecho, esto es lo que está pasando?
Ejemplo de JS Bin
Como se mencionó, esto será un problema cuando se usen componentes controlados porque React está actualizando el valor de la entrada, en lugar de viceversa (React intercepta la solicitud de cambio y actualiza su estado para que coincida).
La respuesta de FakeRainBrigand es genial, pero me he dado cuenta de que no es del todo cierto que una actualización sea sincrónica o asíncrona, lo que hace que la entrada se comporte de esta manera. Si está haciendo algo sincrónicamente, como aplicar una máscara para modificar el valor devuelto, también puede hacer que el cursor salte al final de la línea. Desafortunadamente (?), Así es como funciona React con respecto a las entradas controladas. Pero puede ser trabajado manualmente.
Hay una gran explicación y discusión de esto en los problemas de reaccionar github, que incluye un enlace a una solución JSBin por Ben Alpert [que manualmente asegura que el cursor permanece donde debería estar]
Esto se logra usando un componente <Input>
como este:
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;
},
});
El uso de defaultValue en lugar de valor resolvió el problema para mí. No estoy seguro si esta es la mejor solución, por ejemplo:
De:
return React.DOM.input({value: valueToSet,
onChange: this.changeHandler});
A:
return React.DOM.input({defaultValue: valueToSet,
onChange: this.changeHandler});
Ejemplo de JS Bin
Esta no es exactamente una respuesta, sino un enfoque posible para mitigar el problema. Define un contenedor para las entradas React que gestiona las actualizaciones de valores de forma síncrona a través de un shim de estado local; y versiones de los valores de salida para que solo se aplique el último resultado del procesamiento asíncrono.
Se basa en un trabajo de Stephen Sugden ( https://github.com/grncdr ) que actualicé para React moderno y mejoró al versionar los valores, lo que elimina la condición de carrera.
No es hermosa :)
http://jsfiddle.net/yrmmbjm1/1/
var AsyncInput = asyncInput(''input'');
Aquí es cómo los componentes necesitan usarlo:
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);
Otra versión que intenta hacer el impacto en el código del componente de control menos desagradable es aquí:
http://jsfiddle.net/yrmmbjm1/4/
Eso termina pareciéndose a:
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);
¯ / _ (ツ) _ / ¯
Esto es lo que está pasando.
Sincrónico
- presionas X
- input.value es ''HelXlo''
- llama a
setState({value: ''HelXlo''})
- la dom virtual dice que el valor de entrada debe ser ''HelXlo''
- input.value es ''HelXlo''
- ninguna acción tomada
Asincrónico
- presionas X
- input.value es ''HelXlo''
- No haces nada
- el DOM virtual dice que el valor de entrada debe ser ''Hola''
- reaccionar hace input.value ''Hello''.
Mas tarde...
-
setState({value: ''HelXlo''})
- el DOM virtual dice que el valor de entrada debe ser ''HelXlo''
- reaccionar hace input.value ''HelXlo''
- el navegador salta el cursor hasta el final (es un efecto secundario de establecer .value)
¿Magia?
Sí, hay un poco de magia aquí. Las llamadas React se procesan sincrónicamente después del controlador de eventos. Esto es necesario para evitar parpadeos.
He tenido el mismo problema cuando uso Reflujo. Estado se almacenó fuera de un Componente de setState
que causó un efecto similar al envolver setState
dentro de un setTimeout
.
@dule sugirió que deberíamos hacer que nuestro estado cambie síncrono y asíncrono al mismo tiempo. Así que preparé un HOC que garantiza que el cambio de valor sea sincrónico, por lo que es genial envolver la entrada que sufre un cambio de estado asincrónico.
Una nota: este HOC funcionará solo para los componentes que tienen una API similar a <input/>
, pero supongo que es sencillo hacerlo más genérico si fuera necesario.
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;
Y luego se puede usar de tal manera:
const InputWithSynchronousValueChange = synchronousValueChangerHOC(''input'');
Al convertirlo en HOC podemos tenerlo funcionando para entradas, área de texto y probablemente también para otros. Tal vez el nombre no sea el mejor, así que si alguno de ustedes tiene una sugerencia de cómo mejorar, háganmelo saber :)
Hay un truco con rebote, porque a veces, cuando se realizaba la escritura muy rápido, el error reaparecía.
Tenemos un problema similar y en nuestro caso tenemos que usar actualizaciones estatales asincrónicas.
Así que usamos defaultValue y agregamos un parámetro key
a la entrada asociada con el modelo que refleja la entrada. Esto asegura que para cualquier modelo, la entrada permanecerá sincronizada con el modelo, pero si los cambios reales del modelo forzarán una nueva entrada para que se genere.