Cập nhật kiểu của một thành phần onScroll trong React.js


132

Tôi đã xây dựng một thành phần trong React, được cho là cập nhật kiểu riêng của nó trên cuộn cửa sổ để tạo hiệu ứng thị sai.

renderPhương thức thành phần trông như thế này:

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

Điều này không hoạt động vì React không biết rằng thành phần đã thay đổi và do đó thành phần này không được đăng ký lại.

Tôi đã thử lưu trữ giá trị itemTranslateở trạng thái của thành phần và gọisetState trong cuộc gọi lại cuộn. Tuy nhiên, điều này làm cho việc cuộn không thể sử dụng được vì điều này rất chậm.

Bất kỳ đề nghị về cách làm điều này?


9
Không bao giờ ràng buộc một trình xử lý sự kiện bên ngoài trong một phương thức kết xuất. Các phương thức kết xuất (và bất kỳ phương thức tùy chỉnh nào khác mà bạn gọi rendertrong cùng một luồng) chỉ nên liên quan đến logic liên quan đến kết xuất / cập nhật DOM thực tế trong React. Thay vào đó, như được hiển thị bởi @AustinGreco bên dưới, bạn nên sử dụng các phương thức vòng đời React đã cho để tạo và xóa liên kết sự kiện của bạn. Điều này làm cho nó tự chứa bên trong thành phần và đảm bảo không bị rò rỉ bằng cách đảm bảo ràng buộc sự kiện được loại bỏ nếu / khi thành phần sử dụng nó không được đếm.
Mike Driver

Câu trả lời:


232

Bạn nên ràng buộc người nghe componentDidMounttheo cách đó chỉ được tạo một lần. Bạn sẽ có thể lưu trữ phong cách ở trạng thái, người nghe có lẽ là nguyên nhân của vấn đề hiệu suất.

Một cái gì đó như thế này:

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
Tôi thấy rằng setState'ing bên trong sự kiện cuộn cho hoạt hình là khó khăn. Tôi đã phải tự thiết lập kiểu dáng của các thành phần bằng cách sử dụng ref.
Ryan Rho

1
"Cái này" bên trong handScroll sẽ được trỏ đến cái gì? Trong trường hợp của tôi, nó là "cửa sổ" không phải là thành phần. Tôi kết thúc việc chuyển thành phần dưới dạng tham số
yuji

10
@yuji bạn có thể tránh cần phải truyền thành phần bằng cách ràng buộc điều này trong hàm tạo: this.handleScroll = this.handleScroll.bind(this)sẽ liên kết phần này handleScrollvới thành phần, thay vì cửa sổ.
Matt Parrilla

1
Lưu ý rằng srcEuity không có sẵn trong Firefox.
Paulin Trognon

2
không làm việc cho tôi, nhưng những gì đã làm là thiết lập scrollTop thànhevent.target.scrollingElement.scrollTop
George

31

Bạn có thể chuyển một hàm cho onScrollsự kiện trên phần tử React: https://facebook.github.io/react/docs/events.html#ui-events

<ScrollableComponent
 onScroll={this.handleScroll}
/>

Một câu trả lời khác cũng tương tự: https://stackoverflow.com/a/36207913/1255973


5
Có bất kỳ lợi ích / hạn chế nào đối với phương pháp này so với việc thêm thủ công trình nghe sự kiện vào phần tử cửa sổ @AustinGreco đã đề cập không?
Dennis

2
@Dennis Một lợi ích là bạn không phải tự thêm / xóa trình nghe sự kiện. Mặc dù đây có thể là một ví dụ đơn giản nếu bạn quản lý thủ công một số trình lắng nghe sự kiện trên ứng dụng của mình, thật dễ dàng để quên loại bỏ chúng đúng cách khi cập nhật, điều này có thể dẫn đến lỗi bộ nhớ. Tôi sẽ luôn sử dụng phiên bản tích hợp nếu có thể.
F Lekschas

4
Điều đáng chú ý là điều này gắn một trình xử lý cuộn vào chính thành phần, chứ không phải cho cửa sổ, đó là một điều rất khác. @Dennis Lợi ích của onScroll là nó có nhiều trình duyệt chéo và hiệu suất cao hơn. Nếu bạn có thể sử dụng nó, có lẽ bạn nên sử dụng nó, nhưng nó có thể không hữu ích trong các trường hợp như của OP
Beau

