Having some troubles getting useEffect, useCallback and setTimeout to work together - javascript

Trying to implement some custom scroll behaviour for a slider based on the width. It's using useState, to keep track of the current and total pages inside the slider. This is the code I have ended up with trying to acomplise it but it has some unexpected behaviour.
const [isTimeoutLocked, setIsTimeoutLocked] = useState(false)
const handleScroll = useCallback(() => {
const gridContainer = document.querySelector(".grid");
const totalPages = Math.ceil(
gridContainer.scrollWidth / gridContainer.clientWidth + -0.1
);
setTotalPages(totalPages);
const scrollPos = gridContainer.clientWidth + gridContainer.scrollLeft + 2;
if (gridContainer.scrollWidth > scrollPos) {
gridContainer.scrollBy({
left: gridContainer.clientWidth + 20.5,
behavior: "auto",
block: "center",
inline: "center",
});
setCurrentPage(currentPage + 1);
setIsTimeoutLocked(false)
} else {
gridContainer.scrollTo(0, 0);
setCurrentPage(1);
setIsTimeoutLocked(false)
}
},[currentPage]);
useEffect(() => {
if(!isTimeoutLocked){
setTimeout(() => {
setIsTimeoutLocked(true)
document.querySelector(".grid") && handleScroll();
}, 5000);
}
}, [currentPage, displayDate, isTimeoutLocked, handleScroll]);
The problem here is that when the currentPage is displayed in the ui it will reset back to one if the else runs inside the handleScroll function which is totally fine but then it will result back to the previous value instead of going back to 2 when it gets to the next page. Also since I have added the isTimeoutLocked as a dependency to the useEffect since it was asking for it the setTimeout will run more often but I only want to have it as a dependency to get the correct value not so the useEffect runs everytime it changes. The purpose of the isTimeoutLocked is so when you change the content inside the container by changing the current day it has not registered a bunch of timeouts.

