React-redux: should the render always happen in the same tick as dispatching an action?
Asked Answered
R

4

10

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
Ra answered 24/2, 2017 at 10:17 Comment(4)
I think the react part of this question has been comprehensively answered, thanks guys. I'm still interested in the redux part, is redux supposed to act within the same tick?Ra
I don't know the definitive answer to your question, but I haven't encountered this in an app with hundreds of input fields.Sora
Thanks Radio, that seems to indicate that there is an issue in the way we use redux that causes the delay.Ra
@Ra It is really hard to know whether the problem is being caused by Redux or not without an example, a lot of things might be involved. Redux is a very transparent framework and should not create and asynchronous action if you action creators dont do it intentionally. But React can do many many things under the hood, that is why 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
L
1

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.

Lugworm answered 6/3, 2017 at 1:45 Comment(2)
Thanks Clark, your comment caused me to find the root cause of our problem. How did I not see / think of this myself? We have a self-written middleware for compatibility with some of our legacy code which wraps the next call in ReactUpdates.batchedUpdates.Ra
@Ra No problems mate.Lugworm
I
0

Asynchronously updating without losing the position was never supported

--- Dan Abramov (gaearon)

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

Indreetloire answered 28/2, 2017 at 10:5 Comment(0)
E
0

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.

Endgame answered 28/2, 2017 at 11:15 Comment(2)
If you want to see the problem in action, just check in the jsbin example the bottom input (type text, move cursor to center of text and hit space).Ra
@Ra Sory about that, I've done some research by the way, hope it helpsEndgame
B
0

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.

Bassett answered 6/3, 2017 at 1:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.