If you've ever tried to filter out characters from the input in the onChange
handler, you've most likely ran into the issue when, if you're inserting something in the middle of the input, the cursor jumps to the end of the input.
This happens because React has no way of knowing where you want the cursor to be!
In this lesson, we will create a filtering function that accepts new text and current cursor position, and which returns the filtered text and the new cursor position. We then use a custom useRunAfterUpdate
hook after the set state call to update the cursor position after the component has re-rendered.
Instructor: [00:01] We have a name input that doesn't allow the user to type any numbers or special characters into the box. The way it's implemented is pretty simple. The onChange handler just removes the unwanted characters from event.target.value and assigns a state to that new filtered value.
[00:19] There is just one problem with that. If we put the cursor in the middle of the input and try to insert a number, we can see that the cursor jumps to the very end. That's happening because React doesn't know where to put the cursor.
[00:32] We can fix that. In handleNameChange, let's create a few variables that we'll need. We'll create const input = event.target. We'll create const text = input.value. We'll create const cursor = input.selectionStart.
[00:53] To calculate new name, let's call the filterOut function. Let's give it the text that the user has typed in and the current cursor position. Instead of just expecting new name from that function, let's also expect a new cursor position from it.
[01:12] It's time to define that function. Let's go ahead and create function filterOut. Accepts text and cursor. We'll create const newText = text and const newCursor = cursor. We'll just return those two values.
[01:34] Filtering out text seems pretty easy. We can just do stripText. How do we calculate the new cursor position? To achieve that, we're going to split the text into two parts, before and after the cursor.
[01:47] We'll create a beforeCursor variable. The value will be text sliced from index zero to cursor position. We'll create a const afterCursor, which will equal to text sliced from cursor to the very end of the text.
[02:08] Let's just remove the unwanted characters from both of those parts separately. We'll create a const filteredBeforeCursor, will equal to strip beforeCursor. We'll create a const filteredAfterCursor, which will equal to strip afterCursor.
[02:31] New text will be just a combination of filteredBeforeCursor and filteredAfterCursor. We'll put the cursor where the before part ends. We'll say newCursor = filteredBeforeCursor.length.
[02:48] Let's go back to the handleNameChange function. We are updating the state, but we are not updating the cursor position just yet. Let's go ahead and do that. We'll do input.selectionStart = newCursor. input.selectionEnd = newCursor.
[03:10] If we go to the input now, move the cursor back to the middle of the input, and try to press a number, we still see that the cursor is jumping to the end of the input. This is happening because we are updating cursor position right away, even before React had a chance to give the new value to the input.
[03:27] To fix that, I will paste a custom hook called useRunAfterUpdate. This custom hook will allow us to run an arbitrary function after a React component has rendered using the useLayoutEffect hook.
[03:41] I will use this hook in our component. I'll do const runAfterUpdate = useRunAfterUpdate. We'll go ahead and wrap our cursor updates in logic until a runAfterUpdate call. Give it a function that updates the cursor. Now go to the middle of the input and try to press a number. We see that the cursor is where it should be and that it hasn't moved to the end of the input.