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

Posted

技术标签:

【中文标题】在 React.js 中更新组件 onScroll 的样式【英文标题】:Update style of a component onScroll in React.js 【发布时间】:2015-06-25 21:02:11 【问题描述】:

我在 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。但是,这会导致滚动无法使用,因为这非常慢。

关于如何做到这一点的任何建议?

【问题讨论】:

永远不要在渲染方法中绑定外部事件处理程序。渲染方法(以及您在同一线程中从 render 调用的任何其他自定义方法)应该只关注与渲染/更新 React 中的实际 DOM 相关的逻辑。相反,如下面的@AustinGreco 所示,您应该使用给定的 React 生命周期方法来创建和删除您的事件绑定。这使得它在组件内部自成一体,并通过确保在卸载使用它的组件时移除事件绑定来确保不会泄漏。 【参考方案1】:

您应该在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
    );
,

【讨论】:

我发现动画滚动事件中的 setState'ing 不稳定。我必须使用 refs 手动设置组件的样式。 handleScroll 中的“this”会指向什么?在我的情况下,它是“窗口”而不是组件。我最终将组件作为参数传递 @yuji 您可以通过在构造函数中绑定 this 来避免传递组件:this.handleScroll = this.handleScroll.bind(this) 将在 handleScroll 中将 this 绑定到组件,而不是窗口。 请注意 srcElement 在 Firefox 中不可用。 对我不起作用,但是将 scrollTop 设置为 event.target.scrollingElement.scrollTop【参考方案2】:

为了帮助这里的任何人在使用 Austins 答案时注意到滞后的行为/性能问题,并希望使用 cmets 中提到的 refs 的示例,这是我用于切换类以向上/向下滚动的示例图标:

在渲染方法中:

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

在处理方法中:

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

refs 上的文档。

【讨论】:

你拯救了我的一天!对于更新,此时实际上不需要使用 jquery 来修改类名,因为它已经是原生 DOM 元素。所以你可以简单地做this.scrollIcon.className = whatever-you-want 这个解决方案打破了 React 封装,尽管我仍然不确定在没有滞后行为的情况下解决这个问题 - 也许一个去抖动的滚动事件(可能在 200-250 毫秒)将是一个解决方案 nope debounced scroll 事件仅有助于使滚动更顺畅(在非阻塞意义上),但在 DOM 中应用状态更新需要 500 毫秒到一秒:/ 我也使用了这个解决方案,+1。我同意你不需要 jQuery:只需使用 classNameclassList。另外,我不需要window.addEventListener():我只是用了React的onScroll,只要你不更新props/state,它也一样快!【参考方案3】:

您可以将函数传递给 React 元素上的 onScroll 事件:https://facebook.github.io/react/docs/events.html#ui-events

<ScrollableComponent
 onScroll=this.handleScroll
/>

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

【讨论】:

与手动添加事件侦听器到@AustinGreco 提到的窗口元素相比,这种方法有什么好处/缺点吗? @Dennis 一个好处是您不必手动添加/删除事件侦听器。如果您在整个应用程序中手动管理多个事件侦听器,这可能是一个简单的示例,但很容易忘记在更新时正确删除它们,这可能导致内存错误。如果可能,我会一直使用内置版本。 值得注意的是,这会将滚动处理程序附加到组件本身,而不是附加到窗口,这是一个非常不同的事情。 @Dennis onScroll 的好处是它更跨浏览器和更高性能。如果您可以使用它,您可能应该使用它,但在像 OP 这样的情况下它可能没有用 我根本无法让上面的这个例子工作。有人可以为我提供如何使用 React 的 onScroll 合成事件的链接吗? @youjin ios 上的某些 IE 和 Safari 版本在使用 addEventListener 和滚动时可能会有点不稳定,而 jQuery 为您解决了很多问题(这就是 jQuery 的重点)。如果您好奇,请查看浏览器对两者的支持。我不确定 jQuery 是否比 vanilla js 更高效(实际上我确定不是),但是将滚动处理程序附加到元素本身而不是 window 是因为事件不必冒泡向上通过DOM来处理。不过总有取舍..【参考方案4】:

我制作响应式导航栏的解决方案(位置:不滚动时为“相对”,滚动时固定,不在页面顶部)

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
        >

对我来说没有性能问题。

【讨论】:

你也可以使用一个假标题,它本质上只是一个占位符。所以你有你的固定标题,在它下面你有你的占位符 fakeheader 位置:相对。 没有性能问题,因为这不能解决问题中的视差挑战。 这怎么行? “this”不在其范围内..【参考方案5】:

