react input cursor moves to the end after update - reactjs

When I update the value in my input field, the cursor moves to the end of the field, but I want it to stay where it is. What could be causing this issue?
<Input
type="text"
placeholder="test
name="test"
onChange={getOnChange(index)}
value={testVal}/>
where Input is a component for the text input field, and getOnChange is:
const getOnChange = (index) =>
(event) => props.onChangeTest(event, index);
This is then carried over to the parent component, where I dispatch to update the state via Redux. I can see that the state is being updated fine, but the problem is the cursor is not staying in position, and is always moving to the end of the text

If the cursor jumps to the end of the field it usually means that your component is remounting. It can happen because of key property change on each update of the value somewhere in your parent or changes in your components tree. It's hard to tell without seeing more code. Prevent remounting and the cursor should stop jumping.
Use this effect to track mounting/unmounting
useEffect(() => {
console.log('mounted');
return () => {
console.log('unmounted')
}
}, []);

I would suggest using hooks to solve this
const Component = ({ onChange }) => {
const [text, setText] = useState("");
const isInitialRun = useRef(false);
useEffect(() => {
if (isInitialRun.current) {
onChange(text);
} else {
isInitialRun.current = true;
}
}, [text]);
// or if you want to have a delay
useEffect(() => {
if (isInitialRun.current) {
const timeoutId = setTimeout(() => onChange(text), 500);
return () => clearTimeout(timeoutId);
} else {
isInitialRun.current = true;
}
}, [text])
return (
<Input
type="text"
placeholder="test
name="test"
onChange={setText}
value={text}/>
);
}
To prevent initial call, when nothing changed isInitialRun used

This is the downside of the controlled component design pattern. I've been facing this problem for a long time and just lived with it. But there's an idea that I wanted to try in my spare time but end up never trying it (yet). Perhaps continuing with my idea could help you come up with the solution you need?
<Input
type="text"
placeholder="test
name="test"
onChange={getOnChange(index)}
value={testVal}
/>
// From props.onChangeTest
const onChangeTest = (event, index) => {
// TODO: Memorize the position of the cursor
this.setState({ testVal: event.target.value })
// Because setState is asynchronous
setTimeout(() => {
// TODO:
// Programmatically move cursor back to the saved position
// BUT it must increase/decrease based on number of characters added/removed
// At the same time considering if the characters were removed before or after the position
// Theoretically do-able, but it's very mind-blowing
// to come up with a solution that can actually 'nail it'
}, 0)
}
★ If this is taking too much time and you just want to get work done and ship your app, you might wanna consider using the uncontrolled component design pattern instead.

I was facing same issue, it was due to 2 sequential setState statements. changing to single setState resolved the issue. Might be helpful for someone.
Code before fix:
const onChange = (val) => {
// Some processing here
this.setState({firstName: val}, () => {
this.updateParentNode(val)
})
}
const updateParentNode = (val) => {
this.setState({selectedPerson: {firstName: val}})
}
Code After Fix
const onChange = (val) => {
// Some processing here
this.updateParentNode(val)
}
const updateParentNode = (val) => {
this.setState({selectedPerson: {firstName: val}, firstName: val})
}

You have two options.
make it an uncontrolled input (you can not change the input value later)
make it a properly controlled input
There is code missing here, so I can't say what the problem is.
setState is not the issue: https://reactjs.org/docs/forms.html#controlled-components
If you use setState in the callback React should preserve the cursor position.
Can you give a more complete example?
Is testVal a property that is manipulated from outside the component?

This totally worked for me (the other solutions did not):
const handleChange = (e, path, data) => {
let value = _.isObject(data) ? data.value : data;
let clonedState = { ...originalState };
// save position of cursor
const savedPos = e.target.selectionStart;
_.set(clonedState, path, value); // setter from lodash/underscore
// this wil move cursor to the end
setState({ ...clonedState }); // some use state setter
setTimeout(() => {
// restore cursor position
e.target.setSelectionRange(savedPos, savedPos);
}, 0)
};
Have this on my template (using semantic-ui):
<Input
type="text"
readOnly={false}
onChange={(e, data) => {
handleChange(e, "field", data);
}}
value={state.field}>
</Input>

For me, I was having a <ComponentBasedOnType> and that was the issue, I changed my logic and render my components with a condition && in the parent component.

The cursor on an input will be pushed to the end when you dynamically update the input value through code, which it seems like you are doing because I can see value={testVal} :)
This is a common issue on fields that use input masking!

Related

I can't set focus on the input

