简体   繁体   English

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

[英]Update style of a component onScroll in React.js

I have built a component in React which is supposed to update its own style on window scroll to create a parallax effect.我在 React 中构建了一个组件,它应该在窗口滚动时更新自己的样式以创建视差效果。

The component render method looks like this:组件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>
    );
  }

This doesn't work because React doesn't know that the component has changed, and therefore the component is not rerendered.这不起作用,因为 React 不知道组件已更改,因此不会重新渲染组件。

I've tried storing the value of itemTranslate in the state of the component, and calling setState in the scroll callback.我尝试将itemTranslate的值存储在组件的状态中,并在滚动回调中调用setState However, this makes scrolling unusable as this is terribly slow.然而,这使得滚动无法使用,因为这非常慢。

Any suggestion on how to do this?关于如何做到这一点的任何建议?

You should bind the listener in componentDidMount , that way it's only created once.您应该在componentDidMount绑定侦听器,这样它只会创建一次。 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
    });
},

You can pass a function to the onScroll event on the React element: https://facebook.github.io/react/docs/events.html#ui-events您可以将函数传递给 React 元素上的onScroll事件: https : onScroll

<ScrollableComponent
 onScroll={this.handleScroll}
/>

Another answer that is similar: https://stackoverflow.com/a/36207913/1255973另一个类似的答案: https : //stackoverflow.com/a/36207913/1255973

with hooks带钩子

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

function MyApp () {

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

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

  console.log(offset); 
};

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.对我来说没有性能问题。

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:为了帮助这里任何在使用 Austins 答案时注意到滞后行为/性能问题的人,并想要一个使用评论中提到的 refs 的示例,这是我用来切换类以进行向上/向下滚动图标的示例:

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:并以与 Austin 提到的相同的方式添加/删除您的处理程序:

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

docs on the refs . refs上的文档。

I found that I can't successfully add the event listener unless I pass true like so:我发现除非我像这样传递 true 否则我无法成功添加事件侦听器:

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

An example using classNames , React hooks useEffect , useState and styled-jsx :一个使用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

Function component example using useEffect:使用 useEffect 的函数组件示例:

Note : You need to remove the event listener by returning a "clean up" function in useEffect.注意:您需要通过在 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>
  );
}

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

Update for an answer with React Hooks使用 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)
  })
}

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:我在这里打赌是使用带有新钩子的 Function 组件来解决它,但不是像以前的答案那样使用useEffect ,我认为正确的选项是useLayoutEffect有一个重要原因:

The signature is identical to useEffect, but it fires synchronously after all DOM mutations.签名与 useEffect 相同,但在所有 DOM 突变后同步触发。

This can be found in React documentation .这可以在React 文档中找到。 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.如果我们改用useEffect并重新加载已经滚动的页面, useEffect将为 false 并且我们的类将不会被应用,从而导致不需要的行为。

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

Here is another example using HOOKS fontAwesomeIcon and Kendo UI React下面是使用挂钩fontAwesomeIcon和剑道UI反应的其他例子
[![screenshot here][1]][1] [![这里的截图][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

If you find the above answers not working for you, try this:如果您发现上述答案不适合您,请尝试以下操作:

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

Basically, you need to try document instead of window , and wheel instead of scroll .基本上,您需要尝试document而不是window ,而wheel而不是scroll

Happy coding!快乐编码!

I solved the problem via using and modifying CSS variables.我通过使用和修改 CSS 变量解决了这个问题。 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导航栏.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导航栏.module.css

.Navbar {
    background: var(--navbar-background-color);
}
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> 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.我就是这样做的,而且效果很好。

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

To expand on @Austin's answer, you should add this.handleScroll = this.handleScroll.bind(this) to your constructor:要扩展this.handleScroll = this.handleScroll.bind(this)的答案,您应该将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
    });
},
...

This gives handleScroll() access to the proper scope when called from the event listener.当从事件侦听器调用时,这使handleScroll()可以访问正确的范围。

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.另请注意,您不能在addEventListenerremoveEventListener方法中执行.bind(this) ,因为它们每个都会返回对不同函数的引用,并且在组件卸载时不会删除事件。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM