In my react-redux application, I have a controlled text input. Every time the component changes value, it dispatches an action and in the end, the value comes back through the redux loop and is rendered.
In the example below this works well, but in practice, I've run into an issue where the render happens asynchronously from the action dispatch and the input loses cursor position. To demonstrate the issue, I've added another input with a delay explicitly put in. Adding a space in the middle of a word causes the cursor to skip in the async input.
I have two theories about this and would like to know which one is true:
- This should work, but I have a bug somewhere in my production application that causes the delay
- The fact that it works in the simple example is just luck and react-redux doesn't guarantee that render would happen synchronously
Which one is right?
Working example:
http://jsbin.com/doponibisi/edit?html,js,output
const INITIAL_STATE = {
value: ""
};
const reducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'SETVALUE':
return Object.assign({}, state, { value: action.payload.value });
default:
return state;
}
};
const View = ({
value,
onValueChange
}) => (
<div>
Sync: <input value={value} onChange={(e) => onValueChange(e.target.value)} /><br/>
Async: <input value={value} onChange={(e) => { const v = e.target.value; setTimeout(() => onValueChange(v), 0)}} />
</div>
);
const mapStateToProps = (state) => {
return {
value: state.value
};
}
const mapDispatchToProps = (dispatch) => {
return {
onValueChange: (value) => {
dispatch({
type: 'SETVALUE',
payload: {
value
}
})
}
};
};
const { connect } = ReactRedux;
const Component = connect(
mapStateToProps,
mapDispatchToProps
)(View);
const { createStore } = Redux;
const store = createStore(reducer);
ReactDOM.render(
<Component store={store} />,
document.getElementById('root')
);
EDIT: Clarifying question
Marco and Nathan have both correctly pointed out that this is a known issue in React that won't be fixed. If there is a setTimeout
or other delay between onChange
and setting the value, the cursor position will be lost.
However, the fact that setState just schedules an update is not enough to cause this bug to happen. In the Github issue that Marco linked, there is a comment:
Loosely speaking, setState is not deferring rendering, it's batching updates and executing them immediately when the current React job has finished, there will be no rendering frame in-between. So in a sense, the operation is synchronous with respect to the current rendering frame. setTimeout schedules it for another rendering frame.
This can be seen in JsBin example: the "sync" version also uses setState, but everything is working.
The open question still is: is there something inside of Redux that creates a delay that lets a rendering frame in-between, or could Redux be used in a way that avoids those delays?
Workarounds for the issue at hand are not needed, I found one that works in my case but I'm interested in finding out the answer to the more general question.
EDIT: issue solved
I was happy with Clarks answer and even awarded the bounty, but it turns out it was wrong when I really tested it by removing all middlewares. I also found the github issue that is related to this.
https://github.com/reactjs/react-redux/issues/525
The answer is:
- this is an issue in react-redux that will be fixed with react-redux 5.1 and react v16
setState
is async in the first place, to give React the ability to postpone work to avoid frame drops. There are a lot of ifs, and you're not giving enough information to reproduce the error, since the one you showed was already answered. – Endgame