I have this component https://stackblitz.com/edit/react-ts-u7rw2w?file=App.tsx
When I click and call toggleInputCardName, I see in the console "NO inputRef.current null false". At the same time, an input is rendered on the page and above it I see "null inputRef true".
I tried using useEffect, but the result is the same.
How to understand this? IsInputCardName is simultaneously true and false?
How to set focus on the input in toggleInputCardName?
Try useCallback
const inputRef = useCallback(node => {
if (node) {
node.focus();
node.setSelectionRange(node.value.length, node.value.length);
}
}, []);
It is because the setState is async.
I think about 2 possibilities.
First one :
const toggleInputCardName = () => {
setInputCardNameVisibity(!isInputCardName);
};
React.useEffect(() => {
if (inputRef.current) {
console.log('YES inputRef.current', inputRef.current);
inputRef.current.focus();
inputRef.current.select();
} else {
console.log('NO inputRef.current', inputRef.current, isInputCardName);
}
}, [isInputCardName]);
Second one, you could simply add autofocus on the input and don't use ref :
<input
className="input-card-title"
type="text"
value="value"
autoFocus
/>
You need useLayoutEffect:
const toggleInputCardName = () => {
setInputCardNameVisibity(!isInputCardName)
}
React.useLayoutEffect(() => {
if (!inputRef.current) return
inputRef.current.focus()
inputRef.current.select()
}, [isInputCardName])
The reason it doesn't work in the handler or in a plain useEffect is that they are executing before the input is actually written to the DOM. State changes in react are flushed later, and don't happen immediately. useLayoutEffect waits until the DOM change is committed.
Another way of doing it is by wrapping it in a 0 second timeout. This looks hackier than it is, as a timer with 0 as the time to wait, will just wait until the current call stack is finished before executing.
const toggleInputCardName = () => {
setInputCardNameVisibity(!isInputCardName)
setTimeout(() => {
if (inputRef.current) {
console.log('YES inputRef.current', inputRef.current)
inputRef.current.focus()
inputRef.current.select()
} else {
console.log('NO inputRef.current', inputRef.current, isInputCardName)
}
}, 0)
}
But I'd probably useLayoutEffect.
#OneQ answer of using the focus attribute also makes a lot of sense and reduces noise.

Two independent state piece causing infinite loop - useEffect

I can't wrap my head around the problem I'm experiencing Basically I submit the form and it checks whether or not there are empty values. I then paint the input border either red or green. However, I need to repaint the border all the time, meaning that if user enters a value, the border should turn green (hence the useEffect). I have 2 pieces of state here. One keeps track of validation error indexes (for value === '') The other piece is the createForm state (form fields) itself.
I then send down the indexes via props.
NOTE: infinite loop occurs not on initial render, but when form is submitted with empty values. The infinite loop DOES NOT occur if there is no empty field on form submit.
I'm willing to share additional info on demand.
const [createForm, setCreateForm] = React.useState(() => createFormFields);
const [validationErrorIndexes, setValidationErrorIndexes] = React.useState([]);
//Function that is being triggered in useEffect - to recalculate validaiton error indexes and resets the indexes.
const validateFormFields = () => {
const newIndexes = [];
createForm.forEach((field, i) => {
if (!field.value) {
newIndexes.push(i);
}
})
setValidationErrorIndexes(newIndexes);
}
//(infinite loop occurs here).
React.useEffect(() => {
if (validationErrorIndexes.length) {
validateFormFields();
return;
}
}, [Object.values(createForm)]);
//Function form submit.
const handleCreateSubmit = (e) => {
e.preventDefault();
if (createForm.every(formField => Boolean(formField.value))) {
console.log(createForm)
// TODO: dispatch -> POST/createUser...
} else {
validateFormFields();
}
}
//I then pass down validationErrorIndexes via props and add error and success classes conditionally to paint the border.
{createForm && createForm.length && createForm.map((formEl, i) => {
if (formEl.type === 'select') {
return (
<Select
className={`create-select ${(validationErrorIndexes.length && validationErrorIndexes.includes(i)) && 'error'}`}
styles={customStyles}
placeholder={formEl.label}
key={i}
value={formEl.value}
onChange={(selectedOption) => handleOptionChange(selectedOption, i)}
options={formEl.options}
/>
)
}
return (
<CustomInput key={i} {...{ label: formEl.label, type: formEl.type, value: formEl.value, formState: createForm, formStateSetter: setCreateForm, i, validationErrorIndexes }} />
)
})}
Ok, so here is what is happening:
Initial render - validationErrorIndexes is empty, bugged useEffect does not hit if and passes.
You click submit with 1 empty field - submit calls validateFormFields, it calculates, setValidationErrorIndexes is set, now its length is non zero and if in bugged useEffect will be hit. And here we got a problem...
The problem: on each rerender of your component Object.values(createForm) which are in your dependency array are evaluated and returning new [array]. Every single rerender, not the old one with same data, the new one with same data. And asuming the if guard is gone now due to length is non 0 - validateFormFields is called again. Which does its evaluations, and setting new data with setValidationErrorIndexes. Which causes rerendering. And Object.values(createForm) returns a new array again. So welp.
So basically, 2 solutions.
One is obvious and just replace [Object.values(createForm)] with just [createForm]. (why do you need Object.values here btw?)
Second one - well if it a must to have [Object.values(createForm)].
const validateFormFields = () => {
const newIndexes = [];
console.log(createForm);
createForm.forEach((field, i) => {
if (!field.value) {
newIndexes.push(i);
}
});
console.log(newIndexes);
setValidationErrorIndexes((oldIndexes) => {
// sorry, just an idea. Compare old and new one and return old if equals.
if (JSON.stringify(oldIndexes) === JSON.stringify(newIndexes)) {
return oldIndexes; // will not cause rerender.
}
return newIndexes;
});
};