如果您对正在滚动的子组件感兴趣,那么这个示例可能会有所帮助: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>
    )
  

【讨论】:

【参考方案6】:

要扩展@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),因为它们将各自返回对不同函数的引用,并且在组件卸载时不会删除该事件。

【讨论】:

【参考方案7】:

我发现除非像这样传递 true,否则无法成功添加事件监听器:

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

【讨论】:

它工作正常。但是你能理解为什么我们必须将真正的布尔值传递给这个监听器吗? 来自 w3schools:[w3schools.com/jsref/met_document_addeventlistener.asp]userCapture:可选。一个布尔值,指定事件是在捕获阶段还是在冒泡阶段执行。可能的值: true - 事件处理程序在捕获阶段执行 false - 默认值。事件处理程序在冒泡阶段执行【参考方案8】:

我通过使用和修改 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);

【讨论】:

【参考方案9】:

使用 useEffect 的函数组件示例:

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

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: scrollYpx</div>
    </div>
  );

【讨论】:

【参考方案10】:

使用classNames、React hooks useEffect、useState 和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

【讨论】:

【参考方案11】:

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

签名与 useEffect 相同,但它是同步触发的 在所有 DOM 突变之后。

这可以在React documentation 中找到。如果我们改用 useEffect 并重新加载已经滚动的页面,则 scrolled 将为 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>
  )

【讨论】:

我很好奇useLayoutEffect。我想看看你提到了什么。 如果您不介意,您能否提供一个 repo + 发生这种情况的视觉示例?与useLayoutEffect 相比,我无法重现您在此处提到的useEffect 问题。 当然! https://github.com/calderon/uselayout-vs-uselayouteffect。昨天发生在我身上的类似行为。顺便说一句,我是一个反应新手,可能我完全错了 :D :D 其实我已经尝试过很多次了,重新加载了很多次,有时它会出现红色而不是蓝色的标题,这意味着它有时会应用.Header--scrolled类,但100%次@ 987654334@ 正确应用感谢 useLayoutEffect。 我将 repo 移至 github.com/calderon/useeffect-vs-uselayouteffect【参考方案12】:

带钩子:

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

【讨论】:

这是迄今为止最有效和最优雅的答案。谢谢你。 如果你走这条路,一定要在组件卸载时使用cleanup function来移除监听器。 这甚至没有使用 addEventListenerremoveEventListener 并且有 30 个赞。我想知道我们有多少内存泄漏的应用程序......正如布拉德利格里菲斯所说,使用useEffectcleanup。 我不确定,但我认为这不会导致内存泄漏,因为onscroll 是不是一个窗口上只有一个onscroll,而可能有很多eventListener s。这就是为什么在这种情况下也不需要清理功能。但是,如果我错了,请纠正我。相关:***.com/questions/60745475/… 如果您在应用程序中只有一个滚动事件,但如果您在同一页面中有两个滚动事件,这将非常有效。第二个将覆盖第一个。所以我建议使用事件监听器并在组件卸载时清理它。【参考方案13】:

这是另一个使用 HOOKS fontAwesomeIcon 和 Kendo UI React 的示例 [![截图][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.stack.imgur.com/ZquHI.png

【讨论】:

这太棒了。我在 useEffect() 使用 window.onscroll() 更改导航栏粘性状态时遇到问题...通过此答案发现 window.addEventListener() 和 window.removeEventListener() 是控制粘性的正确方法带有功能组件的导航栏...谢谢!【参考方案14】:

使用 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)
  )

【讨论】:

【参考方案15】:
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 ( &lt;div className="main" style= boxShadow: this.state.change ? 0px 6px 12px rgba(3,109,136,0.14):none &gt;&lt;/div&gt;

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

【讨论】:

【参考方案16】:

如果您发现上述答案不适合您,请尝试以下操作:

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

基本上,您需要尝试document 而不是window,以及wheel 而不是scroll

编码愉快!

【讨论】:

非常感谢,为什么要使用***,这是新功能?***工作正常,这个小任务浪费了 2-3 个小时【参考方案17】:

我经常收到有关渲染的警告。此代码有效,但不确定它是否是最佳解决方案。

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

【讨论】:

以上是关于在 React.js 中更新组件 onScroll 的样式的主要内容,如果未能解决你的问题,请参考以下文章

我可以在 React.js 中更新组件的 props 吗?

React.js - 功能组件中的状态未更新[重复]

在 react js 功能组件中调度操作后如何立即在 redux store 中使用更新的值

通过从非反应组件调度操作来更新 React 组件

React Js使用onChange将数组内的值更新到子组件

React js从父事件中更新子状态