You should add a cleanup function to your useEffect. Since you mutate isTimeoutLocked in your useEffect it can force multiple setTimeouts to run and that is probably the result of wierd behaviour.
When using setTimeout inside useEffect it is always recommended to use it with cleanup. Cleanup will run before the next useEffect is triggered, that gives you a chance to bail out of next setTimeout triggering. Code for it is this:
useEffect(() => {
if(!isTimeoutLocked){
const tid = setTimeout(() => {
setIsTimeoutLocked(true)
document.querySelector(".grid") && handleScroll();
}, 5000);
// this is the cleanup function that is run before next useEffect
return () => clearTimeout(tid);
}
You can find more information here: https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup .
Another thing to be careful is the async nature of setState and batching. Since you are using setState inside a setTimeout, React will NOT BATCH them, so EVERY setState inside your handleScroll will cause a rerender (if it was not inside a setTimeout/async function React would batch them). Since you have alot of interconected states you should look into useReducer.

Related

With flushSync() inside useEffect do we do the same as using useLayoutEffect?

https://reactjs.org/docs/react-dom.html#flushsync
Force React to flush any updates inside the provided callback
synchronously. This ensures that the DOM is updated immediately.
// Force this state update to be synchronous.
flushSync(() => {
setCount(count + 1);
});
// By this point, DOM is updated.
Knowing that, it is the same as using useLayoutEffect, or do I misunderstand flushSync()?
const App = () => {
const [name, setName] = React.useState("Leonardo");
React.useEffect(() => {
ReactDOM.flushSync(() => {
for (let index = 1; index <= 100000; index++) { // for simulate blocking
console.log(index);
}
setName("Jose");
});
});
return (
<div>
<h1>Hello {name}</h1>
</div>
);
};
¿it is the same that this?
React.useLayoutEffect(() => {
for (let index = 1; index <= 100000; index++) {
console.log(index);
}
setName("Jose");
});
useLayoutEffect is useful for things that need to happen before paint your dom or when your code is causing flickering. it's already synchronous and executed always before every useEffect hook in your code.
flushSync is used to convert the setState into synchronous. in 99% of cases you will use flushSync inside a handler like a form submit handler, outside of useEffect to execute an imperative action
function handleSubmit(values) {
flushSync(() => {
setForm(values);
});
}
Be aware that flushSync force a re-rendering, so use it carefully
The common use case of flushSync is update the DOM after settings the state immediately. example scroll to the new added element in the list
flushSync(() => {
setElements((elements) => [
...elements,
{
id: 'random',
},
]);
});
// scroll to element here
Check this example https://codesandbox.io/s/react-18-batching-updates-flushsync-forked-vlrbq8. you can delete flushSync and see the diff
flushSync is used to force React to flush a state update and when you try to put it inside useEffect it won't affect when useEffect is invoked, it will always be after the changes have been reflected on the browser, whereas useLayoutEffect is invoked before and this is the main difference between them.
so flushSync is not a function that is supposed to be executed inside useEffect you will even get this warning
Warning: flushSync was called from inside a lifecycle method. React cannot flush when React is already rendered. Consider moving this call to a scheduler task or microtask.
They are not same. Your code might give the same performance, functional, and results, yet they are different in nature.
flushSync is a low-level API that flushes updates to the React DOM immediately, bypassing the normal scheduling mechanism. It should be used sparingly and only when necessary, as it can lead to poor performance and is more prone to bugs and inconsistencies. On the other hand, useLayoutEffect is a higher-level hook that schedules a DOM update to happen synchronously after all other updates have been processed. It is generally the preferred way to ensure that updates are synchronized with the layout of the page.
I did this code to see if I understood everything, please correct me:
function App2() {
const [c, setC] = React.useState(0);
const inc1 = () => {
/*setC is asynchronous (withoutFlushSync), so it continues the
code downwards and therefore from the DOM it
brings us the value without increment in 1*/
setC((c) => c + 1);
console.log(document.getElementById("myId").innerText); // old value from DOM
console.log(c); // However below log will still point to old value ***BECAUSE OF CLOSURE***
};
const inc2 = () => {
/*waits until the DOM is modified with the new state (in the first click
c = 1)*/
ReactDOM.flushSync(() => { // Wait
setC((c) => c + 1);
});
/* brings the c = 1 of the DOM because it waited until the DOM
was modified with the new state incremented by one. */
console.log(document.getElementById("myId").innerText); // new value from DOM
console.log(c); // However below log will still point to old value ***BECAUSE OF CLOSURE***
};
return (
<div className="App">
Count: <div id="myId">{c}</div>
<button onClick={inc1}>without flushSync</button>
<button onClick={inc2}>with flushSync</button>
</div>
);
}

Infinity state&component updating

I tried to create simple timer with useEffect and setInterval. In theory, on click it has to:
restart cooldown
const restartCooldown = () => {
setCooldownDuration(targetActivity['duration'])
}
useEffect() will see this and decrease cooldown every second within interval
useEffect(() => {
let interval
if (props.activity.cooldown) {
if (cooldownDuration > 0) {
if (props.activity.id === 1) {
console.log('Activity.js state', cooldownDuration)
}
interval = setInterval(() => setCooldownDuration(cooldownDuration - 1000), 1000);
} else {
clearInterval(interval)
}
} else {
setCooldownDuration(0)
}
});
useEffect() in ActivityProgression.js will get percentage of completion
useEffect(() => {
const interval = setInterval(() => setPercentages((props.totalTime - props.cooldown) / props.totalTime * 100), 1000);
})
4. Then it will be rendered
return <ProgressBar animated now={percentages} label={`${percentages}%`} />;
But in fact it doesn't work properly, creating infinity multiple renders on click.
Can you please tell me why? Much much thanks.
Full code repository to try it yourself
I tried everything, but it seems that i simply don't understand some react features :(
The second argument of useEffect is an array of dependencies. Since you didn't provide any dependencies, it runs every render. Because it updates the state, it causes a re-render and thus runs again, infinitely. Add [cooldownDuration] to your use effect like this:
useEffect(() => {...}, [cooldownDuration])
This will make the useEffect only run when cooldownDuration changes.

Too many re-renders even though I want an infinite loop

I'm trying to create a simple idle game so that I can dive deeper into web development.
However, react is complaining about too many re-render, but I do want to re-render every second.
This is the code I have at the moment.
> const Game = () => { const [resourceCopper, setResourceCopper] =
> useState(0);
>
> const gatherCopper = () => {
> setResourceCopper(resourceCopper + 1); };
>
> setInterval(gatherCopper(), 1000);
>
> return (
> <section className="main-window-container">
> <section className="left-container">
> <h3>Resources:</h3>
> <p>Copper: {resourceCopper}</p>
> </section>
The immediate issue is that you're immediately executing gatherCopper, which immediately updates the state, rerender, and will cause an infinite loop.
You should remove the () behind gatherCopper in the setInterval call.
However, this code is very leaky, and because of the way React works you will create a new interval for every time the component renders. This will not work as expected.
The interval should be moved to a a React hook (i.e. useEffect), perhaps there's even hooks which wrap setInterval. A google search will probably come up with some good examples.
With React hooks you'll be able to start the interval when the component mounts for the first time, and you can also tell it to cancel the setInterval when the component unmounts. This is important to do.
update: Example with annotations (sandbox link)
const Game = () => {
const [resourceCopper, setResourceCopper] = useState(0);
useEffect(() => {
// We move this function in here, because we only need this function once
const gatherCopper = () => {
// We use the 2nd way of calling setState, which is a function that will
// receive the current value. This is so that we don't have to use the resourceCopper
// as a dependency for this effect (which would fire it more than once)
setResourceCopper(current => current + 1);
};
// We tell the setInterval to call the new gatherCopper function
const interval = setInterval(gatherCopper, 1000);
// When an effect returns a function, it will be used when the component
// is cleaned up (a.k.a. dismounts). We want to be neat and cancel up the interval
// so we don't keep calling this on components that are no longer there
return () => clearInterval(interval);
}, [setResourceCopper]);
return (
<section className="main-window-container">
<section className="left-container">
<h3>Resources:</h3>
<p>Copper: {resourceCopper}</p>
</section>
</section>
);
};

React Hooks: State always setting back to initial value

Switching over to react hooks and using them for the first time. My state always seems to be set back to the initial value I pass it (0). Code is to have a page automatically scroll down and up. The page is just practice to displaying various file types. What happens is the scrollDir variable will switch to being set to either 1 or -1 and 0. So the console will display 1,0,1,0,1,0,1,0 etc... How do I get the state to stay during an update?
function App(props) {
const [scrollDir, setScrollDir] = useState(0);
function scrollDown() {
if(document.documentElement.scrollTop < 10)
{
setScrollDir(1);
}
else if(document.documentElement.scrollTop >= document.documentElement.scrollHeight - window.innerHeight)
{
setScrollDir(-1);
}
window.scrollBy(0, scrollDir);
}
useEffect(() => {
setInterval(scrollDown, 100);
});
return (
<StackGrid monitorImagesLoaded={true} columnWidth={"33.33%"} >
<img src={posterPNG} />
<img src={posterJPG} />
<img src={posterGIF} />
<video src={posterMP4} loop autoPlay muted />
<Document file={posterPDF}>
<Page pageNumber={1} />
</Document>
</StackGrid>
);
}
useEffect hook takes second argument, an array.
If some value in that array changes then useEffect hook will run again
If you leave that array empty useEfect will run only when component mount - (basicaly like ComponentDidMount lifecycle method)
And if you omit that array useEffect will run every time component rerenders
For example:
useEffect runs only once, when component mounts:
useEffect(() => {
//code
},[]);
useEffect runs everytime when component rerenders:
useEffect(() => {
//code
});
useEffect runs only when some of these variables changes:
let a = 1;
let b = 2;
useEffect(() => {
//code
},[a,b]);
Also, if you set return statement in useEffect hook, it has to return a function, and that function will always run before useEffect render
useEffect(() => {
// code
return () => {
console.log("You will se this before useEffect hook render");
};
}, []);
Using setInterval with hooks isn't very intuitive. See here for an example: https://stackoverflow.com/a/53990887/3984987. For a more in-depth explanation you can read this https://overreacted.io/making-setinterval-declarative-with-react-hooks.
This useInterval custom hook is pretty easy to use as well https://github.com/donavon/use-interval. (It's not mine - I ran across this after responding earlier)

Is it bad to reapply all event listeners when state changes in React

I have a hook that lets me change the page in my React app, using the scroll wheel or the keyup event. At the end, I came to this solution, which works:
function useScrollingPage (pages) {
const [page, setPage] = useState(0);
const wheelHandler = (event) => {
// some logic
setPage(page + 1);
};
useEffect(
() => {
window.addEventListener(EVENTS.WHEEL, wheelHandler);
window.addEventListener(EVENTS.KEY_UP, keyHandler);
return () => {
window.removeEventListener(EVENTS.WHEEL, wheelHandler);
window.removeEventListener(EVENTS.KEY_UP, keyHandler);
};
},
[page],
);
return page;
}
But, while working on it, I ran into a few iterations.
I would call addEventListener outside the useEffect. This way, everything works, but the listener is never removed.
I would pass an empty array as the second argument of useEffect. That way, it only runs on component mount, but it doesn't change the page reference in wheelHandler, so it will always try to set the page to 1 (setPage(0 + 1))
Run useEffect every time the page changes. It works, but it doesn't feel right to remove and add all listeners every time the state changes.
Is it bad for performance, to add/remove event listeners every time the state changes? What would be the best solution for a problem like this?
According to the docs, when the listeners depends on state or props to subscribe you would want to make use of the above pattern, However in the above case you could do with the callback pattern of state updates and pass an empty array as the second argument of useEffect
function useScrollingPage (pages) {
const [page, setPage] = useState(0);
const wheelHandler = (event) => {
// some logic
setPage(page => page + 1);
};
useEffect(
() => {
window.addEventListener(EVENTS.WHEEL, wheelHandler);
window.addEventListener(EVENTS.KEY_UP, keyHandler);
return () => {
window.removeEventListener(EVENTS.WHEEL, wheelHandler);
window.removeEventListener(EVENTS.KEY_UP, keyHandler);
};
},
[],
);
return page;
}
However when you write the above logic, make sure that you aren't using state in the wheelHandler. If you are, then you would follow the pattern that works for you and is recommended.

Categories