I'm referencing an element in a hook that measures the elements dimensions. When I first load the page. The element reference number logs undefined, but then defines itself.
The dimensions are used to position an absolutely positioned element into the center. When the page first loads, the element is centered. As the window size is changed, the element remains centered.
However, when I click a NavLink from react-dom-router and click back to the home page where the element reference belongs, the reference logs as undefined without later defining it self. The element is not centered until I resize the page.
My setup is:
React v18.2.0
react-dom-router v6.3.0
styled-components v5.3.5
My intuition tells me that it's something to do with the useLayoutEffect dependency array..
But any suggestions and help on how to fix this bug would be greatly appreciated.
import React, { useRef } from "react";
import { useWindowDimensions } from "../services/hooks/useWindowDimensions";
import { useGetElementDimensions } from "../services/hooks/useElementDimensions";
import {
HeroImage,
HomeContainer,
TextContainer,
TextContainerOverlay,
TextDescription,
} from "../components/StyledElements/HomeStyles.js";
const Home = () => {
const textRef = useRef();
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const { width:textWidth, height: textHeight } = useGetElementDimensions(textRef)
console.log(textRef);
return (
<HomeContainer height={`${windowHeight - 85}px`}>
<HeroImage
src="https://images.unsplash.com/photo-1587574293340-e0011c4e8ecf?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1932&q=80"
alt="landingImage"
/>
<TextContainer
ref={textRef}
left={`${(windowWidth - textWidth) / 2}px`}
top={`${((windowHeight - textHeight) / 2) - 42}px`}
>
<TextDescription>
Welcome to Pastasauce, a restaurant of italian cuisine focusing
entirely on pasta. On the food selection screen you will be able to
add what you want from the menu and it will be ready for you when you
arrive at the restaurant.
</TextDescription>
<TextContainerOverlay></TextContainerOverlay>
</TextContainer>
</HomeContainer>
);
};
export default Home;
The following is the custom hook, that I'm using for getting the elements dimensions.
import { useCallback, useLayoutEffect, useState } from "react";
export const useGetElementDimensions = (ref) => {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const handleResize = useCallback(() => {
setWidth(ref.current.offsetWidth)
setHeight(ref.current.offsetHeight)
}, [ref]);
useLayoutEffect(() => {
window.addEventListener('load', handleResize);
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('load', handleResize)
window.removeEventListener('resize', handleResize)
}
}, [ref, handleResize])
return {width, height}
}
It is not a bug.
It is undefined because you are assigning the reference of a dom element thats in the same component, which means it has to be mounted first to get a value.
https://reactjs.org/docs/refs-and-the-dom.html
const Home = () => {
const textRef = useRef(); // undefined
...
const { width:textWidth, height: textHeight } = useGetElementDimensions(textRef) // still undefined
console.log(textRef); // says undefined
return (
... <TextContainer
ref={textRef} // gets value AFTER mount
left={`${(windowWidth - textWidth) / 2}px`}
top={`${((windowHeight - textHeight) / 2) - 42}px`}
>
...
);
EDIT:
Possible solution for your case after wrapping your function inside useLayoutEffect: Typescript playground link
export const useUpdateElementDimensions = (ref, onResize) => {
useLayoutEffect(() => {
const resize = () => {
onResize(ref.current.offsetWidth, ref.current.offsetHeight);
}
window.addEventListener('load', resize);
window.addEventListener('resize', resize)
return () => {
window.removeEventListener('load', resize)
window.removeEventListener('resize', resize)
}
}, [ref, onResize]);
}
const Home = () => {
const textRef = useRef();
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const onResize = (w, h) => {
textRef.current.left = windowWidth - w / 2;
textRef.current.top = windowHeight - h / 2 - 42;
}
useUpdateElementDimensions(textRef,onResize)
example:
resize using react js
this is my code:
import React, { useState, useEffect } from 'react';
const getWidthWindow = () => {
const [widthWindow, setWidthWindow] = useState(null)
const updateDimensions = () => {
setWidthWindow(window.screen.width)
}
useEffect(() => {
console.log(widthWindow)
setWidthWindow(window.screen.width)
updateDimensions()
window.addEventListener('resize', updateDimensions)
return () => window.removeEventListener('resize', updateDimensions)
}, [widthWindow])
}
export default getWidthWindow;
I want to get the window width value but the result is like it doesn't match the window size so how to fix it?
Your code is correct but the logging isn't.
Add a hook to log the dimensions when it updates:
useEffect(() => {
console.log(windowDimensions)
}, [windowDimensions])
Working codesandbox.
I go with the above answer of adding windowDimensions to the useEffect's Dependency array but I like to add up little sugar on top of it..
On Resize, the event gets triggered continuously and impacts performance a bit..
So, I have implemented throttling to improve the performance..
Answer for your updated question: Stackblitz link
const GetWidthWindow = () => {
const [widthWindow, setWidthWindow] = useState(window.innerWidth);
useEffect(() => {
let throttleResizeTimer = null;
function handleResize() {
clearTimeout(throttleResizeTimer);
throttleResizeTimer = setTimeout(
() => setWidthWindow(window.innerWidth),
500
);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [widthWindow]);
return <p>{JSON.stringify(widthWindow)}</p>;
};
export default GetWidthWindow;
Answer for your old question:
useEffect(() => {
// implement throttle for little performance gain
let throttleResizeTimer = null;
function handleResize() {
clearTimeout(throttleResizeTimer);
throttleResizeTimer = setTimeout(
() => setWindowDimensions(getWindowDimensions()),
500
);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize); }, [windowDimensions]);
I've made a conditional sticky bar based on scroll position, today I noticed that it has a odd flicker effect (it adds and removes the sticky class very fast on loop) when you reach the given height from the top. When you're above or below it, it works as expected
I tried to change the height from the top, or to use a initial position from top as a condition for the useEffect, but it's the same.
Anyone had the same issue with React before?
My code is:
import { useState, useEffect } from 'react'
import styles from './xxx.module.scss'
import {tagsStore, mobileStore} from '#/utils/store'
import ScrollContainer from 'react-indiana-drag-scroll'
export default function MyComponent() {
const [sticky, setSticky] = useState(false)
const tags = tagsStore(state => state.tags)
const isMobile = mobileStore(state => state.isMobile)
const handler = () => {
setSticky(window.pageYOffset >= 240)
}
useEffect(() => {
window.addEventListener("scroll", handler, { passive: true })
return () => {
window.removeEventListener("scroll", handler, { passive: true });
}
}, [])
return (
<>
{!isMobile && tags.length > 0 &&
<div className={sticky ? styles.tagsListBoxSticky : (styles.tagsListBox + " mt-3")}>
<ScrollContainer>
...
</ScrollContainer>
</div>
}
</>
)
}
Update:
So I tried to figure it out, but couldn't. I have an onClick that triggers the toggleNavbar function.
I also have the windowResized function which checks if the browser is wider than 576. If that condition is true it checks if the navbarState is true. If both conditions are true the toggleNavbar function should be called from the windowResized function.
The issue that I'm having is that the if statement below (the one in the windowResized function) never runs, because the state doesn't update.
if (navbarState) {
toggleNavbar()
}
Is there a way to make sure that the navbarState updates before I do the checks?
navbar.js
import React, { useState, useRef, useEffect } from "react"
import { Link } from "gatsby"
import styles from "./styling/navbar.module.less"
const Navbar = ( props ) => {
const [navbarState, setNavbarState] = useState(false)
const [navHeight, setNavHeight] = useState()
const ref = useRef(null)
useEffect(() => {
let windowResized = () => {
let windowWidth = window.innerWidth
if (windowWidth > 576) {
if (navbarState) {
toggleNavbar()
}
}
}
window.addEventListener('resize', windowResized)
setNavHeight(ref.current.clientHeight)
}, [])
let toggleNavbar = () => {
setNavbarState((navbarState) => !navbarState)
if (navbarState) {
props.updateClassNames(styles.contentLeftAnimate)
}
else{
props.updateClassNames(styles.contentRightAnimate)
}
}
return (
<nav ref={ref} id={"navigation-bar"}>
<div className={`${styles.navLinks} ${navbarState? styles.navActive:""}`}
style={{top: `${navHeight}px`}}>
{props.pages.map((page, index) => (
<Link key={page.name} className={`${styles.navLink} ${styles.navLinkHoverEffect} ${navbarState? styles.navAnimate:""}`}
style={{animationDelay: `${index / 7 + 0.5}s`}} to={page.link}>
{page.name}
</Link>
))}
</div>
<div className={`${styles.burger} ${navbarState? styles.toggle:""}`} onClick={toggleNavbar}>
<div className={styles.line1}></div>
<div className={styles.line2}></div>
<div className={styles.line3}></div>
</div>
</nav>
)
}
export default Navbar
You never set navbarState to false again, also put adding event listeners in an effect is better so try this instead:
//only set the event listener on mount
useEffect(() => {
let windowResized = () => {
let windowWidth = window.innerWidth;
if (windowWidth > 576) {
// if (navbarState) { this was a stale closure
setNavbarState(false);
console.log('Toggle navbar - Width > 576');
} else {//not sure if you need this else
console.log('Window is bigger than mobile');
//set it to true again when it goes over 576 (not sure if you need this)
setNavbarState(true);
}
};
window.addEventListener('resize', windowResized);
//probably never happens to layout but if unmounted
// remove the event listener
return () =>
window.removeEventListener('resize', windowResized);
}, []);
The toggle is probably wrong as well if you expect navbarState to immediately change after calling setNavbarState, instead try this
setNavbarState((navbarState) => {
const newState = !navbarState;
if (newState) {
props.updateClassNames(styles.contentLeftAnimate);
} else {
props.updateClassNames(styles.contentRightAnimate);
}
return newState;
});
I'm using bootstrap 4 nav bar and would like to change the background color after ig 400px down scroll down. I was looking at the react docs and found a onScroll but couldn't find that much info on it. So far I have...
I don't know if I'm using the right event listener or how to set the height etc.
And I'm not really setting inline styles...
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = { scrollBackground: 'nav-bg' };
this.handleScroll = this.handleScroll.bind(this);
}
handleScroll(){
this.setState ({
scrollBackground: !this.state.scrollBackground
})
}
render() {
const scrollBg = this.scrollBackground ? 'nav-bg scrolling' : 'nav-bg';
return (
<div>
<Navbar inverse toggleable className={this.state.scrollBackground}
onScroll={this.handleScroll}>
...
</Navbar>
</div>
);
}
}
export default App;
For those of you who are reading this question after 2020, I've taken #glennreyes answer and rewritten it using React Hooks:
const [scroll, setScroll] = useState(0)
useEffect(() => {
document.addEventListener("scroll", () => {
const scrollCheck = window.scrollY < 100
if (scrollCheck !== scroll) {
setScroll(scrollCheck)
}
})
})
Bear in mind that, useState has an array of two elements, firstly the state object and secondly the function that updates it.
Along the lines, useEffect helps us replace componentDidmount, the function written currently does not do any clean ups for brevity purposes.
If you find it essential to clean up, you can just return a function inside the useEffect.
You can read comprehensively here.
UPDATE:
If you guys felt like making it modular and even do the clean up, you can do something like this:
Create a custom hook as below;
import { useState, useEffect } from "react"
export const useScrollHandler = () => {
// setting initial value to true
const [scroll, setScroll] = useState(1)
// running on mount
useEffect(() => {
const onScroll = () => {
const scrollCheck = window.scrollY < 10
if (scrollCheck !== scroll) {
setScroll(scrollCheck)
}
}
// setting the event handler from web API
document.addEventListener("scroll", onScroll)
// cleaning up from the web API
return () => {
document.removeEventListener("scroll", onScroll)
}
}, [scroll, setScroll])
return scroll
}
Call it inside any component that you find suitable:
const component = () => {
// calling our custom hook
const scroll = useScrollHandler()
....... rest of your code
}
One way to add a scroll listener is to use the componentDidMount() lifecycle method. Following example should give you an idea:
import React from 'react';
import { render } from 'react-dom';
class App extends React.Component {
state = {
isTop: true,
};
componentDidMount() {
document.addEventListener('scroll', () => {
const isTop = window.scrollY < 100;
if (isTop !== this.state.isTop) {
this.setState({ isTop })
}
});
}
render() {
return (
<div style={{ height: '200vh' }}>
<h2 style={{ position: 'fixed', top: 0 }}>Scroll {this.state.isTop ? 'down' : 'up'}!</h2>
</div>
);
}
}
render(<App />, document.getElementById('root'));
This changes the Text from "Scroll down" to "Scroll up" when your scrollY position is at 100 and above.
Edit: Should avoid the overkill of updating the state on each scroll. Only update it when the boolean value changes.
const [scroll, setScroll] = useState(false);
useEffect(() => {
window.addEventListener("scroll", () => {
setScroll(window.scrollY > specify_height_you_want_to_change_after_here);
});
}, []);
Then you can change your class or anything according to scroll.
<nav className={scroll ? "bg-black" : "bg-white"}>...</nav>
It's Better
import React from 'react';
import { render } from 'react-dom';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isTop: true
};
this.onScroll = this.onScroll.bind(this);
}
componentDidMount() {
document.addEventListener('scroll', () => {
const isTop = window.scrollY < 100;
if (isTop !== this.state.isTop) {
this.onScroll(isTop);
}
});
}
onScroll(isTop) {
this.setState({ isTop });
}
render() {
return (
<div style={{ height: '200vh' }}>
<h2 style={{ position: 'fixed', top: 0 }}>Scroll {this.state.isTop ? 'down' : 'up'}!</h2>
</div>
);
}
}
render(<App />, document.getElementById('root'));
This is yet another take / my take on hooks approach for on scroll displaying and hiding of a random page element.
I have been very much inspired from: Dan Abramov's post here.
You can check a full working example, in this CodeSandbox demo.
The following is the code for the useScroll custom hook:
import React, { useState, useEffect } from "react";
export const useScroll = callback => {
const [scrollDirection, setScrollDirection] = useState(true);
const handleScroll = () => {
const direction = (() => {
// if scroll is at top or at bottom return null,
// so that it would be possible to catch and enforce a special behaviour in such a case.
if (
window.pageYOffset === 0 ||
window.innerHeight + Math.ceil(window.pageYOffset) >=
document.body.offsetHeight
)
return null;
// otherwise return the direction of the scroll
return scrollDirection < window.pageYOffset ? "down" : "up";
})();
callback(direction);
setScrollDirection(window.pageYOffset);
};
// adding and cleanning up de event listener
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
});
};
And this hook will be consumed like this:
useScroll(direction => {
setScrollDirection(direction);
});
A full component using this custom hook:
import React, { useState } from "react";
import ReactDOM from "react-dom";
import CustomElement, { useScroll } from "./element";
import Scrollable from "./scrollable";
function Page() {
const [scrollDirection, setScrollDirection] = useState(null);
useScroll(direction => {
setScrollDirection(direction);
});
return (
<div>
{/* a custom element that implements some scroll direction behaviour */}
{/* "./element" exports useScroll hook and <CustomElement> */}
<CustomElement scrollDirection={scrollDirection} />
{/* just a lorem ipsum long text */}
<Scrollable />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Page />, rootElement);
And lastly the code for CustomElement:
import React, { useState, useEffect } from "react";
export default props => {
const [elementVisible, setElementVisible] = useState(true);
const { scrollDirection } = props;
// when scroll direction changes element visibility adapts, but can do anything we want it to do
// U can use ScrollDirection and implement some page shake effect while scrolling
useEffect(() => {
setElementVisible(
scrollDirection === "down"
? false
: scrollDirection === "up"
? true
: true
);
}, [scrollDirection]);
return (
<div
style={{
background: "#ff0",
padding: "20px",
position: "fixed",
width: "100%",
display: `${elementVisible ? "inherit" : "none"}`
}}
>
element
</div>
);
};
I have changed #PouyaAtaei answer a bit for my use case.
import { useState, useEffect } from "react"
// Added distance parameter to determine how much
// from the top tell return value is updated.
// The name of the hook better reflects intended use.
export const useHasScrolled = (distance = 10) => {
// setting initial value to false
const [scroll, setScroll] = useState(false)
// running on mount
useEffect(() => {
const onScroll = () => {
// Logic is false tell user reaches threshold, then true after.
const scrollCheck = window.scrollY >= distance;
if (scrollCheck !== scroll) {
setScroll(scrollCheck)
}
}
// setting the event handler from web API
document.addEventListener("scroll", onScroll)
// cleaning up from the web API
return () => {
document.removeEventListener("scroll", onScroll)
}
}, [scroll, setScroll])
return scroll
}
Calling the hook:
const component = () => {
// calling our custom hook and optional distance agument.
const scroll = useHasScrolled(250)
}
These are two hooks - one for direction (up/down/none) and one for the actual position
Use like this:
useScrollPosition(position => {
console.log(position)
})
useScrollDirection(direction => {
console.log(direction)
})
Here are the hooks:
import { useState, useEffect } from "react"
export const SCROLL_DIRECTION_DOWN = "SCROLL_DIRECTION_DOWN"
export const SCROLL_DIRECTION_UP = "SCROLL_DIRECTION_UP"
export const SCROLL_DIRECTION_NONE = "SCROLL_DIRECTION_NONE"
export const useScrollDirection = callback => {
const [lastYPosition, setLastYPosition] = useState(window.pageYOffset)
const [timer, setTimer] = useState(null)
const handleScroll = () => {
if (timer !== null) {
clearTimeout(timer)
}
setTimer(
setTimeout(function () {
callback(SCROLL_DIRECTION_NONE)
}, 150)
)
if (window.pageYOffset === lastYPosition) return SCROLL_DIRECTION_NONE
const direction = (() => {
return lastYPosition < window.pageYOffset
? SCROLL_DIRECTION_DOWN
: SCROLL_DIRECTION_UP
})()
callback(direction)
setLastYPosition(window.pageYOffset)
}
useEffect(() => {
window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll)
})
}
export const useScrollPosition = callback => {
const handleScroll = () => {
callback(window.pageYOffset)
}
useEffect(() => {
window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll)
})
}
how to fix :
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.
MenuNews
const [scroll, setScroll] = useState(false);
useEffect(() => {
window.addEventListener("scroll", () => {
setScroll(window.scrollY > specify_height_you_want_to_change_after_here);
});
}, []);
Approach without scroll event listener
import { useEffect, useState } from "react";
interface Props {
elementId: string;
position: string;
}
const useCheckScrollPosition = ({ elementId, position }: Props) => {
const [isOverScrollPosition, setIsOverScrollPosition] = useState<boolean>(false);
useEffect(() => {
if (
"IntersectionObserver" in window &&
"IntersectionObserverEntry" in window &&
"intersectionRatio" in window.IntersectionObserverEntry.prototype
) {
const observer = new IntersectionObserver((entries) => {
setIsOverScrollPosition(entries[0].boundingClientRect.y < 0);
});
const flagElement = document.createElement("div");
flagElement.id = elementId;
flagElement.className = "scroll-flag";
flagElement.style.top = position;
const container = document.getElementById("__next"); // React div id
const oldFlagElement = document.getElementById(elementId);
if (!oldFlagElement) container?.appendChild(flagElement);
const elementToObserve = oldFlagElement || flagElement;
observer.observe(elementToObserve);
}
}, [elementId, position]);
return isOverScrollPosition;
};
export default useCheckScrollPosition;
and then you can use it like this:
const isOverScrollPosition = useCheckScrollPosition({
elementId: "sticky-header",
position: "10px",
});
isOverScrollPosition is a boolean that will be true if you scroll over position provided value (10px) and false if you scroll below it.
This approach will add a flag div in react root.
Reference: https://css-tricks.com/styling-based-on-scroll-position/