I am trying to add click even listener to div based class name, here the code looks,
useEffect(() => {
const element = document.querySelectorAll('some-class');
element[0].addEventListener('click',(e)=>{
If(isTrigger)
{
dispatch(setIsTrigger(false))
}
else
{
dispatch(setIsTrigger(true))
}
}, [isTrigger])
Here trying collapse and expand div class based isTrigger value. But its calling multiple times, how to avoid it?
Can some one please help
I am going to take redux out of the equation for a minute. Instead of redux lets assume you have this above the snippet you posted:
[isTrigger, setIsTrigger] = useState(false);
The effect is updating isTrigger, which is a dependency of the effect. This will cause it to go into a loop. Look below at your effect without redux:
useEffect(() => {
const element = document.querySelectorAll('some-class');
element[0].addEventListener('click',(e)=>{
if(isTrigger)
{
setIsTrigger(false)
}
else {
setIsTrigger(true)
}
}, [isTrigger]) // since its a dependency here, its going to get called everytime
// isTrigger is updated. Since your updating it in the function, you get a loop
Hope this helps explain the multiple times.
As for the solving collapse/expand, do you really need the effect? Not sure how your code looks like or your redux state(I see you are using dispatch), but a possible solution:
const isTrigger = useSelector(selectIsTrigger);
handleClick = () => {
dispatch(setIsTrigger(!isTrigger));
};
return (
... some stuff
<div click={handleClick}></div>
)
I hope this helps a little at least.
Also just a note, you should also clean up after your effect since you are adding an event listener. Effects with Cleanup. You should do this when your component unmounts. By cleaning up, you'll avoid listening to events multiple times and memory leaks.
example:
useEffect(() => {
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, [handleClick]);
Related
Occasionally I may want to unmount and remount a component with new data inside it. This could look like:
setAllPosts(undefined);
setAllPosts(newArrayOfPosts);
Because React batches state changes, depending on where the newArrayOfPosts is coming from, the state won't change. I've been able to hack a solution with a setTimeout() of 1 second and then filling in setAllPosts(), but this feels so wrong.
Is there a best practice way to tell React to slow down for a moment? or maybe to not batch update this particular state change?
P.S. I know there are better ways to do this, but I am working inside a third party environment and am pretty limited to what I have access to.
This is how you can do it -
import { flushSync } from 'react-dom';
const handleClick = () => {
flushSync(() => {
setAllPosts(undefined);
// react will create a re-render here
});
flushSync(() => {
setAllPosts(newArrayOfPosts);
// react will create a re-render here
});
};
This is used to un-batch the react states. This is just a single way of doing it. The other way could be to use setTimeout. Please note that with version 18 of react, state updates within setTimeouts are also being batched - this is known as Automatic Batching, but we still can achieve this by using different setTimeouts -
const handleClick = () => {
setTimeout(() => {
setAllPosts(undefined);
// react will create a re-render here
}, 10);
setTimeout(() => {
setAllPosts(newArrayOfPosts);
// react will create a re-render here
},20);
};
Just make sure to keep a time difference to rule out the batching done by React.
Once react 18 is available (it's currently a release-candidate) there will be a function that can force updates to not be batched: flushSync
import { flushSync } from 'react-dom';
flushSync(() => {
setAllPosts(undefined);
});
flushSync(() => {
setAllPosts(newArrayOfPosts);
});
Until then, you may need to do the setTimeout approach (though it doesn't need to be a whole second).
P.S. I know there are better ways to do this, but I am working inside a third party environment and am pretty limited to what I have access to.
Yeah, if you can do something else that would probably be better. Most of the time, if you want to deliberately unmount/remount a component, that is best achieved by using a key which you change when you want the remount to happen.
const [key, setKey] = useState(0);
const [allPosts, setAllPosts] = useState([]);
// ...
setKey(prev => prev + 1);
setAllPosts(newArrayOfPosts);
// ...
return (
<SomeComponent key={key} posts={allPosts} />
)
Occasionally I may want to unmount and remount a component with new data inside it.
It sounds like this use-case calls for a useEffect() with a dependency based on something you care about, like another piece of state or prop being provided to this component.
useEffect(() => {
setAllPosts(newArrayOfPosts);
}, [shouldUpdate]);
I've even seen examples of people triggered useEffect() with a dependency of a piece of state called count or renderCount. Not sure if this is necessarily best practice but it's one way to go about things.
const [count, setCount] = useState(0);
const [allPosts, setAllPosts] = useState([]);
useEffect(() => {
setAllPosts(props.values);
}, [count]);
const handleChange = () => {
setCount(prevCount => prevCount + 1); // This will trigger your useEffect when handleChange() in invoked
}
I know there already are already some related questions, like How can React useEffect watch and update state?, but still, I don't get it totally.
Let's say I set an index state based on a prop; and I need to sanitize that value anytime it is set.
<MyComponent index={4}/>
This is how I attempted to do it:
useEffect(() => {
setIndex(props.index);
}, [props.index]);
useEffect(() => {
const sanitized = sanitizeIndex(index);
setIndex(sanitized);
},[index])
const sanitizeIndex = index => {
//check that index exists in array...
//use fallback if not...
//etc.
return index
}
It does not work (infinite loop), since the state is watched and updated by the second useEffect().
Of course, I could avoid this by calling sanitizeIndex() on the prop, so I only need a single instance of useEffect():
useEffect(() => {
setIndex(sanitizeIndex(props.index));
}, [props.index]);
Thing is, I call setIndex plenty of times in my code, and I fear to miss using sanitizeIndex.
Is there another way to "catch" and update a state value being set ?
Thanks !
This seems like a good case for a custom hook. Here's an example of how to implement one for your case (given the information currently provided in your question), including comments about how/why:
Be sure to read the documentation for useCallback if you are not already familiar with it. It's especially important to understand how to use the dependency array (link 1, link 2) when using hooks which utilize it (like useCallback and useEffect).
<div id="root"></div><script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.16.12/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
const {useCallback, useEffect, useState} = React;
/**
* You didn't show exactly how you are sanitizing, so I'm using this function
* instead. It will simply make sure the input number is always even by
* adding 1 to it if it's odd.
*/
function makeEven (n) {
return n % 2 === 0 ? n : n + 1;
}
function useSanitizedIndex (sanitizeIndex, unsanitizedIndex) {
const [index, setIndex] = useState(sanitizeIndex(unsanitizedIndex));
// Like setIndex, but also sanitizes
const setSanitizedIndex = useCallback(
(unsanitizedIndex) => setIndex(sanitizeIndex(unsanitizedIndex)),
[sanitizeIndex, setIndex],
);
// Update state if arguments change
useEffect(
() => setSanitizedIndex(unsanitizedIndex),
[setSanitizedIndex, unsanitizedIndex],
);
return [index, setSanitizedIndex];
}
function IndexComponent (props) {
// useCallback memoizes the function so that it's not recreated on every
// render. This also prevents the custom hook from looping infinintely
const sanitizeIndex = useCallback((unsanitizedIndex) => {
// Whatever you actually do to sanitize the index goes in here,
// but I'll just use the makeEven function for this example
return makeEven(unsanitizedIndex);
// If you use other variables in this function which are defined in this
// component (e.g. you mentioned an array state of some kind), you'll need
// to include them in the dependency array below:
}, []);
// Now simply use the sanitized index where you need it,
// and the setter will sanitize for you when setting it (like in the
// click handler in the button below)
const [index, setSanitizedIndex] = useSanitizedIndex(sanitizeIndex, props.index);
return (
<div>
<div>Sanitized index (will always be even): {index}</div>
<button onClick={() => setSanitizedIndex(5)}>Set to 5</button>
</div>
);
}
function Example () {
const [count, setCount] = useState(0);
return (
<div>
<div>Count: {count}</div>
<button onClick={() => setCount(n => n + 1)}>Increment</button>
<IndexComponent index={count} />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>
So think of useEffect like an event listener in javascript. It's not the same thing, but think of it like that. The event or "what's being watched", in this case, you've asked it to watch props.index. It will run that function inside the useEffect every time anything in the dependency array (props.index - in your case) changes. So what's happening here is you're updating props.index every time props.index changes. This is your infinite loop.
Couple things here, create a copy of props.index as something ie.
const [something, setSomething = useState(props.index);
(I won't get into destructuring, but worth looking that up too)
You don't want to manipulate your props directly the way you are doing.
This solves that, as well as gives you the correct way to look at your useEffect. Since you want to update something whenever that prop changes, you could leave props.index (again look up destructuring) in your dependency array, and change the useEffect to:
const [something, setSomething] = useState(props.index);
useEffect(() => {
setSomething(props.index);
}, [props.index]);
As another pointed out, this is difficult without knowing exactly what you're doing, but this is kind of an overview which hopefully helps you understand what is going on here and why you're getting a loop here.
You mentioned you fear missing out on sanitizing, then you should not be using setIndex directly. Instead, you can create a new function to santize and set the index.
useEffect(() => {
setSanitizeIndex(props.index);
}, [props.index]);
const setSanitizeIndex = (value) => {
const sanitizeIndex = sanitizeIndex(value);
setIndex(sanitizeIndex)
}
With that, you should not be calling setIndex any more in your codes, but only call setSanitizeIndex.
One potential solution for this that I have done in the past with a similar issue is to indirectly trigger the useEffect. Create a dummy state that does not relate to the state being updated and update the dummy state whenever you want the effect to be triggered.
const [dummyState, setDummyState] = useState(0)
useEffect(() => {
setIndex(props.index);
}, [props.index]);
useEffect(() => {
const sanitized = sanitizeIndex(index);
setIndex(sanitized);
},[dummyState])
const sanitizeIndex = index => {
//check that index exists in array...
//use fallback if not...
//etc.
return index
}
return (
//update the state you want to update then update the dummy state to
//trigger the useEffect
<button onClick={() =>{setIndex(123);
setDummyState(dummyState++);}/>
)
I think the accepted answers solution is a lot less janky but in more complex situations this might be your easiest and most easy-to-understand solution
So, I'm using hooks to manage the state of a set of forms, set up like so:
const [fieldValues, setFieldValues] = useState({}) // Nothing, at first
When setting the value, the state doesn't update:
const handleSetValues = values => {
const _fieldValues = {
...fieldValues,
...values
}
setFieldValues(_fieldValues)
console.log(fieldValues) // these won't be updated
setTimeout(() => {
console.log(fieldValues) // after ten seconds, it's still not updated
},10000)
}
If I call the function a second time, it'll have updated, but that's not gonna work for me.
I never saw behaviour like this in class components.
Is it meant to... like, not update? Or just update whenever it feels like it? Really confusing behaviour.
setFieldValues(_fieldValues) is an async call, means you won't able to get the result in the very next line of this.
You can use useEffect hook.
useEffect(() => {
// do your work here
}, [fieldValues]);
It seems from your question that you have background of Class components of React, so useEffect is similar to componentDidMount and componentDidUpdate lifecycle methods.
useEffect calls whenever the state in the dependency array (in your case [fieldValues]) changes and you get the updated value in useEffect body.
You can also perform componentWillUnmount work in useEffect as well.
Have a brief guide.
setFieldValues is an asynchronous function, so logging the value below the statement will not have any effect.
Regarding using setTimeout, the function would capture the current value of props being passed to it and hence that would be the value printed to the console. This is true to any JS function, see the snippet below:
function init(val) {
setTimeout(() => {
console.log(val);
}, 1000);
}
let counterVal = 1;
init(counterVal);
counterVal++;
So how can we print the values when the value changes? The easy mechanism is to use a useEffect:
useEffect(() => {
console.log(fieldValues)
}, [fieldValues]);
I have a question about using setInterval inside functional components and the behaviour of functions inside the aforementioned components.
I have the following piece of code (stripped and simplified to show the problem only):
let timerInterval;
const ModuleAssignedUtils = (props) => {
const [timer, setTimer] = useState(0);
updateTimer = () => {
let timer_tmp;
if (timer <= 0) {
clearInterval(timerInterval)
setTimer(0)
setButtonPressed(false);
}
else {
timer_tmp = timer - 1;
setTimer(timer_tmp)
}
}
const locateVehicle = () => {
if (!buttonPressed) {
setButtonPressed(true);
setTimer(10);
timerInterval = setInterval(() => {
updateTimer();
}, 1000);
}
}
return (
...
<ButtonContainer timer={timer}
noButtonLoading
onEvent={locateVehicle} />
...
)
}
This code actually does the job. User presses the button, which triggers the function locateVehicle, which in turn sets the timer in state to 10 and then starts the interval with the updateTimer function.
However, as soon as I change the updateTimer definition to include const or function, this piece of code stops working. The state is set, but updateTimer is always called with timer = 0, so it doesn't know that the state has been changed.
I struggle to understand the difference between declaring the function without any proper declaration and actually declaring it properly with const or function keywords. Would appreciate a lot if someone could explain me this behaviour.
P.S. I have since refactored this component and am using useInterval custom hook as suggested by Dan Abramov, so everything is fine. Just trying to understand the logic behind the behaviour in this particular case.
Thanks in advance!
The reason it stopped working is that your context (the good old this) is not set up.
When you write down an arrow function expressions such as your updateTimer, it automatically binds up your cotext, just like using the bind function.
You will encounter with the same issue in case of function declaration inside the setInterval method.
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 1 year ago.
Improve this question
I have a use case that I'd like to solve it with React hooks, but it's not working as I expected.
Expectation: Not on top will be shown on scrolling down and will disappear when scrolling back on top of the document.
Current: Not on top shown on scrolling down but not disappear when scrolling back on top of the document.
Code sandbox link: https://codesandbox.io/s/wkroy1r2xl
Thanks in advance.
The component adds the event listener once: only on mount. At that time, it adds the handleScroll listener that is "captured" with that specific isHeaderMoved value at that specific point in time. Whenever the page is scrolled, the callback that will be called is the one with that initial isHeaderMoved value. The current code basically calls handleScroll with false over and over.
In short, you need to add isHeaderMoved as a dependency to the effect. You should also move the handleScroll listener inside the effect, so its dependencies can be tracked more properly by the hooks eslint plugin.
useEffect(
() => {
const handleScroll = () => {
if (window.pageYOffset > 0) {
if (!isHeaderMoved) {
setIsHeaderMoved(true)
}
} else if (isHeaderMoved) {
setIsHeaderMoved(false)
}
}
window.addEventListener("scroll", handleScroll)
return () => {
window.removeEventListener("scroll", handleScroll)
}
},
[isHeaderMoved],
)
Refer to these sections from Dan Abromov's posts on the subject (which have examples that can explain it better than I could):
https://overreacted.io/a-complete-guide-to-useeffect/#each-render-has-its-own-event-handlers
https://overreacted.io/a-complete-guide-to-useeffect/#each-render-has-its-own-effects
https://overreacted.io/a-complete-guide-to-useeffect/#moving-functions-inside-effects
It's worth going through the whole post to understand things fully, if you have the time.
From skyboxer's comment, it occurred to me that this would also work, but I'll keep all of the above for informational purposes.
useEffect(() => {
const handleScroll = () => {
setIsHeaderMoved(window.pageYOffset > 0)
}
window.addEventListener("scroll", handleScroll)
return () => {
window.removeEventListener("scroll", handleScroll)
}
}, [])
This should work, you have to pass the window.pageYOffset or isHeaderMoved to the useEffect to fire it in every time it changes, if you left it [] it'll fire once.
useEffect(
() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
},
[isHeaderMoved]
);
I think you also can use useRef() to do this.
I had the same annoying problem once
try adding a key prop on the component when it is created in the parent code
<yourcomponent key="some_unique_value" />
This is because in most cases, especially when your component is reused, it may usually re-render only with some changes instead of actually creating it again when you reuse it, based on the way it's created, Hence the useEffect is not going to be called. eg inside Switch, Router, loops, conditionals...
So adding a key will prevent this from happening. If it is in a loop, you need to make sure each element is unique, maybe by including the index i in the key if you can't find any better unique key.