Reputation: 2637
I have built a component in React which is supposed to update its own style on window scroll to create a parallax effect.
The component render
method looks like this:
function() {
let style = { transform: 'translateY(0px)' };
window.addEventListener('scroll', (event) => {
let scrollTop = event.srcElement.body.scrollTop,
itemTranslate = Math.min(0, scrollTop/3 - 60);
style.transform = 'translateY(' + itemTranslate + 'px)');
});
return (
<div style={style}></div>
);
}
This doesn't work because React doesn't know that the component has changed, and therefore the component is not rerendered.
I've tried storing the value of itemTranslate
in the state of the component, and calling setState
in the scroll callback. However, this makes scrolling unusable as this is terribly slow.
Any suggestion on how to do this?
Upvotes: 225
Views: 499641
Reputation: 2158
with hooks:
import React, { useEffect, useState } from 'react';
function MyApp () {
const [offset, setOffset] = useState(0);
useEffect(() => {
const onScroll = () => setOffset(window.scrollY);
// clean up code
window.removeEventListener('scroll', onScroll);
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
console.log(offset);
};
Upvotes: 121
Reputation: 1791
You can pass a function to the onScroll
event on the React element: https://facebook.github.io/react/docs/events.html#ui-events
<ScrollableComponent
onScroll={this.handleScroll}
/>
Another answer that is similar: https://stackoverflow.com/a/36207913/1255973
2023-10 Update
Upvotes: 61
Reputation: 462
If you find the above answers not working for you, try this:
React.useEffect(() => {
document.addEventListener('wheel', yourCallbackHere)
return () => {
document.removeEventListener('wheel', yourCallbackHere)
}
}, [yourCallbackHere])
Basically, you need to try document
instead of window
, and wheel
instead of scroll
.
Happy coding!
Upvotes: 4
Reputation: 85
I often get a warning about rendering. This code works, but not sure if it's the best solution.
const listenScrollEvent = () => {
if (window.scrollY <= 70) {
setHeader("header__main");
} else if (window.scrollY >= 70) {
setHeader("header__slide__down");
}
};
useEffect(() => {
window.addEventListener("scroll", listenScrollEvent);
return () => {
window.removeEventListener("scroll", listenScrollEvent);
}
}, []);
Upvotes: 2
Reputation: 9
constructor() {
super()
this.state = {
change: false
}
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
console.log('add event');
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
console.log('remove event');
}
handleScroll = e => {
if (window.scrollY === 0) {
this.setState({ change: false });
} else if (window.scrollY > 0 ) {
this.setState({ change: true });
}
}
render() { return ( <div className="main" style={{ boxShadow: this.state.change ?
0px 6px 12px rgba(3,109,136,0.14):
none}} ></div>
This is how I did it and works perfect.
Upvotes: 1
Reputation: 1045
Update for an answer with React Hooks
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)
})
}
Upvotes: 6
Reputation: 9344
Here is another example using HOOKS fontAwesomeIcon and Kendo UI React
[![screenshot here][1]][1]
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const ScrollBackToTop = () => {
const [show, handleShow] = useState(false);
useEffect(() => {
window.addEventListener('scroll', () => {
if (window.scrollY > 1200) {
handleShow(true);
} else handleShow(false);
});
return () => {
window.removeEventListener('scroll');
};
}, []);
const backToTop = () => {
window.scroll({ top: 0, behavior: 'smooth' });
};
return (
<div>
{show && (
<div className="backToTop text-center">
<button className="backToTop-btn k-button " onClick={() => backToTop()} >
<div className="d-none d-xl-block mr-1">Top</div>
<FontAwesomeIcon icon="chevron-up"/>
</button>
</div>
)}
</div>
);
};
export default ScrollBackToTop;```
[1]: https://i.sstatic.net/ZquHI.png
Upvotes: 4
Reputation: 146
My bet here is using Function components with new hooks to solve it, but instead of using useEffect
like in previous answers, I think the correct option would be useLayoutEffect
for an important reason:
The signature is identical to useEffect, but it fires synchronously after all DOM mutations.
This can be found in React documentation. If we use useEffect
instead and we reload the page already scrolled, scrolled will be false and our class will not be applied, causing an unwanted behavior.
An example:
import React, { useState, useLayoutEffect } from "react"
const Mycomponent = (props) => {
const [scrolled, setScrolled] = useState(false)
useLayoutEffect(() => {
const handleScroll = e => {
setScrolled(window.scrollY > 0)
}
window.addEventListener("scroll", handleScroll)
return () => {
window.removeEventListener("scroll", handleScroll)
}
}, [])
...
return (
<div className={scrolled ? "myComponent--scrolled" : ""}>
...
</div>
)
}
A possible solution to the problem could be https://codepen.io/dcalderon/pen/mdJzOYq
const Item = (props) => {
const [scrollY, setScrollY] = React.useState(0)
React.useLayoutEffect(() => {
const handleScroll = e => {
setScrollY(window.scrollY)
}
window.addEventListener("scroll", handleScroll)
return () => {
window.removeEventListener("scroll", handleScroll)
}
}, [])
return (
<div class="item" style={{'--scrollY': `${Math.min(0, scrollY/3 - 60)}px`}}>
Item
</div>
)
}
Upvotes: 6
Reputation: 2625
Function component example using useEffect:
Note: You need to remove the event listener by returning a "clean up" function in useEffect. If you don't, every time the component updates you will have an additional window scroll listener.
import React, { useState, useEffect } from "react"
const ScrollingElement = () => {
const [scrollY, setScrollY] = useState(0);
function logit() {
setScrollY(window.pageYOffset);
}
useEffect(() => {
function watchScroll() {
window.addEventListener("scroll", logit);
}
watchScroll();
// Remove listener (like componentWillUnmount)
return () => {
window.removeEventListener("scroll", logit);
};
}, []);
return (
<div className="App">
<div className="fixed-center">Scroll position: {scrollY}px</div>
</div>
);
}
Upvotes: 13
Reputation: 3459
An example using classNames, React hooks useEffect, useState and styled-jsx:
import classNames from 'classnames'
import { useEffect, useState } from 'react'
const Header = _ => {
const [ scrolled, setScrolled ] = useState()
const classes = classNames('header', {
scrolled: scrolled,
})
useEffect(_ => {
const handleScroll = _ => {
if (window.pageYOffset > 1) {
setScrolled(true)
} else {
setScrolled(false)
}
}
window.addEventListener('scroll', handleScroll)
return _ => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
return (
<header className={classes}>
<h1>Your website</h1>
<style jsx>{`
.header {
transition: background-color .2s;
}
.header.scrolled {
background-color: rgba(0, 0, 0, .1);
}
`}</style>
</header>
)
}
export default Header
Upvotes: 19
Reputation: 915
I solved the problem via using and modifying CSS variables. This way I do not have to modify the component state which causes performance issues.
index.css
:root {
--navbar-background-color: rgba(95,108,255,1);
}
Navbar.jsx
import React, { Component } from 'react';
import styles from './Navbar.module.css';
class Navbar extends Component {
documentStyle = document.documentElement.style;
initalNavbarBackgroundColor = 'rgba(95, 108, 255, 1)';
scrolledNavbarBackgroundColor = 'rgba(95, 108, 255, .7)';
handleScroll = () => {
if (window.scrollY === 0) {
this.documentStyle.setProperty('--navbar-background-color', this.initalNavbarBackgroundColor);
} else {
this.documentStyle.setProperty('--navbar-background-color', this.scrolledNavbarBackgroundColor);
}
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
render () {
return (
<nav className={styles.Navbar}>
<a href="/">Home</a>
<a href="#about">About</a>
</nav>
);
}
};
export default Navbar;
Navbar.module.css
.Navbar {
background: var(--navbar-background-color);
}
Upvotes: 1
Reputation: 1157
I found that I can't successfully add the event listener unless I pass true like so:
componentDidMount = () => {
window.addEventListener('scroll', this.handleScroll, true);
},
Upvotes: 18
Reputation: 3156
To expand on @Austin's answer, you should add this.handleScroll = this.handleScroll.bind(this)
to your constructor:
constructor(props){
this.handleScroll = this.handleScroll.bind(this)
}
componentDidMount: function() {
window.addEventListener('scroll', this.handleScroll);
},
componentWillUnmount: function() {
window.removeEventListener('scroll', this.handleScroll);
},
handleScroll: function(event) {
let scrollTop = event.srcElement.body.scrollTop,
itemTranslate = Math.min(0, scrollTop/3 - 60);
this.setState({
transform: itemTranslate
});
},
...
This gives handleScroll()
access to the proper scope when called from the event listener.
Also be aware you cannot do the .bind(this)
in the addEventListener
or removeEventListener
methods because they will each return references to different functions and the event will not be removed when the component unmounts.
Upvotes: 1
Reputation: 141
If what you're interested in is a child component that's scrolling, then this example might be of help: https://codepen.io/JohnReynolds57/pen/NLNOyO?editors=0011
class ScrollAwareDiv extends React.Component {
constructor(props) {
super(props)
this.myRef = React.createRef()
this.state = {scrollTop: 0}
}
onScroll = () => {
const scrollTop = this.myRef.current.scrollTop
console.log(`myRef.scrollTop: ${scrollTop}`)
this.setState({
scrollTop: scrollTop
})
}
render() {
const {
scrollTop
} = this.state
return (
<div
ref={this.myRef}
onScroll={this.onScroll}
style={{
border: '1px solid black',
width: '600px',
height: '100px',
overflow: 'scroll',
}} >
<p>This demonstrates how to get the scrollTop position within a scrollable
react component.</p>
<p>ScrollTop is {scrollTop}</p>
</div>
)
}
}
Upvotes: 8
Reputation: 279
My solution for making a responsive navbar ( position: 'relative' when not scrolling and fixed when scrolling and not at the top of the page)
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll(event) {
if (window.scrollY === 0 && this.state.scrolling === true) {
this.setState({scrolling: false});
}
else if (window.scrollY !== 0 && this.state.scrolling !== true) {
this.setState({scrolling: true});
}
}
<Navbar
style={{color: '#06DCD6', borderWidth: 0, position: this.state.scrolling ? 'fixed' : 'relative', top: 0, width: '100vw', zIndex: 1}}
>
No performance issues for me.
Upvotes: 27
Reputation: 469
to help out anyone here who noticed the laggy behavior / performance issues when using Austins answer, and wants an example using the refs mentioned in the comments, here is an example I was using for toggling a class for a scroll up / down icon:
In the render method:
<i ref={(ref) => this.scrollIcon = ref} className="fa fa-2x fa-chevron-down"></i>
In the handler method:
if (this.scrollIcon !== null) {
if(($(document).scrollTop() + $(window).height() / 2) > ($('body').height() / 2)){
$(this.scrollIcon).attr('class', 'fa fa-2x fa-chevron-up');
}else{
$(this.scrollIcon).attr('class', 'fa fa-2x fa-chevron-down');
}
}
And add / remove your handlers the same way as Austin mentioned:
componentDidMount(){
window.addEventListener('scroll', this.handleScroll);
},
componentWillUnmount(){
window.removeEventListener('scroll', this.handleScroll);
},
docs on the refs.
Upvotes: 21
Reputation: 33544
You should bind the listener in componentDidMount
, that way it's only created once. You should be able to store the style in state, the listener was probably the cause of performance issues.
Something like this:
componentDidMount: function() {
window.addEventListener('scroll', this.handleScroll);
},
componentWillUnmount: function() {
window.removeEventListener('scroll', this.handleScroll);
},
handleScroll: function(event) {
let scrollTop = event.srcElement.body.scrollTop,
itemTranslate = Math.min(0, scrollTop/3 - 60);
this.setState({
transform: itemTranslate
});
},
Upvotes: 298