I can't set focus on the input - reactjs

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.

Related

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;
});
};

Hooks (useLayoutEffekt) gets triggered too often / values inside Component

I am having problems figuring out the correct way how to use useEffect / useLayoutEffect.
I have a component, which waits on a pointerevent (PointerEventTypes.POINTERTAP (comes from babylon). It is within a useLayoutEffect hook. It then fires up pointerClick() to do something.
The problem I am having is that there are 2 dependencies I need to watch: buildings and activeBuilding. How can I achieve that the function pointerClick() does not get triggered two times on the first single click?
I think what is happening is that there is one event (PointerTAP) and the function gets called. Then within the function I am changing activeBuilding which then again triggers a re-render and a second run of the function pointerClick().
If I instead use useEffect (not useLayoutEffect) I have to click twice to set the data because it does change the values after rendering. So it's not what I want.
Here is the example code:
const pointerClick = () => {
if (scene) {
const pickInfo = scene.pick(scene.pointerX, scene.pointerY)
if (buildings.length === 0 && activeBuilding === -1) {
setActiveBuilding(0)
}
if (activeBuilding != -1) {
if (pickInfo?.pickedMesh?.metadata?.measurement?.type === 'handle') {
handleDeletePoint(
pickInfo.pickedMesh.metadata.measurement.id,
activeBuilding
)
} else if (pickInfo?.pickedMesh?.name === 'measureOutline') {
setActiveBuilding(-1)
}
else if (
!pickInfo?.pickedMesh?.metadata?.measurement &&
pickInfo?.pickedPoint
) {
handleAddPoint(pickInfo.pickedPoint, activeBuilding)
}
}
}
}
useLayoutEffect(() => {
if (scene) {
const obs = scene?.onPointerObservable.add((pointerInfoEvent) => {
switch (pointerInfoEvent.type) {
case PointerEventTypes.POINTERTAP:
pointerClick()
// setClicked(false)
break
}
})
return () => {
scene.onPointerObservable.remove(obs)
}
}
}, [canvas, scene, buildings, activeBuilding])
return <Measure />
}
Do I have to get rid of one of the two: buildings or activeBuilding as a dependency (what I think I cannot do since I need both values to be changeable). What am I doing wrong here?
Thank you so much!

Cannot update a component while rendering a different Component - ReactJS

I know lots of developers had similar kinds of issues in the past like this. I went through most of them, but couldn't crack the issue.
I am trying to update the cart Context counter value. Following is the code(store/userCartContext.js file)
import React, { createContext, useState } from "react";
const UserCartContext = createContext({
userCartCTX: [],
userCartAddCTX: () => {},
userCartLength: 0
});
export function UserCartContextProvider(props) {
const [userCartStore, setUserCartStore] = useState([]);
const addCartProduct = (value) => {
setUserCartStore((prevState) => {
return [...prevState, value];
});
};
const userCartCounterUpdate = (id, value) => {
console.log("hello dolly");
// setTimeout(() => {
setUserCartStore((prevState) => {
return prevState.map((item) => {
if (item.id === id) {
return { ...item, productCount: value };
}
return item;
});
});
// }, 50);
};
const context = {
userCartCTX: userCartStore,
userCartAddCTX: addCartProduct,
userCartLength: userCartStore.length,
userCartCounterUpdateCTX: userCartCounterUpdate
};
return (
<UserCartContext.Provider value={context}>
{props.children}
</UserCartContext.Provider>
);
}
export default UserCartContext;
Here I have commented out the setTimeout function. If I use setTimeout, it works perfectly. But I am not sure whether it's the correct way.
In cartItemEach.js file I use the following code to update the context
const counterChangeHandler = (value) => {
let counterVal = value;
userCartBlockCTX.userCartCounterUpdateCTX(props.details.id, counterVal);
};
CodeSandBox Link: https://codesandbox.io/s/react-learnable-one-1z5td
Issue happens when I update the counter inside the CART popup. If you update the counter only once, there won't be any error. But when you change the counter more than once this error pops up inside the console. Even though this error arises, it's not affecting the overall code. The updated counter value gets stored inside the state in Context.
TIL that you cannot call a setState function from within a function passed into another setState function. Within a function passed into a setState function, you should just focus on changing that state. You can use useEffect to cause that state change to trigger another state change.
Here is one way to rewrite the Counter class to avoid the warning you're getting:
const decrementHandler = () => {
setNumber((prevState) => {
if (prevState === 0) {
return 0;
}
return prevState - 1;
});
};
const incrementHandler = () => {
setNumber((prevState) => {
return prevState + 1;
});
};
useEffect(() => {
props.onCounterChange(props.currentCounterVal);
}, [props.currentCounterVal]);
// or [props.onCounterChange, props.currentCounterVal] if onCounterChange can change
It's unclear to me whether the useEffect needs to be inside the Counter class though; you could potentially move the useEffect outside to the parent, given that both the current value and callback are provided by the parent. But that's up to you and exactly what you're trying to accomplish.

