React useEffect runs every time I scroll after using hooks - reactjs

I have a hook to save scroll value on window scroll.
(BTW, my state object is more complicated than this example code. That's why I'm using useReducer. I simplified the state for this question.)
Then when I attached that hook to App, everything inside useEffect runs every time I scroll.
I get that console.log(scroll) prints every time on scroll, but why is console.log("somethingelse") also prints every time on scroll?
I have other codes to put inside useEffect, so it's a huge problem if everything inside useEffect runs on scroll.
How do I make only useScroll() related code to run on scroll, and everything else runs only on re-render like useEffect supposed to do?
I made CodeSandbox example below.
function App() {
const scroll = useScroll();
useEffect(() => {
console.log(scroll);
console.log(
"this also runs everytime I scroll. How do I make this run only on re-render?"
);
});
return ( ... );
}

The way your code is currently written the useEffect code will be executed on each update. When scrolling the state is updated, so the effect will be executed.
You can pass props as the second variable to the useEffect to determine whether it should run or not.
So, if you create an effect like this it will run every time the scroll is updated
useEffect(() => {
console.log('This wil run on mount when scroll changes')
}, [scroll]) // Depend on scroll value, might have to be memoized depending on the exact content of the variable.
useEffect(() => {
console.log('This will only run once when the component is mounted')
}, []) // No dependencies.
useEffect(() => {
console.log('This will run on mount and when `someStateVar` is updated')
}, [someStateVar]) // Dependent on someStateVar, you can pass in multiple variables if you want.
Without passing the second argument, the effect will always run on every render. You can add multiple effects in a single component. So, you'll probably want to write one containing the stuff that needs to be executed when scroll updates and create one or more others that only run on mount of after some variable changes.

There is a concept of cleanup functions which can be attached to an useEffect hook. This cleanup function will be run once the rerender is complete. It's usually used in scenarios where you need to unsubscribe from a subscription once the render is complete. Below is the sample code from React website
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// Clean up the subscription
subscription.unsubscribe();
};
});
More details can be found in the blogpost
useEffect Hook

Related

How to implement promise when updating a state in functional React (when using useState hooks)

I know similar questions are bouncing around the web for quite some time but I still struggle to find a decision for my case.
Now I use functional React with hooks. What I need in this case is to set a state and AFTER the state was set THEN to start the next block of code, maybe like React with classes works:
this.setState({
someStateFlag: true
}, () => { // then:
this.someMethod(); // start this method AFTER someStateFlag was updated
});
Here I have created a playground sandbox that demonstrates the issue:
https://codesandbox.io/s/alertdialog-demo-material-ui-forked-6zss6q?file=/demo.tsx
Please push the button to get the confirmation dialog opened. Then confirm with "YES!" and notice the lag. This lag occurs because the loading data method starts before the close dialog flag in state was updated.
const fireTask = () => {
setOpen(false); // async
setResult(fetchHugeData()); // starts immediately
};
What I need to achieve is maybe something like using a promise:
const fireTask = () => {
setOpen(false).then(() => {
setResult(fetchHugeData());
});
};
Because the order in my case is important. I need to have dialog closed first (to avoid the lag) and then get the method fired.
And by the way, what would be your approach to implement a loading effect with MUI Backdrop and CircularProgress in this app?
The this.setState callback alternative for React hooks is basically the useEffect hook.
It is a "built-in" React hook which accepts a callback as it's first parameter and then runs it every time the value of any of it's dependencies changes.
The second argument for the hook is the array of dependencies.
Example:
import { useEffect } from 'react';
const fireTask = () => {
setOpen(false);
};
useEffect(() => {
if (open) {
return;
}
setResult(fetchHugeData());
}, [open]);
In other words, setResult would run every time the value of open changes,
and only after it has finished changing and a render has occurred.
We use a simple if statement to allow our code to run only when open is false.
Check the documentation for more info.
Here is how I managed to resolve the problem with additional dependency in state:
https://codesandbox.io/s/alertdialog-demo-material-ui-forked-gevciu?file=/demo.tsx
Thanks to all that helped.

Why is this event listener not removed?