State is always one click late

I have a problem with my code, basically setRevalue is always one click late.
const Exchanger = () => {
const [revalue, setRevalue] = useState('null')
const [valueOfInput, setValueOfInput] = useState('');
const [valueOfSelect, setValueOfSelect] = useState('');
const handleClick = () => {
switch (valueOfSelect) {
case 'option_1':
axios.get('http://api.nbp.pl/api/exchangerates/rates/a/chf/?format=json')
.then((response) => {
setRevalue(response.data.rates[0].mid)
console.log(revalue);
})
break;
const handleInputChange = (event) => {
setValueOfInput(event.target.value);
}
const handleSelectChange = (event) => {
setValueOfSelect(event.target.value);
}
return(
<>
<Input
labelText= 'Amount'
onChange= { handleInputChange }
value = { valueOfInput }
/>
<Select selectValue={ valueOfSelect } onSelectChange={ handleSelectChange } />
<Button onClick={handleClick}>Klik!</Button>
</>
)
On the first click it returns a null which it was supposed to be before the click.
Any help would be much appreciated.
Hey so you have a clousure issue there
In JavaScript, closures are created every time a function is created, at function creation time.
At the time the .then() function is created, revalue has the value of the last render, That's why even after updating the state with setRevalue (and thus triggering a rerender), you get the "old value" (the one corresponding to the render in which the .then function was created).
I don't know exactly your use case, but seems like you can achieve whatever you are trying to achieve by doing:
const revalue = response.data.rates[0].mid
setRevalue(revalue)
console.log(revalue);
if you want to access to revalue right after setting it you have to use useEffect with revalue as a parameter , it would be like this
useEffect(()=>{
console.log(revalue);
},[revalue])
now every time revalue change you will have current result

React - UseEffect not re-rendering with new data?

This is my React Hook:
function Student(props){
const [open, setOpen] = useState(false);
const [tags, setTags] = useState([]);
useEffect(()=>{
let input = document.getElementById(tagBar);
input.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById(tagButton).click();
}
});
},[tags])
const handleClick = () => {
setOpen(!open);
};
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
tagList.push(input.value);
console.log("tag");
console.log(tags);
console.log("taglist");
console.log(tagList);
setTags(tagList);
}
const tagDisplay = tags.map(t => {
return <p>{t}</p>;
})
return(
<div className="tags">
<div>
{tagDisplay}
</div>
<input type='text' id={tagBar} className="tagBar" placeholder="Add a Tag"/>
<button type="submit" id={tagButton} className="hiddenButton" onClick={addTag}></button>
<div>
);
What I am looking to do is be able to add a tag to these student elements (i have multiple but each are independent of each other) and for the added tag to show up in the tag section of my display. I also need this action to be triggerable by hitting enter on the input field.
For reasons I am not sure of, I have to put the enter binding inside useEffect (probably because the input element has not yet been rendered).
Right now when I hit enter with text in the input field, it properly updates the tags/tagList variable, seen through the console.logs however, even though I set tags to be the re-rendering condition in useEffect (and the fact that it is also 1 of my states), my page is not updating with the added tags
You are correct, the element doesn't exist on first render, which is why useEffect can be handy. As to why its not re-rendering, you are passing in tags as a dependency to check for re-render. The problem is, tags is an array, which means it compares the memory reference not the contents.
var myRay = [];
var anotherRay = myRay;
var isSame = myRay === anotherRay; // TRUE
myRay.push('new value');
var isStillSame = myRay === anotherRay; // TRUE
// setTags(sameTagListWithNewElementPushed)
// React says, no change detected, same memory reference, skip
Since your add tag method is pushing new elements into the same array reference, useEffect thinks its the same array and is not re-triggers. On top of that, React will only re-render when its props change, state changes, or a forced re-render is requested. In your case, you aren't changing state. Try this:
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
// Create a new array reference with the same contents
// plus the new input value added at the end
setTags([...tagList, input.value]);
}
If you don't want to use useEffect I believe you can also use useRef to get access to a node when its created. Or you can put the callback directly on the node itself with onKeyDown or onKeyPress
I can find few mistake in your code. First, you attaching event listeners by yourself which is not preferred in react. From the other side if you really need to add listener to DOM inside useEffect you should also clean after you, without that, another's listeners will be added when component re-rendered.
useEffect( () => {
const handleOnKeyDown = ( e ) => { /* code */ }
const element = document.getElementById("example")
element.addEventListener( "keydown", handleOnKeyDown )
return () => element.removeEventListener( "keydown", handleOnKeyDown ) // cleaning after effect
}, [tags])
Better way of handling events with React is by use Synthetic events and components props.
const handleOnKeyDown = event => {
/* code */
}
return (
<input onKeyDown={ handleOnKeyDown } />
)
Second thing is that each React component should have unique key. Without it, React may have trouble rendering the child list correctly and rendering all of them, which can have a bad performance impact with large lists or list items with many children. Be default this key isn't set when you use map so you should take care about this by yourself.
tags.map( (tag, index) => {
return <p key={index}>{tag}</p>;
})
Third, when you trying to add tag you again querying DOM without using react syntax. Also you updating your current state basing on previous version which can causing problems because setState is asynchronous function and sometimes can not update state immediately.
const addTag = newTag => {
setState( prevState => [ ...prevState, ...newTage ] ) // when you want to update state with previous version you should pass callback which always get correct version of state as parameter
}
I hope this review can help you with understanding React.
function Student(props) {
const [tags, setTags] = useState([]);
const [inputValue, setInputValue] = useState("");
const handleOnKeyDown = (e) => {
if (e.keyCode === 13) {
e.preventDefault();
addTag();
}
};
function addTag() {
setTags((prev) => [...prev, inputValue]);
setInputValue("");
}
return (
<div className="tags">
<div>
{tags.map((tag, index) => (
<p key={index}>{tag}</p>
))}
</div>
<input
type="text"
onKeyDown={handleOnKeyDown}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a Tag"
/>
<button type="submit" onClick={addTag}>
ADD
</button>
</div>
);
}

How to catch remove event in react-select when removing item from selected?

first of all, thanks for awesome work. It makes a lot easier for me. Then here i want to catch remove event, how can I do that? I read the documentation and could not find remove event
I don't think they have an event for that. They just have the onChange.
So, to detect if some option was removed, you would have to compare the current state with the new values the onChange emitted.
Example:
handleOnChange(value) {
let difference = this.state.selected.filter(x => !value.includes(x)); // calculates diff
console.log('Removed: ', difference); // prints array of removed
this.setState({ selected: value });
}
render() {
return (
<div>
<Select
multi={this.state.multi}
options={this.state.options}
onChange={this.handleOnChange.bind(this)}
value={this.state.selected}
showNewOptionAtTop={false}
/>
</div>
);
}
Demo: https://codesandbox.io/s/7ymwokxoyq
React Select exposes a variety of eventListeners to you via props. The onchange function prop now has the following signature. (value: ValueType, action: ActionType).
Accordingly, you can get these events:
select-option, deselect-option, remove-value, pop-value, set-value, clear, create-option, through which you can handle creation and deletion.
Please refer to documentation
They don't have any event for that. I was facing the same problem but in my case, I was needing both the added and removed item. In case someone wants the both values
import 'difference' from 'lodash/difference'
this.currentTags = []
handleChange = (options) => {
const optionsValue = options.map(({ value }) => value)
if (this.currentTags.length < options.length) {
const addedElement = difference(optionsValue, this.currentTags)[0]
this.currentTags.push(addedElement)
console.log("addedElement",addedElement)
//call custom add event here
}
else {
let removedElement = difference(this.currentTags, optionsValue)[0]
console.log("removedElement", removedElement)
// call custom removed event here
let index = this.currentTags.indexOf(removedElement)
if (index !== -1) {
this.currentTags.splice(index, 1)
}
}
}
This code of snippet is working for me to know if any item is removed
I am calling handle change function on onChange event in reat-select
const [selectedGroups, setSelectedGroups] = useState([]);
const handleChange = (value) => {
const diifer = value.length <= selectedGroups.length;
if(diifer){
// item is removed
}
var arr = value.map((object) => object.value);
setSelectedGroups(arr);
};

Resources