Unable to stop window.onScroll from triggering even after component unmounts - javascript

I have 2 different kinds of NavBar components in my project: a NavBarTransparent.jsx that's only used in index.jsx (home page), and a NavBar.jsx that's used on all other pages.
Now, I have a function set to trigger each time the window scrolls in the NavBarTransparent.jsx component:
// components/NavBarTransparent.jsx
useEffect(() => {
window.removeEventListener('scroll', scrollFunction);
});
const scrollFunction = () => {
const nav = document.getElementById('nav');
const overlay = document.getElementById('nav-overlay');
const travel = document.documentElement.scrollTop;
const travelRem = travel / 16;
const navHeight = 7 - (travelRem / 6);
if (navHeight <= 4) {
nav.style.lineHeight = `${4}rem`;
overlay.style.opacity = 1;
} else if (navHeight >= 7) {
nav.style.lineHeight = `${7}rem`;
overlay.style.opacity = 0;
} else {
nav.style.lineHeight = `${navHeight}rem`;
overlay.style.opacity = (7 - navHeight) / 3;
}
};
The other navbar component (NavBar.jsx) does not have this event listener. However, the event listener still gets triggered when I visit, say, /about from / (client-side routing). Why is the listener getting hit when the other page doesn't even use that component?
The repo is up at https://github.com/amitschandillia/proost/tree/master/web

