Bugs of Week 11/19/2018 — Jumping Cursor

Wang Tianjian
Dec 10, 2018 · 6 min read

An interesting bug popped up this week. The CS team came to us saying that a registration form on our website is very hard to change.

Bug description

The user, during registration, has put in his email as johman@gmail.com , and then realized his real email was john@gmail.com . Of course, he would like to correct it. He clicked between ‘a’ and ’n’, thinking he would hit backspace twice and the word would be fine. Instead, he found himself facing the new email johmn@gmail.co , as the cursor mysteriously jumped to the back of the word after he hit the first backspace. This is a ‘real’ bug with lots of negative effects on user experience, so I set out to find the cause of the problem.

Debugging

After playing around for a while in local environment, it seems that not only that form, but also two other forms on our website are suffering from jumping cursor. These forms defer in the elements within them, the part of Redux store they are connected to, and everything else. They only have one thing in common: they are using the same HOC wrapper to give them a isDirty field, and a setField method to call for onChange.

Every input element in the form would be something like this:

<input type="text"
value={inputValue}
onChange={e => setField(e.target.value)}
/>

And the setField method in the HOC is implemented like this:

setField(value) {
this.setState({isDirty: true}, () => {
this.props.dispatch(updateInput(value));
});
}

Which seems perfectly normal on the first site. The HOC’s isDirty state is set to true whenever something is set on the form, and then dispatches an action to update the value in the store.

After a desperate hour on trying to locate the problem using chrome debugger, fighting through all the async updates of React, and monitoring the changes in Redux store, something strange occurred.

So I was limiting the time isDirtywas really set:

setField(value) {
if (this.state.isDirty) {
this.props.dispatch(updateInput(value));
}
this.setState({isDirty: true}, () => {
this.props.dispatch(updateInput(value));
});
}

At this time I realized that if the state is dirty, React would be able to manage the cursor correctly. However every time setState is called, the cursor would jump to the end of input.

Correct code

A simple change then fixed the problem:

setField(value) {
this.setState({isDirty: true});
this.props.dispatch(updateInput(value));
}

This feels somewhat interesting because it does basically the same thing as the first version of setField , only now there’s no guarantee that the store is updated after the state is set. Or rather, given the update of Redux store is synchronous, the store would be set before the local state changes. And this seems to mean something to React as to where to put its cursor.

Root cause guessing

input element is different from normal elements likediv or span in that it has a cursor, and it’s the browser that’s managing it. In fact, React gives simply no means to manage cursor, but would rather expose a ref functionality for you to directly call focus on.

So what happens when react is controlling an input element? It must be doing something different than when it’s controlling, say, a button that render a string in the component’s state. Else, the cursor would always be at the back of the input, since React is re-rendering the input element or updating its value attribute.

It’s logical to guess React is treating an input element’s onChange and setState to respect the browser and don’t change the element, because this is what the browser is currently doing really well already. And the only time React takes the element into control is that the value change is not caused by onChange.

So in one of its ‘Update Cycle’s, React should be checking if the state change of a input is coming directly from the input element, or from some other source, for example, another input element that you want to keep their values in sync, or when using Redux, the store state changed. If coming from the input element, don’t alter the element value since the browser is currently managing them, and if coming from another source, reset the value of the element.

In the original implementation, the store state changed after the ‘Update Cycle’ is complete because it is the callback of setState . Therefore it would be the second ‘Update Cycle’ that the store changes and React decide to render the input field again. All it knows is that this input element is currently in focus, so it calls focus() to the element, therefore setting the cursor to be at the back of the input string.

Root Cause

Thanks to Tom Navrátil below, the root cause is now discovered. Turned out this has nothing to do with React or Redux.

Let’s forget about setting the form Dirty for a minute, and look into what would happen if there’s only the dispatch.

setField(value) {
this.props.dispatch(updateInput(value));
}

It’s quite obvious that the input will behave normally, even as we edit it in the middle of the string.

Now, what would happen if we make the dispatch asynchronous, but not using setState?

setField(value) {
Promise.resolve().then(() => {
this.props.dispatch(updateInput(value));
});
}

This turns out to also have jumping cursors! When editing the input field from the middle, the cursor jumps to the back of the string again.

The problem turned out to be simply that setState callback is async, it has nothing to do with the update/lifecycle of React.

But WHY? Why would an asynchronous update of the input field move the cursor? What kind of magic lies below this?

Input Focus Management

The browser manages the input cursor position, and it is following a simple rule when JS modifies the value of the input field:

  • If the input value is not the same as the original value in the element, move the cursor to the end of the input value.
  • If the input value is the same as the original value in the element, the cursor would also be kept in its original position.

To better illustrate the effect of the rule, let’s have a slower dispatch:

setField(value) {
window.setTimeout(() => {
this.props.dispatch(updateInput(value));
}, 1000);
}

Set the delay time to 1s, we’ll see that when user tries to delete the additional 5 in 12534 , the steps will be like this, (with | demonstrating the cursor):

  1. User click between 5 and 3 : 125|34
  2. User hit backspace: 12534|
  3. After 1 second: 1234|

In step 2, the user clicked between 5 and 3, and the input element’s original value is changed to 1234 , but without updating the store, React still thinks it’s 12534 , therefore setting the value to 12534 . The browser noticed a difference between the input value and the original value, then moved the cursor to the end of the string.

In step 3, after 1 second, the store value was changed, and React, noticing that the value is indeed 1234 , decided to update the input field value. Now the browser, still noticing a difference between the input value and the original value, updated the input field and moved the cursor to the end of the string again.

Conclusion

This appears to be a bug with React and/or Redux, but is, in fact, an issue working with the browsers. Also, React’s update function treat input elements most probably in the same way as it’s treating other elements.

So next time some naughty cursor hops and jumps, it is probably because of some sort of async updating its values.

If someone interested in seeing the issue, I’ve created a GitHub repo.

And again, thank you for your detailed explanation Tom Navrátil.

Wang Tianjian

Written by

Love writing documentation daily for humans and computers alike.

More From Medium

Related reads

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade