在React.js中更新组件onScroll的样式

我在React中构建了一个组件,它应该在窗口滚动上更新自己的风格,以创建视差效果。

组件render方法看起来像这样:

  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>
);
}

这是行不通的,因为React不知道组件已经更改,因此组件不会重新呈现。

我已经尝试在组件的状态中存储itemTranslate的值,并在滚动回调中调用setState。然而,这使得滚动无法使用,因为它非常慢。

有什么建议吗?

422559 次浏览

你应该在componentDidMount中绑定监听器,这样它只被创建一次。您应该能够在状态中存储样式,侦听器可能是性能问题的原因。

就像这样:

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
});
},

为了帮助那些在使用austin answer时注意到延迟行为/性能问题的人,并想要一个使用评论中提到的引用的例子,这里有一个我用来切换一个类的滚动向上/向下图标的例子:

在渲染方法中:

<i ref={(ref) => this.scrollIcon = ref} className="fa fa-2x fa-chevron-down"></i>

在handler方法中:

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');
}
}

然后像Austin提到的那样添加/删除你的处理程序:

componentDidMount(){
window.addEventListener('scroll', this.handleScroll);
},
componentWillUnmount(){
window.removeEventListener('scroll', this.handleScroll);
},

参考文献上的docs。

你可以将一个函数传递给React元素的onScroll事件

<ScrollableComponent
onScroll={this.handleScroll}
/>

另一个类似的答案:https://stackoverflow.com/a/36207913/1255973

我的解决方案,使一个响应式导航栏(位置:“相对”时不滚动和固定时滚动,而不是在页面顶部)

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}}
>

对我来说没有性能问题。

如果你感兴趣的是滚动的子组件,那么这个例子可能会有帮助: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>
)
}
}

为了扩展@Austin的答案,你应该在你的构造函数中添加this.handleScroll = this.handleScroll.bind(this):

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
});
},
...

这使handleScroll()在从事件侦听器调用时可以访问适当的作用域。

还要注意,不能在addEventListenerremoveEventListener方法中执行.bind(this),因为它们将各自返回对不同函数的引用,并且当组件卸载时,事件不会被删除。

我发现我不能成功地添加事件监听器,除非我像这样传递true:

componentDidMount = () => {
window.addEventListener('scroll', this.handleScroll, true);
},

我通过使用和修改CSS变量解决了这个问题。这样我就不必修改会导致性能问题的组件状态。

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);
}

使用useEffect的函数组件示例:

请注意:你需要通过在useEffect中返回一个"clean up"函数来移除事件监听器。如果不这样做,每次组件更新时,都会有一个额外的窗口滚动侦听器。

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>
);
}

一个使用一会, React 钩子useEffectuseStatestyled-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

我在这里打赌是使用带有新钩子的函数组件来解决它,但不是像以前的答案那样使用useEffect,我认为正确的选项是useLayoutEffect,原因很重要:

签名与useEffect相同,但是它同步触发

这可以在反应的文档中找到。如果我们使用useEffect代替,并且重新加载已经滚动的页面,则scroll将为false,并且我们的类将不会被应用,从而导致不必要的行为。

一个例子:

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>
)
}

这个问题的一个可能的解决方案可能是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>
)
}

用钩子:

import React, { useEffect, useState } from 'react';


function MyApp () {


const [offset, setOffset] = useState(0);


useEffect(() => {
const onScroll = () => setOffset(window.pageYOffset);
// clean up code
window.removeEventListener('scroll', onScroll);
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);


console.log(offset);
};
下面是另一个使用钩子 fontAwesomeIcon和Kendo UI React的例子 [![这里截图][1]][1]< / p >
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.stack.imgur.com/ZquHI.png

用React Hooks更新答案

这是两个钩子——一个用于方向(上/下/无),另一个用于实际位置

像这样使用:

useScrollPosition(position => {
console.log(position)
})


useScrollDirection(direction => {
console.log(direction)
})

下面是钩子:

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)
})
}
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

我就是这么做的,而且效果很好。

如果你发现上面的答案对你不起作用,试试这个:

React.useEffect(() => {
document.addEventListener('wheel', yourCallbackHere)
return () => {
document.removeEventListener('wheel', yourCallbackHere)
}
}, [yourCallbackHere])

基本上,你需要尝试document而不是windowwheel而不是scroll

编码快乐!

我经常收到关于渲染的警告。这段代码可以工作,但不确定它是否是最好的解决方案。

   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);
}
}, []);