I'm really new to React and I've been trying for some time now to make a scroll to top function. Using the existing tutorials and everything on google i've made this component :
Import React, { useState, useEffect } from "react";
export default function ScrollToTop() {
const [isVisible, setIsVisible] = useState(false)
const toggleVisibility = () => {
if(window.pageYOffset > 300) {
setIsVisible(true);
} else {
setIsVisible(false);
}
};
const ScrollToTop = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
useEffect(() => {
window.addEventListener('scroll', toggleVisibility);
return () => {
window.removeEventListener('scroll', toggleVisibility);
};
}, []);
if (!isVisible) {
return false
}
return (
<button className="button" onClick={ScrollToTop}>
<div className="button__arrow button__arrow--up"></div>
</button>
)
};
the problem is that when i import it in the App.js it doesn't work properly, the scrolling part works perfectly, but the button it just stays at bottom of the page instead of appearing after a certain amount of scroll. This is the App.js:
return (
<div>
{loading ? (
<h1>Loading...</h1>
) : (
<>
<Navbar />
<div className="Grid-container">
{pokemonData.map((pokemon, i) => {
return <Card key={i} pokemon={pokemon} />;
})}
</div>
<ScrollToTop/>
</>
)}
</div>
);
}
I don't see any overt issues in your ScrollTop implementation that would cause the scroll-to-top button to remain on the page even after scrolling back to the top, but I do see room for optimization.
For onScroll events it's best to use passive listeners.
Invoke toggleVisibility once immediately in the mounting useEffect to ensure proper initial state.
Apply Boolean Zen to the visibility toggler; Update the isVisible state with the result of the boolean comparison.
Just conditionally render the button in a single return.
Code:
function ScrollToTop() {
const [isVisible, setIsVisible] = useState(false);
const toggleVisibility = () => {
setIsVisible(window.pageYOffset > 300);
};
const ScrollToTop = () => {
window.scrollTo({
top: 0,
behavior: "smooth"
});
};
useEffect(() => {
toggleVisibility();
window.addEventListener("scroll", toggleVisibility, { passive: true });
return () => {
window.removeEventListener("scroll", toggleVisibility, { passive: true });
};
}, []);
return isVisible ? (
<button className="button" onClick={ScrollToTop}>
<div className="button__arrow button__arrow--up">TOP</div>
</button>
) : null;
}
If you're still having issues with your implementation and usage try to create a codesandbox that reproduces the issue that we can inspect and debug live.
Related
[Solved] My input component is losing focus as soon as I press any key only when its value is controlled from outside the portal
NOTE: I am sorry. While writing this, I found the problem in my code, but I decided to post this anyway
[Reason] I was inlining the close function, so the useEffect hook got triggered every time close changed when the component was rendered again due to state changes and thus calling the activeElement.blur() on each keystroke.
Portal
const root = document.getElementById('root')
const modalRoot = document.getElementById('modal-root')
const Portal = ({ children, className, drawer = false }) => {
const element = React.useMemo(() => document.createElement('div'), [])
React.useEffect(() => {
element.className = clsx('modal', className)
modalRoot.appendChild(element)
return () => {
modalRoot.removeChild(element)
}
}, [element, className])
return ReactDOM.createPortal(children, element)
}
Modal
const Modal = (props) => {
const { children, show = false, close, className } = props
const backdrop = React.useRef(null)
const handleTransitionEnd = React.useCallback(() => setActive(show), [show])
const handleBackdropClick = React.useCallback(
({ target }) => target === backdrop.current && close(),
[]
)
const handleKeyUp = React.useCallback(
({ key }) => ['Escape'].includes(key) && close(),
[]
)
React.useEffect(() => {
if (backdrop.current) {
window.addEventListener('keyup', handleKeyUp)
}
if (show) {
root.setAttribute('inert', 'true')
document.body.style.overflow = 'hidden'
document.activeElement.blur?.() // ! CULPRIT
}
return () => {
root.removeAttribute('inert')
document.body.style.overflow = 'auto'
window.removeEventListener('keyup', handleKeyUp)
}
}, [show, close])
return (
<>
{show && (
<Portal className={className}>
<div
ref={backdrop}
onClick={handleBackdropClick}
onTransitionEnd={handleTransitionEnd}
className={clsx('backdrop', show && 'active')}>
<div className="content">{children}</div>
</div>
</Portal>
)}
</>
)
}
Custom Textfield
const TextField = React.forwardRef(
({ label, className, ...props }, ref) => {
return (
<div className={clsx('textfield', className)}>
{label && <label>{label}</label>}
<input ref={ref} {...props} />
</div>
)
}
)
I was inlining the close function, so the useEffect hook got triggered every time close changed when the component was rendered again due to state changes and thus calling the activeElement.blur() on each keystroke.
In Modal.jsx
...
React.useEffect(() => {
...
if (show) {
root.setAttribute('inert', 'true')
document.body.style.overflow = 'hidden'
document.activeElement.blur?.() // ! CULPRIT
}
...
}, [show, close]) // as dependency
...
<Modal
show={show}
close={() => setShow(false)} // this was inlined
className="some-modal"
>
...
</Modal>
TAKEAWAY
Do not inline functions
Usually there is no reason to pass a function (pointer) as dependency
I am making a slider that has arrows to scroll through the items only if the items are too wide for the div. It works, except when resizing the window to make it large enough, the arrows don't disappear. The arrows should only appear if scroll arrows is true.
{scrollArrows && (
<div className="arrow arrow-left" onClick={goLeft}>
<
</div>
)}
But when I console.log it, even if the arrows are there, scroll arrows ALWAYS is false. Here is the relevant code:
import "./ProjectRow.css";
import Project from "../Project/Project";
import React, { useEffect, useState } from "react";
const ProjectRow = (props) => {
const rowRef = React.useRef();
const [hasLoaded, setHasLoaded] = useState(false);
const [scrollArrows, setScrollArrows] = useState(false);
const [left, setLeft] = useState(0);
const [rowWidth, setRowWidth] = useState(null);
function debounce(fn, ms) {
let timer;
return () => {
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn.apply(this, arguments);
}, ms);
};
}
useEffect(() => {
const setVariables = () => {
console.log(scrollArrows);
if (rowRef.current?.offsetWidth < rowRef.current?.scrollWidth) {
setRowWidth(rowRef.current.offsetWidth);
if (!scrollArrows) {
console.log("scrollArrows true now");
setScrollArrows(true);
}
} else if (scrollArrows) {
setScrollArrows(false);
}
};
if (!hasLoaded) setVariables();
const debouncedHandleResize = debounce(setVariables, 300);
window.addEventListener("resize", debouncedHandleResize);
setHasLoaded(true);
return () => {
window.removeEventListener("resize", debouncedHandleResize);
};
}, []);
const goLeft = () => {
const projectWidth = rowRef.current.childNodes[2].firstChild.offsetWidth;
if (rowRef.current.childNodes[2].firstChild.getBoundingClientRect().x < 0)
setLeft(left + projectWidth + 20);
};
const goRight = () => {
const projectWidth = rowRef.current.childNodes[2].firstChild.offsetWidth;
if (
rowRef.current.childNodes[2].lastChild.getBoundingClientRect().x +
projectWidth >
rowWidth
)
return setLeft(left - (projectWidth + 20));
};
return (
<div className="project-row" ref={rowRef}>
<h3 className="project-row-title light-gray bg-dark-gray">
{props.data.groupName}
</h3>
<hr className="gray-bar" />
<div className="slider" style={{ left: `${left}px` }}>
{props.data.projects.map((project, i) => (
<Project data={project} key={i} />
))}
</div>
{scrollArrows && (
<div className="arrow arrow-left" onClick={goLeft}>
<
</div>
)}
{scrollArrows && (
<div className="arrow arrow-right" onClick={goRight}>
>
</div>
)}
</div>
);
};
export default ProjectRow;
The "scroll arrows true now" gets logged, but scrollArrows variable stays false, even after waiting to resize or just resizing between two widths that need it.
EDIT: Fixed it by removing the if on the else if. I figured that might be the issue, but I don't know why it was preventing it from functioning properly, so it feels bad.
Your 'useEffect' doesn't look well structured. You have to structure it this way:
React.useEffect(()=>{
// Functions
window.addEventListener('resize', ...
return ()=>{
window.removeEventListener('resize', ...
}
},[])
Please follow this answer and create a Hook: Why useEffect doesn't run on window.location.pathname changes?
Try changing that and it might update your value and work.
See codesandbox here
I am trying to add a modal that shows up with a delay when hovering over a div. However, it's getting a bit tricky because, for example, if the timeout interval is 1000ms, and you hover over said div and then hover away from that div within the 1000ms, the modal will still show up. What I want to happen is for the modal to show up after the delay (e.g. 1000ms) only if you have maintained mouseover over the div for that delay period. How can I create this effect instead of the side effects I'm seeing now? Thanks!
index.tsx:
import * as React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const Modal: React.FC = () => {
const divRef = React.useRef<HTMLDivElement>(null);
const [showModal, setShowModal] = React.useState<boolean>(false);
React.useEffect(() => {
const divNode = divRef.current;
const handleEvent = (event: Event): void => {
if (divNode) {
if (divNode.contains(event.target as Node)) {
setTimeout(() => setShowModal(true), 1000);
} else {
setShowModal(false);
}
}
};
document.addEventListener("mouseover", handleEvent);
return () => {
document.removeEventListener("mouseover", handleEvent);
};
}, [divRef]);
return (
<div className="container">
<div className="div" ref={divRef}>
Hover Me
</div>
{showModal && <div className="modal">modal</div>}
</div>
);
};
const App: React.FC = () => (
<>
<Modal />
<Modal />
<Modal />
<Modal />
</>
);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You should add a mouse out event that will hide the modal.
Call a funtion on 'mouseout' event listener and set the showModal to false. In that way, it will hide the modal if you move your mouse any time.
setShowModal(false)
Updated: Can you also set timeout to a variable and then on mouseout fire clearTimeout(variable_that_set_to_timeout)
React.useEffect(() => {
const divNode = divRef.current;
let timeout = null;
const handleEvent = (event: Event): void => {
if (divNode) {
if (divNode.contains(event.target as Node)) {
timeout = setTimeout(() => setShowModal(true), 1000);
} else {
setShowModal(false);
}
}
};
const hideModal = (event: Event): void => {
clearTimeout(timeout);
setShowModal(false);
};
divNode.addEventListener("mouseover", handleEvent);
divNode.addEventListener("mouseout", hideModal);
return () => {
document.removeEventListener("mouseover", handleEvent);
};
}, [divRef]);
Link of sandbox
You should really avoid changing the DOM when working with react. React isn't jQuery.
you could try making this your modal code:
const Modal: React.FC = () => {
const [timeout, setModalTimeout] = React.useState(null);
const [showModal, setShowModal] = React.useState<boolean>(false);
return (
<div className="container">
<div className="div" onMouseEnter={() => {
timeout && !showModal && clearTimeout(timeout);
setModalTimeout(setTimeout(() => setShowModal(true), 1000))
}} onMouseLeave={() => {
timeout && clearTimeout(timeout)
setShowModal(false);
}}>
Hover Me
</div>
{showModal && <div className="modal">modal</div>}
</div>
);
};
Sources:
https://reactjs.org/docs/refs-and-the-dom.html
https://reactjs.org/docs/react-dom.html
The proper way to do this would be to create a useTimeout hook and manage maintain the state of the hover.
import { useState } from "react";
import useTimeout from "./useTimeout";
export default function App() {
const [visible, setVisible] = useState(false);
const [hovered, setHovered] = useState(false);
//close after 3s
useTimeout(() => setVisible(true), !visible && hovered ? 3000 : null);
return (
<div className="App">
<h1>Hover Timeout Example</h1>
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
Hover me for 3s to show modal
<div>Hover status: {hovered ? "true" : "false"}</div>
</div>
{visible && (
<div>
<h1>Modal</h1>
<div>
<button onClick={() => setVisible(false)}>close</button>
</div>
</div>
)}
</div>
);
}
Code Sandbox
I have a event when you resize the window will show desktop sidebar or mobile sidebar. if window is less than But there are variables that aren't updated immediately to show sidebar if I'm in desktop window, I could that with class
I've created a sandbox https://codesandbox.io/s/sidebar-hooks-8oefi
to see the code, I have the class component in App_class.js which if I replace in App it works, but with hooks (App_hooks.js file, by default in App.js) I can't make it works
Thanks for your attention. I’m looking forward to your reply.
with class I could do that using:
if (isMobile !== wasMobile) {
this.setState({
isOpen: !isMobile
});
}
const App = props => {
//minicomponent to update width
const useListenResize = () => {
const [isOpen, setOpen] = useState(false);
const [isMobile, setMobile] = useState(true);
//const [previousWidth, setPreviousWidth] = useState( -1 );
let previousWidth = -1;
const updateWidth = () => {
const width = window.innerWidth;
const widthLimit = 576;
let newValueMobile = width <= widthLimit;
setMobile(isMobile => newValueMobile);
const wasMobile = previousWidth <= widthLimit;
if (isMobile !== wasMobile) {
setOpen(isOpen => !isMobile);
}
//setPreviousWidth( width );
previousWidth = width;
};
useEffect(() => {
updateWidth();
window.addEventListener("resize", updateWidth);
return () => {
window.removeEventListener("resize", updateWidth);
};
// eslint-disable-next-line
}, []);
return isOpen;
};
const [isOpen, setOpen] = useState(useListenResize());
const toggle = () => {
setOpen(!isOpen);
};
return (
<div className="App wrapper">
<SideBar toggle={toggle} isOpen={isOpen} />
<Container fluid className={classNames("content", { "is-open": isOpen })}>
<Dashboard toggle={toggle} isOpen={isOpen} />
</Container>
</div>
);
};
export default App;
with setState didn't work
The issue is after setting isMobile value here,
setMobile(isMobile => newValueMobile);
You are immediately refering that value,
if (isMobile !== wasMobile) {
setOpen(isOpen => !isMobile);
}
Due to async nature of setState, your are getting previous value of isMobile here all the times.
To make this work you need to make some change to your code.
You are directly mutating previousWidth value, you should have previousWidth in a state and use setter function to change the value.
const [previousWidth, setPreviousWidth] = useState(-1);
setPreviousWidth(width); //setter function
You cannot get value immediately after setState. You should use another useEffect with isMobile and previousWidth as dependency array.
useEffect(() => {
const wasMobile = previousWidth <= widthLimit;
if (isMobile !== wasMobile) {
setOpen(isOpen => !isMobile);
}
}, [isMobile, previousWidth]);
Demo
Since updateWidth is registered once on component mount with useEffect, then it will refer to a stale state (isMobile), which is always true, therefore, setOpen(isOpen => !isMobile) will never work.
EDIT: The following code illustrates a simpler version of your issue:
const Counter = () => {
const [count, setCount] = useState(0)
const logCount = () => {
console.log(count);
}
useEffect(() => {
window.addEventListener('resize', logCount);
}, [])
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
If you run this code, you will notice that even if you change the value of count by clicking on the button, you will always get the initial value when resizing the browser (check the console), and that's because logCount refer to a state that was obtained at the moment it was defined.
#ravibagul91 answer works, because it access the state from useEffect, and not from the event handler.
You may wanna check: React hooks behaviour with event listener and Why am I seeing stale props or state inside my function?, it will give you a better idea of what's going on.
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/