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


133

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

关于如何执行此操作的任何建议?


9
切勿在render方法内绑定外部事件处理程序。渲染方法(以及您render在同一线程中调用的任何其他自定义方法)应仅与涉及在React中渲染/更新实际DOM的逻辑有关。相反,如下面的@AustinGreco所示,您应该使用给定的React生命周期方法创建和删除事件绑定。这使得它在组件内部是独立的,并且通过确保在卸载使用它的组件时/删除事件绑定来确保不泄漏。
Mike Driver

Answers:


232

您应该在中绑定侦听器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
    });
},

26
我发现动画的滚动事件中的setState'ing很不稳定。我必须使用引用手动设置组件的样式。
瑞安·罗

1
handleScroll内部的“ this”将指向什么?就我而言,它不是“窗口”组件。我最终将组件作为参数传递
yuji

10
@yuji您可以通过在构造函数中绑定此组件来避免传递组件:this.handleScroll = this.handleScroll.bind(this)将其绑定handleScroll到组件而不是window中。
马特·帕里利亚

1
请注意,srcElement在Firefox中不可用。
Paulin Trognon

2
不适用于我,但所做的是将scrollTop设置为event.target.scrollingElement.scrollTop
乔治

31

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

<ScrollableComponent
 onScroll={this.handleScroll}
/>

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


5
与手动将事件侦听器添加到提到的窗口元素@AustinGreco相比,此方法有什么好处/缺点?
丹尼斯

2
@Dennis的一个好处是您不必手动添加/删除事件侦听器。尽管这可能是一个简单的示例,但是如果您在整个应用程序中手动管理多个事件侦听器,则很容易忘记在更新时正确删除它们,这可能会导致内存错误。如果可能的话,我将始终使用内置版本。
F Lekschas

4
值得注意的是,这会将滚动处理程序附加到组件本身,而不是附加到窗口,这是非常不同的事情。@Dennis onScroll的优点是它的跨浏览器和性能更高。如果您可以使用它,则可能应该使用,但是在像OP那样的情况下它可能没有用
Beau

20

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

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

我没有任何性能问题。


您还可以使用伪造的标题,它实际上只是一个占位符。因此,您拥有固定的标头,在其下方,您的占位符假标头的位置为:relative。
robins_

没有性能问题,因为这不能解决问题中的视差挑战。
juanitogan

19

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

在render方法中:

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

并按照奥斯汀提到的方式添加/删除处理程序:

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

refs上的文档。


4
你救了我的一天!对于更新,实际上,此时您无需使用jquery来修改类名,因为它已经是本机DOM元素。所以你可以简单地做this.scrollIcon.className = whatever-you-want
southp

2
这个解决方案打破了React封装,尽管我仍然不确定是否存在滞后行为-也许去抖动的滚动事件(可能在200-250毫秒)是这里的解决方案
Jordan

nope去抖动的滚动事件仅有助于使滚动更平滑(在非阻塞意义上),但是要在DOM中应用状态更新需要500毫秒至一秒:/
Jordan

我也使用了此解决方案+1。我同意您不需要jQuery:只需使用className或即可classList。另外,我不需要window.addEventListener():我只是使用React的onScroll,只要您不更新道具/状态,它就一样快!
本杰明

13

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

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

工作正常 但是您能弄清楚为什么我们必须将真正的布尔值传递给此侦听器。
莎·查塔尼亚

2
来自w3schools:[ w3schools.com/jsref/met_document_addeventlistener.asp] userCapture:可选。一个布尔值,指定是在捕获阶段还是冒泡阶段执行事件。可能的值:true-在捕获阶段执行事件处理程序false-默认。事件处理程序冒泡阶段执行
让-玛丽·Dalmasso

12

一个使用classNames的示例,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

1
请注意,由于此useEffect没有第二个参数,因此它将在Header渲染时每次运行。
约旦

2
@乔丹,你是对的!我在这里写这个错误。我将编辑答案。非常感谢你。
giovannipds

8

带钩

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

function MyApp () {

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

  useEffect(() => {
    window.onscroll = () => {
      setOffset(window.pageYOffset)
    }
  }, []);

  console.log(offset); 
};

正是我所需要的。谢谢!
aabbccsmith

这是迄今为止最有效和最优雅的答案。谢谢你
Chigozie Orunta

这需要更多的关注,完善。
安德斯·基特森

6

使用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: {scrollY}px</div>
    </div>
  );
}

请注意,由于此useEffect没有第二个参数,因此它将在每次组件渲染时运行。
约旦

好点子。我们是否应该在useEffect调用中添加一个空数组?
理查德

那就是我会做的:)
乔丹

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

1

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

1

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

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

这可以在React文档中找到。如果useEffect改为使用,而是重新加载已经滚动的页面,则滚动将为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。我想看看你提到的内容。
giovannipds

如果您不介意,能否请您提供此情况的回购和视觉示例?与相比,我只是无法重现您在useEffect此处提到的问题useLayoutEffect
giovannipds

当然! https://github.com/calderon/uselayout-vs-uselayouteffect。就在昨天,我发生了类似的情况。顺便说一句,我是一个新手,反应以及可能我完全错了:d:d
卡尔德龙

实际上,我已经尝试了很多次,重新加载了很多次,有时它以红色而不是蓝色显示标题,这意味着它.Header--scrolled有时在应用类,但是.Header--scrolledLayout感谢useLayoutEffect正确地应用了100%次。
卡尔德隆


1

下面是使用另一个例子挂钩 fontAwesomeIcon和剑道UI阵营
[![截图这里] [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()是控制我的粘性的正确方法导航栏带有功能组件...谢谢!
迈克尔·

1

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

0

要扩展@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()从事件侦听器调用时,可以访问适当的作用域。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.