Fixed it with the following:
useLayoutEffect(() => {
if(transparent) { window.addEventListener('scroll', scrollFunction); }
// returned function will be called on component unmount
return () => {
window.removeEventListener('scroll', scrollFunction);
}
}, []);
Looks like window event listeners added before a component mounts must be explicitly removed before the component unmounts. I was adding the listener via the useEffect hook, but not unmounting it, hence the issue of persistent triggers. Now, I have used the return() snippet of useEffect (the Hooks equivalent of componentWillUnmount) to remove the listener and it works fine.
Also, am using useLayoutEffect instead of useEffect because if you want to manipulate DOM and do so before browser paint, as is the case here, useLayoutEffect` is preferable.

Related

How to force new state in React (Hooks) for textarea when clicking on another element?

codesandbox.io sandbox
github.com repository
I am creating this small Wiki project.
My main component is Editor(), which has handleClick() and handleModeChange() functions defined.
handleClick() fires when a page in the left sidebar is clicked/changed.
handleModeChange() switches between read and write mode (the two icon buttons in the left sidebar).
When in read mode, the clicking on different pages in the left sidebar works properly and changes the main content on the right side.
However, in write mode, when the content is echoed inside <TextareaAutosize>, the content is not changed when clicking in the left menu.
In Content.js, I have:
<TextareaAutosize
name="textarea"
value={textareaValue}
minRows={3}
onChange={handleMarkdownChange}
/>
textareaValue is defined in Content.js as:
const [textareaValue, setTextareaValue] = useState(props.currentMarkdown);
const handleMarkdownChange = e => {
setTextareaValue(e.target.value);
props.handleMarkdownChange(e.target.value);
};
I am unsure what is the correct way to handle this change inside textarea. Should I somehow force Editor's child, Content, to re-render with handleClick(), or am I doing something completely wrong and would this issue be resolved if I just changed the definition of some variable?
I have been at this for a while now...
You just need to update textareaValue whenever props.currentMarkdown changes. This can be done using useEffect.
useEffect(() => {
setTextareaValue(props.currentMarkdown);
}, [props.currentMarkdown]);
Problem:
const [textareaValue, setTextareaValue] = useState(props.currentMarkdown);
useState will use props.currentMarkdown as the initial value but it doesn't update the current state when props.currentMarkdown changes.
Unrelated:
The debounce update of the parent state can be improved by using useRef
const timeout = useRef();
const handleMarkdownChange = (newValue) => {
if (timeout.current) {
clearTimeout(timeout.current);
}
timeout.current = setTimeout(function () {
console.log("fire");
const items = [];
for (let value of currentData.items) {
if (value["id"] === currentData.active) {
value.markdown = newValue;
value.unsaved = true;
}
items.push(value);
}
setCurrentData({ ...currentData, items: items });
}, 200);
};
using var timeout is bad because it declares timeout on every render, whereas useRef gives us a mutable reference that is persisted across renders

ReactJS double render for a Boolean state with useState

I'm just playing around with ReactJS and trying to figure out some strange behavior with the useState hook.
A component should not re-rendered if the state is set with the same primitive value (Boolean) as it was before
const useScroll = ({positionToCross = 10}) => {
const window = useWindow();
const [isPositionCrossed, setIsPositionCrossed] = useState(window.scrollY > positionToCross);
useEffect(() => {
const onScroll = function (e) {
window.requestAnimationFrame(function () {
const lastKnownScrollPosition = window.scrollY;
setIsPositionCrossed(lastKnownScrollPosition > positionToCross);
});
}
window.addEventListener('scroll', onScroll);
return () => {
window.removeEventListener("scroll", onScroll)
}
}, []);
console.log(`useScroll - render window.scrollY = ${window.scrollY.toFixed(0)} isPositionCrossed = `, isPositionCrossed)
return {isPositionCrossed}
}
here is the console output - you can see the component and the hook are both rendered two times with "true" (after scrolled over 100px)
"useScroll - render window.scrollY = 101 isPositionCrossed = ", true
"useScroll - render window.scrollY = 103 isPositionCrossed = ", true
If you try simple code that on click handler setState and if you click two times and in each update state with same value the component again re-render.
As react doc says:
If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.
I hope the answers from this post and this github discussion help you to understand why this happens
and there are another related topics like this post and this one

How to properly write a custom react hook which depends on a value?

The goal of my hook is to determine the "open direction" of the menu.
When there is not enough height, the open direction will switch from "bottom" to "top".
I simplified my code to a dummy example to be able to demonstrate it.
You can see it in the animated gif I added.
So, what is my problem?
In my real code, the logic of the "open direction" depends on the last "openDirection" from the state.
With classes which I'm used to, it works well, I've added the code example using classes.
In the handleResize I'm writing to the console the current openDirection, to simulate the use of it in my real code.
BUT in my hooks code example, you can see that each time I am writing to the console from the handleResize, the openDirection is always the first initial value of the state which is bottom.
I guess it works this way because I'm giving the [] dependency, so useLayoutEffect works on the first time only. But what should I do? if I pass openDirection as a dependency, it will work, but then it means that each time addEventListener will be added and removeEventListener will be removed, for every resize event!
I'm really confused about it, what should I do?
import { useLayoutEffect, useState } from 'react';
const useOpenDirection = () => {
const [openDirection, setOpenDirection] = useState("bottom");
useLayoutEffect(() => {
const handleResize = () => {
console.log('openDirection', openDirection);
if (window.innerHeight < 622) {
setOpenDirection("top");
} else {
setOpenDirection("bottom");
}
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return openDirection;
};
export default useOpenDirection;
The complete code for live editing:
classes code example
hooks code example
Your openDirection has become a stale closure, because of empty array as dependencies. You can access current value of openDirection if you use functional update:
useLayoutEffect(() => {
const handleResize = () => {
setOpenDirection(current => {
// If you need you can also use current to compute return value of this function
if (window.innerHeight < 622) {
console.log('openDirection', current);
return "top";
} else {
console.log('openDirection', current);
return "bottom";
}
});
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);

How to provide window width to all React components that need it inside my app?

So I've got this hook to return the windowWidth for my App components. I'll call this Option #1.
import {useEffect, useState} from 'react';
function useWindowWidth() {
const [windowWidth,setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWindowWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowWidth;
}
export default useWindowWidth;
And right now I'm basically using it on every component that depends on the window width to render, like:
function Component(props) {
const windowWidth = useWindowWidth();
return(
// RETURN SOMETHING BASED ON WINDOW WIDTH
);
}
And since the hook has an event listener for the resize events, the component stays responsive even after window resizes.
But I'm worried that I'm attaching a new listener for every component that uses that hook and it might slow things down at some point. And I've though of other approach:
Option #2
I use the useWindowWidth() hook only one time, inside a top level component like <App/> and I'll provide the windowWidth value down the chain via context.
Like:
function App() {
const windowWidth = useWindowWidth();
return(
<WindowWidthContext.Provider value={windowWidth}>
<Rest_of_the_app/>
</WindowWidthContext.Provider>
);
}
And then, every component that needs it could get it via:
function Component() {
const windowWidth = useContext(WindowWidthContext);
return(
// SOMETHING BASED ON WINDOW WIDTH
);
}
QUESTION
Am I right in being bothered by that fact that I'm setting up multiple resize listeners with Option #1 ? Is Option #2 a good way to optmize that flow?
If your window with is used by so many components as you mentioned, you must prefer using context. As it reads below:
Context is for global scope of application.
So, #2 is perfect choice here per react.
First approach #1 might be good for components in same hierarchy but only up-to 2-3 levels.
I'm not sure if adding and removing event listeners is a more expensive operation than setting and deleting map keys but maybe the following would optimize it:
const changeTracker = (debounceTime => {
const listeners = new Map();
const add = fn => {
listeners.set(fn, fn);
return () => listeners.delete(fn);
};
let debounceTimeout;
window.addEventListener('resize', () => {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(
() => {
const width=window.innerWidth;
listeners.forEach(l => l(width))
},
debounceTime
);
});
return add;
})(200);
function useWindowWidth() {
const [windowWidth, setWindowWidth] = useState(
() => window.innerWidth
);
useEffect(
() =>//changeTracker returns a remove function
changeTracker((width) =>
setWindowWidth(width)
),
[]
);
return windowWidth;
}
As HMR said in an above thread, my solution was to use redux to hold the width value. With this strategy you only need one listener and you can restrict how often you update with whatever tool you like. You could check if the width value is within the range of a new breakpoint and only update redux when that is true. This only works if your components dont need a steady stream of the window width, in that case just debounce.

ReactJS useEffect not rendering after dependency changed

So I'm currently working with React Hooks, and I'm trying to use useEffect. It supposed whenever that dependencies changed, the useEffect would re-render right? But it doesn't work for me. Here's my code :
const [slidesPerView, setSlidesPerView] = React.useState(0)
React.useEffect(() => {
setSlidesPerView(() => (window.innerWidth <= 375 ? 1 : 2))
console.log("rerender?", slidesPerView)
}, [window.innerWidth])
Everytime I changed the screen size, useEffect won't re-render. I wonder what did I do wrong?
useEffect will respond to either props changes or state changes.
Every time screen size changes component has no idea, if window.innerWidth is changed or not, because it is not in a state or props.
To get it working you need to store window.innerWidth into state, and attach a event listener to your window, whenever window size changes it will get the window.innerWidth and store it into the state, and as state changes your useEffect will get re-run, and finally your component will get re-render.
const [size, setSize] = React.useState(window.innerWidth)
React.useEffect(() => {
//Attach event on window which will track window size changes and store the width in state
window.addEventListener("resize", updateWidth);
setSlidesPerView(() => (size <= 375 ? 1 : 2));
console.log("rerender?", slidesPerView);
//It is important to remove EventListener attached on window.
return () => window.removeEventListener("resize", updateWidth);
}, [size])
const updateWidth = () => {
setSize(window.innerWidth)
}
Demo

Categories