20

Giải pháp của tôi để tạo một thanh điều hướng phản hồi (vị trí: 'tương đối' khi không cuộn và cố định khi cuộn và không ở đầu trang)

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

Không có vấn đề hiệu suất cho tôi.


Bạn cũng có thể sử dụng một tiêu đề giả mà về cơ bản chỉ là một trình giữ chỗ. Vì vậy, bạn đã có tiêu đề cố định của bạn và bên dưới tiêu đề bạn đã có trình giả mạo giữ chỗ với vị trí: tương đối.
robins_

Không có vấn đề về hiệu suất vì điều này không giải quyết được thách thức thị sai trong câu hỏi.
juanitogan

19

để giúp đỡ bất cứ ai ở đây nhận thấy các vấn đề về hành vi / hiệu suất chậm khi sử dụng câu trả lời của Austins và muốn có một ví dụ sử dụng các ref được đề cập trong các bình luận, đây là một ví dụ tôi đang sử dụng để chuyển đổi một lớp cho biểu tượng cuộn lên / xuống:

Trong phương thức kết xuất:

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

Trong phương thức xử lý:

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

Và thêm / xóa trình xử lý của bạn giống như cách Austin đã đề cập:

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

tài liệu về các ref .


4
Bạn đã cứu ngày của tôi! Để cập nhật, bạn thực sự không cần sử dụng jquery để sửa đổi tên lớp tại thời điểm này, vì nó đã là một thành phần DOM gốc. Vì vậy, bạn có thể chỉ cần làm this.scrollIcon.className = whatever-you-want.
phía nam

2
giải pháp này phá vỡ đóng gói React mặc dù tôi vẫn không chắc chắn về cách giải quyết vấn đề này mà không có hành vi chậm trễ - có thể là một sự kiện cuộn đã được công bố (có thể 200-250 ms) sẽ là một giải pháp ở đây
Jordan

Không phải sự kiện cuộn đã được công bố chỉ giúp việc cuộn mượt mà hơn (theo nghĩa không bị chặn), nhưng phải mất 500ms đến một giây để các bản cập nhật được áp dụng trong DOM: /
Jordan

Tôi cũng đã sử dụng giải pháp này, +1. Tôi đồng ý bạn không cần jQuery: chỉ cần sử dụng classNamehoặc classList. Ngoài ra, tôi không cầnwindow.addEventListener() : Tôi chỉ sử dụng React's onScrollvà nó nhanh như vậy, miễn là bạn không cập nhật đạo cụ / trạng thái!
Benjamin

13

Tôi thấy rằng tôi không thể thêm thành công người nghe sự kiện trừ khi tôi vượt qua đúng như vậy:

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

Nó đang hoạt động. Nhưng bạn có thể hiểu tại sao chúng ta phải truyền boolean thực sự cho người nghe này không.
shah chaitanya

2
Từ w3schools: [ w3schools.com/jsref/met_document_addeventlistener.asp] userCapture : Tùy chọn. Giá trị Boolean xác định xem sự kiện sẽ được thực hiện trong khi bắt hoặc trong pha sủi bọt. Các giá trị có thể: true - Trình xử lý sự kiện được thực thi trong pha bắt giữ sai- Mặc định. Người xử lý sự kiện được thực hiện trong giai đoạn sủi bọt
Jean-Marie Dalmasso

12

Một ví dụ sử dụng classNames , React hook 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
Lưu ý rằng vì useEffect này không có đối số thứ hai, nên nó sẽ chạy mỗi khi Tiêu đề hiển thị.
Jordan

2
@Jordan bạn nói đúng! Lỗi của tôi viết này ở đây. Tôi sẽ chỉnh sửa câu trả lời. Cảm ơn rât nhiều.
giovannipds

8

với móc

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

function MyApp () {

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

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

  console.log(offset); 
};

Chính xác những gì tôi cần. Cảm ơn!
aabbccsmith

Đây là câu trả lời hiệu quả và thanh lịch nhất trong tất cả. Cảm ơn vì điều đó.
Chigozie Orunta

