I want someone to confirm my intuition on below problem.
My goal was to keep track (similar to here) of props.personIdentId and when it changed, to call setSuggestions([]).
Here is code:
function Identification(props) {
let [suggestions, setSuggestions] = useState([]);
let prevPersonIdentId = useRef(null)
let counter = useRef(0) // for logs
// for accessing prev props
useEffect(() => {
prevPersonIdentId.current = props.personIdentId;
console.log("Use effect", props.personIdentId, prevPersonIdentId.current, counter.current++)
});
// This props value has changed, act on it.
if (props.personIdentId != prevPersonIdentId.current) {
console.log("Props changed", props.personIdentId, prevPersonIdentId.current, counter.current++)
setSuggestions([])
}
But it was getting in an infinite loop as shown here:
My explanation why this happens is that: initially when it detects the prop has changed from null to 3, it calls setSuggestions which schedules a re-render, then next re-render comes, but previous scheduled useEffect didn't have time to run, hence the prevPersonIdentId.current didn't get updated, and it again enters and passes the if condition which checks if prop changed and so on. Hence infinite loop.
What confirms this intuition is that using this condition instead of old one, fixes the code:
if (props.personIdentId != prevPersonIdentId.current) {
prevPersonIdentId.current = props.personIdentId;
setSuggestions([])
}
Can someone confirm this or add more elaboration?
useEffect - is asynchronous function!
And you put yours condition in synchronous part of component. Off course synchronous part runs before asynchronous.
Move condition to useEffect
useEffect(() => {
if (personIdentId !== prevPersonIdentId.current) {
prevPersonIdentId.current = personIdentId;
console.log(
"Use effect",
personIdentId,
prevPersonIdentId.current,
counter.current++
);
setSuggestions([]);
}
});
It could be read as:
When mine component updates we check property personIdentId for changes and if yes we update ref to it's value and run some functions
It looks like this is what's happening:
On the first pass, props.personId is undefined and
prevPersonIdentId.current is null. Note that if (props.personIdentId !=
prevPersonIdentId.current) uses != so undefined is coerced to
null and you don't enter the if.
Another render occurs with the same conditions.
props.personId now changes, so you enter the if.
setSuggestions([]) is called, triggering a re-render.
loop forever
Your useEffect is never invoked, because you keep updating your state and triggering re-renders before it has a chance to run.
If you want to respond to changes in the prop, rather than attempting to roll your own change-checking, you should just use useEffect with the value you want to respond to in a dependency array:
useEffect(() => {
setSuggestions([])
}, [props.personId] );
Related
I am having an issue with a Next-js React checkbox list snippet after extracting it into the sandbox.
whenever I clicked the checkbox, I get the error:
TypeError: Cannot read properties of undefined (reading 'id')
which originated from line 264:
setCheckedThread(prev => new Set(prev.add(pageData.currentThreads[index].id)));
but at the top of the index.js I have defined the static JSON
and in useEffect() I update the pageData state with:
setPageData({
currentPage: threadsDataJSON.threads.current_page,
currentThreads: threadsDataJSON.threads.data,
totalPages: totalPages,
totalThreads: threadsDataJSON.threads.total,
});
so why when I clicked the checkbox it throws the error?
my sandbox link: https://codesandbox.io/s/infallible-goldberg-vfu0ve?file=/pages/index.js
It looks like your useEffect on line 280 only triggers once you've checked a box (for some reason), so until you trigger that useEffect, pageData.currentThreads remains empty, which is where the error you're running into comes from.
I'd suggest moving all the state initialization from the useEffect into the useState call itself. E.g.
// Bad
const [something, setSomething] = useState(/* fake initial state */);
useEffect(() => {
setSomething(/* real initial state */)
}, []);
// Good
const [something, setSomething] = useState(/* real initial state */);
Here's a fork of your sandbox with this fix.
This is occurring because in Home you've created the handleOnChange function which is passed to the List component that is then passed to the memoized Item component. The Item component is kept the same across renders (and not rerendered) if the below function that you've written returns true:
function itemPropsAreEqual(prevItem, nextItem) {
return (
prevItem.index === nextItem.index &&
prevItem.thread === nextItem.thread &&
prevItem.checked === nextItem.checked
);
}
This means that the Item component holds the first initial version of handleOnChange function that was created when Home first rendered. This version of hanldeOnChange only knows about the initial state of pageData as it has a closure over the initial pageData state, which is not the most up-to-date state value. You can either not memoize your Item component, or you can change your itemPropsAreEqual so that Item is rerendered when your props.handleOnChange changes:
function itemPropsAreEqual(prevItem, nextItem) {
return (
prevItem.index === nextItem.index &&
prevItem.thread === nextItem.thread &&
prevItem.checked === nextItem.checked &&
prevItem.handleOnChange === nextItem.handleOnChange // also rerender if `handleOnChange` changes.
);
}
At this point you're checking every prop passed to Item in the comparison function, so you don't need it anymore and can just use React.memo(Item). However, either changing itemPropsAreEqual alone or removing itemPropsAreEqual from the React.memo() call now defeats the purpose of memoizing your Item component as handleOnChange gets recreated every time Home rerenders (ie: gets called). This means the above check with the new comparison function will always return false, causing Item to rerender each time the parent Home component rerenders. To manage that, you can memoize handleOnChange in the Home component by wrapping it in a useCallback() hook, and passing through the dependencies that it uses:
const handleOnChange = useCallback(
(iindex, id) => {
... your code in handleOnChange function ...
}
, [checkedState, pageData]); // dependencies for when a "new" version of `handleOnChange` should be created
This way, a new handleOnChange reference is only created when needed, causing your Item component to rerender to use the new up-to-date handleOnChange function. There is also the useEvent() hook which is an experimental API feature that you could look at using instead of useCallback() (that way Item doesn't need to rerender to deal with handleOnChange), but that isn't available yet as of writing this (you could use it as a custom hook for the time being though by creating a shim or using alternative solutions).
See working example here.
I am using React Hooks to manage states within a component.
const addNode = () => {
let pform = pForm
let handles = [vForm, yForm, hForm]
let access_info = [virtualForm, management1Form, management2Form, consoleForm]
let newObj = {
...currentForm,
p: pform,
handles: handles,
access_info: access_info,
}
console.log('newObj', newObj)
setCurrentForm(
newRouterObj
)
console.log(currentForm)
let currArr = [...addedNodes]
currArr.push(currentForm)
setAddedNodes(currArr)
intializeForms()
}
The function above is an onClick that I use when I press an Add button. The forms (pForm, vForm, yForm, etc.) are all separate states. I gather them together and put them into a single object newObj and use setCurrentForm to update the currentForm state to newObj.
When I console.log the newObj, everything goes in fine. However, when I check the currentForm after the setCurrentForm, the fields (p, handles, and access_info) are empty.
I know that states in React can have a delay in updates so I might have to use useEffect. However, in my use case, which is to gather different states and put them in as a new field in the currentForm state seems useEffect is not the best way to solve it. Can anyone help please?
You are misunderstanding exactly how useState works. When you call the useState setter function, the state value isn't actually updated immediately, instead it will trigger the component to re-render with the updated value. Even though you call the setter half way through the function, the state value will remain the original value for the entire lifetime of that function call.
You could slightly tweak what you have to be
const addNode = () => {
...
let currArr = [...addedNodes]
// you know that currentForm is supposed to be newObj, so just push that
// see my explanation above to understand why it currentForm isn't what you expect
currArr.push(newObj)
...
}
It's an async action so values will not be assigned/updated instantly. You need to watch for the changes using useEffect hook to log new values and to do anything in case
useEffect(() => {
// Whenever `currentForm` will be updated, this callback will be invoked
console.log('updated currentForm values', currentForm);
},[currentForm]);
I have a form that I want to do a basic validation check. I want to check if any of the values are empy except for the checkboxes, but we can start with just one validation. I thought I could use useState hook, but it always sets the default to true on first submit (when the value is filled out), even tho I am setting the value to true when the input value is filled.
const [nameVal, setNameVal] = useState(false);
const submitForm = async (e) => {
e.preventDefault();
//console.log(e.target.name.value);
if(e.target.name.value === ""){
setNameVal(false);
console.log(nameVal);
}
else{
setNameVal(true);
console.log(nameVal);
}
How do I get the console.log(nameVal) to show true the first time the form is submitted and the target.name.value is filled out. Currently the first time it is false and then every time after that it is true. So everything seems to work as I would like except the initial click. Thanks ahead of time
Let's review how useState works
When you do this
const [nameVal, setNameVal] = useState(false);
you're creating a variable (with an initial value of false) and a function. That means that in your handler (assuming you've filled a value properly)
const submitForm = async (e) => {
e.preventDefault();
if(e.target.name.value === ""){
setNameVal(false);
console.log(nameVal); // --> here it will log true
you will see logged true, because nameVal is the variable from above (from the useState line). When you run setNameVal(false) basically it continues to execute your code, and once that has finished, it fires the reconciliation algorithm in React, which will cause a new render. This time, (the second time), when this line runs
const [nameVal, setNameVal] = useState(false);
nameVal will be true. That's because the initial value is used only on the first render. and because it knows the value of the state has changed, thanks to you calling the setXXX function to update to a new state
You need to consider your functional components as a function of state and props, and every change for all the changes you make on your state, you will render the new output in the next render cycle. So that means - every render has its own props and state
(that's not mine, though, read this amazing article from Dan Abramov which also talks about useEffect https://overreacted.io/a-complete-guide-to-useeffect/)
========================
As to your question, how to "toggle" some message to show with useState ? a general form could be
const [showMessage, setShowMessage] = useState(false)
const someEvent = () => {
// do stuff
if (someCondition) {
setShowMessage(true)
}
}
// more stuff
return (
<div>
{ showMessage && <span>I am a message!</span>}
<Foo onChange={someEvent} />
</div>
)
Explanation:
Initial render. showMessage is false, so { showMessage && <span>I am a message!</span>} will return false (which makes React render nothing)
When some event trigger our function handler we will set showMessage to true.
On next render (the reconciliation algorithm detects a change on state and fires a new render), we see showMessage as true, so the span is rendered
At the initial state, you have namevalue to be false, like so:
const [nameVal, setNameVal] = useState(false);
I'll suggest you set the new state of nameVal like so:
setNameVal(!nameVal);
I have a function that's invoked on click to update the state, but only updates when it's clicked twice. For example, if I click once, it duplicates the prior update, and only when I click once more, the state gets updated.
const select = day => {
let markedDay = day.dateString
setSelected([...selected, markedDay])
let obj = selected.reduce((c, v) => Object.assign(c, {[v]: { selected: true, disableTouchEvent: true }}), {})
setBooking(obj)
}
The purpose of the function is to 1) gather the data in an array with the setSelected hook and 2) convert it to an object with newly assigned properties using Object.assign and reduce.
When the function is invoked once, the state selected from the hook setSelected shows either empty if invoked for the very first time or the previous state. The same pattern goes for obj. I want the function to update the state upon the first invocation, and not have to be invoked twice.
Update
I've simplified the function to following, but still the same problem:
const select = day => {
let markedDay = day.dateString
setSelected({...selected, [markedDay]: { selected: true, disableTouchEvent: true }})
}
The function is for react-native-calendars and an example of selected would be:
"2019-11-18": Object {
"selected": true,
"disableTouchEvent": true,
}
Each time I select a date, I want to see the above object updated on the state, but only when I click it for the second time, it gets updated.
Setting state in React is async, so the updates won't be reflected to the state until next render. The issue with your code is that you're using the new value of state right after it's set, however the updated value will be available only on the next component render. To fix this issue you can reuse the updated value of selected before setting it to state:
const select = day => {
let markedDay = day.dateString
const newSelected = [...selected, markedDay];
let obj = newSelected.reduce((c, v) => Object.assign(c, {
[v]: {
selected: true,
disableTouchEvent: true
}
}), {})
setSelected(newSelected)
setBooking(obj);
}
Alternatively, if you want to use the updated state value in the setBooking, you'd need to make that call inside the useEffect while tracking selected variable.
const select = day => {
let markedDay = day.dateString
setSelected([...selected, markedDay]);
}
useEffect(() => {
let obj = selected.reduce((c, v) => Object.assign(c, {
[v]: {
selected: true,
disableTouchEvent: true
}
}), {})
setBooking(obj);
}, [selected])
That might be a preferable approach when you have more complex state.
I can only hazard a guess on what's happening, but I think that what we're looking at here is a race condition. I'm guessing that either setBooking or setSelected is supposed to take values from your state and adjust the rendering of the final product, yes?
The problem is that setting state in react is asynchronous. If I say
console.log(this.state.foo) // 'bar'
this.setState({foo:'fizz'})
console.log(this.state.foo) // still 'bar'
react is still setting the state in the background; it hasn't finished yet, so I get the old value.
There are three methods to fixing this error.
Method 1: Make your render statements depend on the state/call functions that depend on state. That way, whenever the state updates, what you're rendering updates.
Method 2: Pass the parameters that you wanted to store in state as arguments to your setBooking or setSelected function(s). No need to worry about race conditions if they're just receiving the values.
Method 3: You can write force setState to be synchronous.
this.setState({foo:'bar'},()=>{
//do something
})
The "do something" portion of this code will not execute until after state has been set. It has a little bit of code smell (You're not taking advantage of asynch, after all) but sometimes it's necessary if you want to re-use a function in componentDidMount as well as in the life cycle of your component.
I am trying to implement animation using a wrapper component using useState and useEffect.
If a certain value on the props change I would like that to trigger the start of the animation:
const propVal = getter(props);
const mounted = useRef(true);
useEffect(() => {
//code to initialize and start animation removed
//because a started animation uses requestAnimationFrame
//a component may want to set state for animation while not
//mounted anymore, prevent the error by setting mounted to false
return () => (mounted.current = false);
}, [propVal]);//<--if propVal changes the animation starts
Full code is here
The propVal could be the id of an item (new item will animate in) or a property called deleted indicating an item has been removed (should animate out)
I am trying to create a smaller code example to re produce the problem I am facing but was not able (yet) to do so.
The problem is that if I delete an item of then the mounted.current = false (returned callback from useEffects) part gets called even though the component did not unmount.
When I change the code to return () => false && (mounted.current = false); basically removing the safeguard to prevent unmounted components from animating then deleting an item will animate without error. This tells me that the component is not unmounted but somehow the returned callback for onUnmount is called.
Sorry for not (yet) being able to provide a simpler reproduction of this problem. Maybe someone knows why the callback would be called when the component is obviously not unmounted (removing the safe guard does not cause errors and animates as expected)
If I understood correctly, you should set the mounted ref to true in the effect.
Otherwise, any change to the propVal dependency will stop recurAnimate from working.
Say the value changes from an id => to "deleted",
The cleanup effect runs but the component isn't unmounted,
then the effect runs again with the new value, but the isMounted ref
keeps recurAnimate locked.
edit: The cleanup function returned from the effect !== componentWillUnmount.
It will run every time the effect will be rerun, and then lastly when the component unmounts.