React useCallback function doesn't get current context value

I am trying to implement a use case with React Hooks and React Context API with mouse events.
I want to add mousemove event for a container. If user moves over an object (rectangle), a dispatch action is called and context value is updated. I want to achieve that the action is not dispatched repeatedly by checking context value before dispatching. The issue is that function doesn't get current context value.
This the event function useMouseEvents.js
import * as React from "react";
import { DragToCreateContext, actionTypes } from "./reducer";
export function useMouseEvents(elRef) {
const { dragToCreate, dispatchDragToCreate } = React.useContext(
DragToCreateContext
);
console.log("out of callback", dragToCreate);
const handleMouseMove = React.useCallback(
(evt) => {
if (evt.target.tagName === "P") {
console.log("inside callback", dragToCreate);
}
if (evt.target.tagName === "P" && dragToCreate.sourceNodeId === null) {
console.log("dispatch");
dispatchDragToCreate({
type: actionTypes.ACTIVATE,
sourceNodeId: 1
});
}
},
[dragToCreate]
);
React.useEffect(() => {
const el = elRef?.current;
if (el) {
el.addEventListener("mousemove", handleMouseMove);
return () => {
el.addEventListener("mousemove", handleMouseMove);
};
}
}, [elRef, handleMouseMove]);
}
codesandbox.io
If you hover over rectangle, you will see in console log:
inside callback {sourceNodeId: null}
dispatch
out of callback {sourceNodeId: 1}
inside callback {sourceNodeId: null}
dispatch
out of callback {sourceNodeId: 1}
inside callback {sourceNodeId: 1}
inside callback {sourceNodeId: null}
but it should be
inside callback {sourceNodeId: null}
dispatch
out of callback {sourceNodeId: 1}
inside callback {sourceNodeId: 1}
The behaviour that you see is because your listeners on mouseMove are removed and added whenever your context value changes. Also since your listener is recreated in useEffect it might so happen that before a new listener is attached, an old one executes and you get an old value from the closure.
To solve such scenarios, you can make use of a ref to keep track of updated context values and use that inside your listener callback. This way you will be able to avoid addition and removal of mouse event listener
import * as React from "react";
import { DragToCreateContext, actionTypes } from "./reducer";
export function useMouseEvents(elRef) {
const { dragToCreate, dispatchDragToCreate } = React.useContext(
DragToCreateContext
);
console.log("out of callback", dragToCreate);
const dragToCreateRef = React.useRef(dragToCreate);
React.useEffect(() => {
dragToCreateRef.current = dragToCreate;
}, [dragToCreate]);
const handleMouseMove = React.useCallback((evt) => {
if (evt.target.tagName === "P") {
console.log("inside callback", dragToCreateRef.current);
}
if (
evt.target.tagName === "P" &&
dragToCreateRef.current.sourceNodeId === null
) {
console.log("dispatch");
dispatchDragToCreate({
type: actionTypes.ACTIVATE,
sourceNodeId: 1
});
}
}, []);
React.useEffect(() => {
const el = elRef?.current;
if (el) {
el.addEventListener("mousemove", handleMouseMove);
return () => {
el.addEventListener("mousemove", handleMouseMove);
};
}
}, [elRef, handleMouseMove]);
}
Working codesandbox demo
I can't really find the answers I was hoping for but here's a couple things for you and maybe anyone else wanting to write an answer:
The inside callback {sourceNodeId: null} tells me that something is causing the Context to reset to it's initial values and the way you used your Provider is pretty atypical to what I usually see (changing this didn't really seem to fix anything though).
I thought maybe the useContext inside of useMouseEvents is getting just the default context, but I tried moving things around to guarentee that wasn't the case but that didn't seem to work. (Someone else might want to retry this?)
Edit: Removed this suggestion
Kinda unrelated to the issue, but you're going to want to change your useEffect too:
React.useEffect(() => {
const el = elRef?.current;
if (el) {
el.addEventListener("mousemove", handleMouseMove);
return () => {
el.removeEventListener("mousemove", handleMouseMove);
};
}

react input cursor moves to the end after update

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!

Resources