Điều này cần quan tâm nhiều hơn, hoàn hảo.
Anders Kitson

6

Ví dụ thành phần chức năng sử dụng useEffect:

Lưu ý : Bạn cần xóa trình lắng nghe sự kiện bằng cách trả về chức năng "dọn sạch" trong useEffect. Nếu bạn không, mỗi khi thành phần cập nhật, bạn sẽ có một trình nghe cuộn cửa sổ bổ sung.

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

Lưu ý rằng vì useEffect này không có đối số thứ hai, nên nó sẽ chạy mỗi khi thành phần hiển thị.
Jordan

Điểm tốt. Chúng ta có nên thêm một mảng trống vào lệnh gọi useEffect không?
Richard

Đó là những gì tôi sẽ làm :)
Jordan

5

Nếu điều bạn quan tâm là một thành phần con đang cuộn, thì ví dụ này có thể hữu ích: 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

Tôi đã giải quyết vấn đề thông qua việc sử dụng và sửa đổi các biến CSS. Bằng cách này, tôi không phải sửa đổi trạng thái thành phần gây ra vấn đề về hiệu suất.

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

Đặt cược của tôi ở đây là sử dụng các thành phần Hàm với các móc mới để giải quyết nó, nhưng thay vì sử dụng useEffectnhư trong các câu trả lời trước, tôi nghĩ rằng tùy chọn chính xác sẽ là useLayoutEffectvì một lý do quan trọng:

Chữ ký này giống hệt với useEffect, nhưng nó kích hoạt đồng bộ sau tất cả các đột biến DOM.

Điều này có thể được tìm thấy trong tài liệu React . Nếu chúng tôi sử dụng useEffectthay thế và chúng tôi tải lại trang đã được cuộn, cuộn sẽ là sai và lớp của chúng tôi sẽ không được áp dụng, gây ra một hành vi không mong muốn.

Một ví dụ:

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

Một giải pháp khả thi cho vấn đề có thể là 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>
  )
}

Tôi tò mò về useLayoutEffect. Tôi đang cố gắng xem những gì bạn đã đề cập.
giovannipds

Nếu bạn không phiền, bạn có thể vui lòng cung cấp repo + ví dụ trực quan về điều này xảy ra không? Tôi chỉ không thể tái tạo những gì bạn đã đề cập như một vấn đề useEffectở đây so với useLayoutEffect.
giovannipds

Chắc chắn rồi! https://github.com/calderon/uselayout-vs-uselayouteffect . Điều này đã xảy ra với tôi chỉ ngày hôm qua với một hành vi tương tự. BTW, tôi là một người mới phản ứng và có thể tôi hoàn toàn sai: D: D
Calderón

Trên thực tế, tôi đã thử điều này nhiều lần, tải lại rất nhiều và đôi khi nó xuất hiện tiêu đề màu đỏ thay vì màu xanh lam, điều đó có nghĩa là .Header--scrolledđôi khi nó đang áp dụng lớp, nhưng 100% .Header--scrolledLayoutđược áp dụng chính xác nhờ useLayoutEffect.
Calderón


1

Dưới đây là một ví dụ khác sử dụng phông chữ HOOKSAwgieIcon và Kendo UI React
[! [Ảnh chụp màn hình tại đây] [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

Điều này thật tuyệt. Tôi đã gặp sự cố khi sử dụng thanh điều hướng với một thành phần chức năng ... cảm ơn!
Michael

1

Cập nhật câu trả lời với React Hook

Đây là hai móc - một cho hướng (lên / xuống / không) và một cho vị trí thực tế

Sử dụng như thế này:

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

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

Dưới đây là các móc:

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

Để mở rộng câu trả lời của @ Austin, bạn nên thêm this.handleScroll = this.handleScroll.bind(this)vào hàm tạo của mình:

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

Điều này cho phép handleScroll()truy cập vào phạm vi thích hợp khi được gọi từ người nghe sự kiện.

Ngoài ra, hãy lưu ý rằng bạn không thể thực hiện .bind(this)trong addEventListenerhoặc removeEventListenerphương thức bởi vì mỗi phương thức sẽ trả về các tham chiếu đến các chức năng khác nhau và sự kiện sẽ không bị xóa khi thành phần ngắt kết nối.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.