Reputation: 7113
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:
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:
Upvotes: 10
Views: 598
Reputation: 6027
What middleware are you using in your Redux application? Perhaps one of them is wrapping a promise around your action dispatches. Using Redux without middleware does not exhibit this behaviour, so I think it's probably something specific to your setup.
Upvotes: 1
Reputation: 486
I think react-redux and redux are totally irrelevant to your case, this is pure React behavior. React-redux eventually calls setState on your component, there's no magic.
The problem that your async setState creates rendering frame between the react rendering and browser native event is because the batch update mechanism only happens within React synthetic events handler and lifecycle methods. Can check this post for detail.
Upvotes: 0
Reputation: 316
Asynchronously updating without losing the position was never supported
The solution is to track the cursor position and use a ref
inside componentDidUpdate()
to place the cursor correctly.
Additional info:
When you set attributes in react, internally this happens:
node.setAttribute(attributeName, '' + value);
When you set value
this way, the behavior is inconsistent:
Using setAttribute() to modify certain attributes, most notably value in XUL, works inconsistently, as the attribute specifies the default value.
--- https://developer.mozilla.org/en/docs/Web/API/Element/setAttribute
Regarding your question about whether rendering occurs synchronously, react's setState()
is asynchronous and used internally by react-redux:
There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains
--- https://facebook.github.io/react/docs/react-component.html#setstate
There is an internal joke in the team that React should have been called "Schedule" because React does not want to be fully "reactive".
--- https://facebook.github.io/react/contributing/design-principles.html#scheduling
Upvotes: 0
Reputation: 7459
The issue is not related to Redux, but to React. It is a known issue and won't be fixed in the React core as it is not considered a bug but an "unsupported feature".
This answer explains the scenario perfectly.
Some attempts to address this issue have been made, but as you might see, they all involve a wrapper component around the input, so it's a very nasty solution if you ask me.
Upvotes: 0