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동일한 스레드에서 호출하는 다른 사용자 정의 메소드 )는 React에서 실제 DOM을 렌더링 / 업데이트하는 것과 관련된 논리에만 관심이 있어야합니다. 대신 아래 @AustinGreco에 표시된 것처럼 지정된 React 라이프 사이클 메소드를 사용하여 이벤트 바인딩을 작성하고 제거해야합니다. 이렇게하면 구성 요소 내부에 자체적으로 포함되며 구성 요소를 사용하는 구성 요소를 마운트 해제 할 때 / 언제든지 이벤트 바인딩을 제거하여 누출을 방지 할 수 있습니다.
Mike Driver

답변:


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이 고르지 않다는 것을 알았습니다. 심판을 사용하여 구성 요소 스타일을 수동으로 설정해야했습니다.
Ryan Rho

1
handleScroll 내부의 "this"는 무엇을 가리 킵니까? 제 경우에는 구성 요소가 아닌 "창"입니다. 나는 매개 변수로 구성 요소를 통과 끝
유우지가

10
@ yuji 당신은 생성자에서 이것을 바인딩하여 구성 요소를 전달할 필요가 없습니다 : this.handleScroll = this.handleScroll.bind(this)이것을 handleScroll창 대신 구성 요소에 바인딩합니다 .
매트 파 릴라

1
Firefox에서는 srcElement를 사용할 수 없습니다.
Paulin Trognon

2
나를 위해 작동하지 않았지만 scrollTop을 설정 한 것은event.target.scrollingElement.scrollTop
George

31

https://facebook.github.io/react/docs/events.html#ui-eventsonScroll 에서 React 요소 의 이벤트에 함수를 전달할 수 있습니다 .

<ScrollableComponent
 onScroll={this.handleScroll}
/>

비슷한 또 다른 답변 : https : //.com/a/36207913/1255973


5
@AustinGreco가 언급 한 창 요소에 이벤트 리스너를 수동으로 추가하는 것 보다이 방법에 대한 이점 / 단점이 있습니까?
Dennis

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

나에게 성능 문제가 없습니다.


본질적으로 단지 자리 표시자인 가짜 헤더를 사용할 수도 있습니다. 따라서 고정 헤더가 있고 그 아래에 position : relative가있는 자리 표시 자 fakeheader가 있습니다.
robins_

문제의 시차 문제를 해결하지 못하므로 성능 문제가 없습니다.
juanitogan

19

Austins 응답을 사용할 때 게으른 동작 / 성능 문제를 발견하고 주석에 언급 된 참조를 사용하는 예제를 원하는 사람을 돕기 위해 스크롤 업 / 다운 아이콘으로 클래스를 토글하는 데 사용한 예제는 다음과 같습니다.

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

그리고 Austin이 언급 한 것과 같은 방식으로 처리기를 추가 / 제거하십시오.

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

심판 에 대한 문서 .


4
당신은 내 하루를 구했다! 업데이트를 위해 실제로는 jquery를 사용하여 클래스 이름을 수정할 필요가 없습니다. 이미 네이티브 DOM 요소이기 때문입니다. 따라서 간단하게 할 수 this.scrollIcon.className = whatever-you-want있습니다.
southp

2
이 솔루션은 여전히 ​​게으른 동작 없이이 문제를 해결할 방법이 확실하지 않지만 React 캡슐화를 중단합니다. 어쩌면 디 바운스 스크롤 이벤트 (어쩌면 200-250ms)가 여기에 해결책이 될 것입니다.
Jordan

nope debounced scroll 이벤트는 스크롤을 부드럽게 만드는 데 도움이되지만 (블로킹이 아닌 의미로) DOM에 적용되는 상태 업데이트가 초당 500ms에서 1 초가 걸립니다 ./
Jordan

이 솔루션도 사용했습니다 (+1). 단지 사용 : 당신이 jQuery를 필요하지 않습니다 동의 className또는 classList. 또한 필요하지 않았습니다window.addEventListener() . 방금 React 's를 사용 onScroll했으며 소품 / 상태를 업데이트하지 않는 한 빠릅니다!
Benjamin

13

다음과 같이 true를 전달하지 않으면 이벤트 리스너를 성공적으로 추가 할 수 없습니다.

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

작동합니다. 그러나이 리스너에게 진정한 부울을 전달해야하는 이유를 알 수 있습니까?
shah chaitanya

2
w3schools에서 : [ w3schools.com/jsref/met_document_addeventlistener.asp] userCapture : 선택 사항. 이벤트를 캡처 또는 버블 링 단계에서 실행할지 여부를 지정하는 부울 값입니다. 가능한 값 : true-이벤트 핸들러가 캡처 단계에서 실행됩니다. false- 기본값. 이벤트 핸들러는 버블 링 단계에서 실행된다
장 마리 Dalmasso를

12

classNames , React 후크 useEffect , useStatestyled-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에는 두 번째 인수가 없으므로 헤더가 렌더링 될 때마다 실행됩니다.
요르단

2
@Jordan 당신이 맞아요! 내 실수는 여기에 쓰는 중입니다. 답을 편집하겠습니다. 대단히 감사합니다.
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

이것은 더 많은주의가 필요하고 완벽합니다.
Anders Kitson

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 호출에 빈 배열을 추가해야합니까?
Richard

그것이 내가하는 것입니다 :)
요르단

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

여기 내 기능은 새로운 후크가있는 함수 구성 요소를 사용하여 해결하는 것이지만 useEffect이전 답변 에서처럼 사용하는 대신 올바른 옵션이 useLayoutEffect중요한 이유 라고 생각합니다 .

서명은 useEffect와 동일하지만 모든 DOM 변이 후에 동기식으로 실행됩니다.

이것은 React documentation 에서 찾을 수 있습니다 . 우리가 사용하는 경우 useEffect대신 우리는 페이지 이미 스크롤 잘못된 것입니다 스크롤 우리의 클래스가 원치 않는 행동을 일으키는 원인이 적용되지 않습니다를 다시로드합니다.

예를 들면 :

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 . 이것은 어제 비슷한 행동으로 나에게 일어났다. BTW, 나는 반응 초보자이고 아마도 완전히 틀렸다 : D : D
Calderón

실제로 나는 이것을 여러 번 시도하고 많이 다시로드했으며 때로는 파란색 대신 빨간색으로 헤더로 표시됩니다. 이는 .Header--scrolled때때로 클래스를 적용한다는 것을 의미 하지만 .Header--scrolledLayoutuseLayoutEffect 덕분에 100 % 시간 이 올바르게 적용됩니다.
칼데론


1

HOOKS fontAwesomeIcon 및 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.stack.imgur.com/ZquHI.png

대단해. window.onscroll ()을 사용하여 스크롤에서 내 탐색 표시 줄 상태를 변경하는 데 useEffect ()에서 문제가 발생했습니다. 기능적 구성 요소가 포함 된 탐색 모음 ... 감사합니다!
마이클

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)in addEventListener또는 removeEventListener메소드는 서로 다른 함수에 대한 참조를 리턴하고 구성 요소를 마운트 해제 할 때 이벤트가 제거되지 않으므로 in 또는 메소드를 수행 할 수 없습니다 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.