Based on this popular answer https://stackoverflow.com/a/19014495 I have the following code.
I am trying to understand why "remove effect" is never logged.
function useWindowSize(){
const [size, setSize] = useState([0,0]);
useLayoutEffect(() => {
console.log("use effect");
function updateSize(){
setSize([window.innerWidth, window.innerHeight]);
}
window.addEventListener('resize', updateSize);
updateSize();
return () => {
console.log("remove effect");
window.removeEventListener('resize', updateSize);
}
}, []);
return size;
}
This custom hook is used in a function component
function InfiniteScroll () {
const [width, height] = useWindowSize();
// rest of code should be irrelevant
}
Based on the React documentation an empty array as second argument for the built in hooks means that the effect is used and then cleaned up only once. https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects. I was surprised therefore that this is used in this code snippet because the event listener would be removed immediately. However in testing I discovered that whilst "use effect" is logged "remove effect"is not. Why? What other concept am I missing?
Cleanup effect with an empty dependency array runs on component unmount.
It also mentioned in the docs you shared:
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.
So having conditional rendering will show the log
const [show,toggle] = useReducer(p=>!p,true);
// Will log "remove effect" on show === false
<>
<button onClick={toggle}>toggle</button>
{show && <InfiniteScroll />}
</>
For more info see useEffect use cases.
The return part in the useEffect hook is a clean-up process that is only happening when the targeted component is unmounting, That is what you're missing try to navigate away or destroy this component and you should see the log message.
I already tried the snippet you showed and I think it's working as expected and showing the remove effect log once.
according to the react docs,
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run.
so on mounting this component the listener already added to window once, and then as long as you are still in the same component you have access to the event listener. on unmount the remove effect will be logged as well. you have to unmount the component to see the remove effect log

React JS | using setTimeout prints a number on Page

I am simply trying to change the heading and content states using React Hooks but I get a number shown on the page, a little google search showed up a bunch of stuff related to how setInterval and Timeout generate a key or value but I have no idea why they're showing up on the page.I can hide it using an empty div but I am curious if I am doing anything wrong, also if I use a class instead of a function the value rapidly increases and my CPU maxes out.
function MyComponent (){
const [heading, setHeading] = useState('React(Loading)')
const [content, setContent] = useState('Loading...')
return(
<main>
<h1>{heading}</h1>
<p>{content}</p>
{
setTimeout(() =>{
setHeading('React(Loaded)')
setContent('Loaded')
}, 2000)} // should change the values, which it does with addition of a number
</main>
);
}
The resulting page is that renders is here
Also on a side note I tried using a onload function to do the same thing but nothing happens.
setTimeout returns a number, which is used to identify the timeout when you use clearTimeout. That is why you see the number below the content.
To hide the number, you should move the setTimeout to be outside of the return function. Also, you should use as little JS as possible in the return statement and just use JSX over there, to make the component more clear and readable.
But just moving the setTimeout to be before the return statement is not enough. The function will run on every render, and there are many things that can trigger a re-render - a state change, or a parent re-rendering. So on every re-render, you will set a new timeout. The timeout itself updates a state which triggers a render which triggers the setTimeout - so you are creating an infinite loop.
So you want to call setTimeout only once - you can use useEffect, which will re-run only when the dependency array changes, but if you will leave it empty, it will run only once, because nothing will change and a re-run will never be triggered.
function MyComponent (){
const [heading, setHeading] = useState('React(Loading)')
const [content, setContent] = useState('Loading...')
useEffect((
setTimeout(() =>{
setHeading('React(Loaded)')
setContent('Loaded')
}, 2000)
), []);
return(
<main>
<h1>{heading}</h1>
<p>{content}</p>
</main>
);
}
So, by using the above answer we get the following error
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function
The reason for this is again the fact that setTimeout returns a number, the final answer is to use the code as a separate function as below:
useEffect( timeOutFunction, [])
function timeOutFunction() {
setTimeout(() => {
setHeading('React(Loaded)')
setContent('Loaded'), 2000)
}

Save React Component scroll position in Redux on Component update

I have a React Component using a hook to save the scroll position of the component when the component unmounts. This works great but fails when navigating from one set of data to another set of data without the component unmounting.
For instance, imagine the Slack Interface where there is a sidebar of message channels on the left and on the right is a list of messages (messageList). If you were to navigate between two channels, the messageList component would update with a new set of data for the messageList, but the component was never unmounted so scroll position never gets saved.
I came up with a solution that works, but also throws a warning.
My current useEffect hook for the component (stripped down) and the code that currently saves scroll position whenever the messageList ID changes:
// Component...
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
// Save scroll position when Component unmounts
useEffect(() => {
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, []);
// Save scroll position when Parent ID changes
const oldParent = usePrevious(parent);
if (oldParent && parent._id !== oldParent._id) {
setScrollOffset(oldParent._id, list ? list.scrollTop : 0);
}
// ...Component
The error this throws is:
Warning: Cannot update a component from inside the function body of a different component.
And the line that is causing it is the setScrollOffset call inside of the last if block. I'm assuming that while this works it is not the way that I should be handling this sort of thing. What is a better way to handle saving scroll position when a specific prop on the component changes?
Add parent._id to the dependency array. Refactor your code to still cache the previous parent id, add that to the dependency, and move the conditional test inside the effect.
Cleaning up an effect
The clean-up function runs before the component is removed from the UI
to prevent memory leaks. Additionally, if a component renders multiple
times (as they typically do), the previous effect is cleaned up before
executing the next effect.
// Return previous parent id and cache current
const oldParent = usePrevious(parent);
// Save scroll position when Component unmounts or parent id changes
useEffect(() => {
if (oldParent && parent._id !== oldParent._id) {
setScrollOffset(oldParent._id, list ? list.scrollTop : 0);
}
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, [parent._id, oldParent]);
If this does't quite fit the bill, then use two effects, one for the mount/unmount and the other for just updates on the parent id.
Thanks to the suggestions of #drew-reese, he got me pointed down the right path. After adopting his solution (which previously I could not get working properly), I was able to isolate my problem to usage with react-router. (connected-react-router in my case). The issue was that the component was rendering and firing the onScroll event handler and overwriting my scroll position before I could read it.
For me the solution ended up being to keep my existing useEffect hook but pull the scroll offset save out of it and into useLayoutEffect (Had to keep useEffect since there is other stuff in useEffect that I removed for the sake of keeping the sample code above lean). useLayoutEffect allowed me to read the current scroll position before the component fired the onScroll event which was ultimately overwriting my saved scroll position reference to 0.
This actually made my code much cleaner overall by removing the need for my usePrevious hook entirely. My useLayoutEffect hook now looks like this:
useLayoutEffect(() => {
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, [parent._id]);

Why useEffect doesn't fire on every render?

My component has two Rect.useEffect hook
const Example = ({ user }) => {
React.useEffect(() => {
autorun(() => {
console.log("inside autorun", user.count);
});
});
// Only runs once
React.useEffect(() => {
console.log("Why not me?");
});
return <Observer>{() => <h1>{user.count}</h1>}</Observer>;
};
I update this component using mobx. It is re-rendered correctly. But "Why not me?" is printed only once.
As per official docs
By default, effects run after every completed render
This means console.log("Why not me?"); too should run every time prop user is updated. But it doesn't. The console output is this
What's the reason behind this apparent inconsistency?
My complete code can be viewed here
In Mobx, just like Observer component which provides a render function callback, autorun function also executes independently of the react lifecycle.
This behaviour happens because you have user count as a observable variable.
According to the mobx-react docs
Observer is a React component, which applies observer to an anonymous
region in your component. It takes as children a single, argumentless
function which should return exactly one React component. The
rendering in the function will be tracked and automatically
re-rendered when needed.
and mobx docs
When autorun is used, the provided function will always be triggered
once immediately and then again each time one of its dependencies
changes.
You can confirm this behvaior by logging directly inside the functional component and you will observer that the component is only rendered once
EDIT:
To answer your question
If I change useEffect to this
React.useEffect(autorun(() => {console.log("inside autorun", user.count)}));
basically remove anonymous function from useEffect and just pass
autorun directly, then it is run only once. Why is it so? What's the
difference?
The difference is that autorun returns a disposer function which when run will dispose of the autorun and would not longer execute it.
From the docs:
The return value from autorun is a disposer function, which can be
used to dispose of the autorun when you no longer need it.
Now what happens is that since useEffect executes the callback provided to it when it runs, the callback executed is the disposer function returned by autorun which essentially cancels your autorun.
It looks like your component doesn't rerender. autorun receives a callback and might call it independently from render.
Example component rerenders only when its parent rerenders or when its props change.
Use this code to observe what's really happening:
const Example = ({ user }) => {
console.log('render');
React.useEffect(() => {
console.log('useEffect autorun');
autorun(() => {
console.log("inside autorun", user.count);
});
});
// Only runs once
React.useEffect(() => {
console.log("Why not me?");
});
return <Observer>{() => <h1>{user.count}</h1>}</Observer>;
};

Resources